summaryrefslogtreecommitdiff
path: root/silx/gui/widgets
diff options
context:
space:
mode:
Diffstat (limited to 'silx/gui/widgets')
-rw-r--r--silx/gui/widgets/PrintPreview.py74
-rw-r--r--silx/gui/widgets/RangeSlider.py198
-rw-r--r--silx/gui/widgets/UrlSelectionTable.py164
3 files changed, 380 insertions, 56 deletions
diff --git a/silx/gui/widgets/PrintPreview.py b/silx/gui/widgets/PrintPreview.py
index 94a8ed4..96af34b 100644
--- a/silx/gui/widgets/PrintPreview.py
+++ b/silx/gui/widgets/PrintPreview.py
@@ -285,7 +285,7 @@ class PrintPreviewDialog(qt.QDialog):
def addSvgItem(self, item, title=None,
comment=None, commentPosition=None,
- viewBox=None):
+ viewBox=None, keepRatio=True):
"""Add a SVG item to the scene.
:param QSvgRenderer item: SVG item to be added to the scene.
@@ -295,6 +295,8 @@ class PrintPreviewDialog(qt.QDialog):
:param QRectF viewBox: Bounding box for the item on the print page
(xOffset, yOffset, width, height). If None, use original
item size.
+ :param bool keepRatio: If True, resizing the item will preserve its
+ original aspect ratio.
"""
if not qt.HAS_SVG:
raise RuntimeError("Missing QtSvg library.")
@@ -331,35 +333,23 @@ class PrintPreviewDialog(qt.QDialog):
svgItem.setFlag(qt.QGraphicsItem.ItemIsMovable, True)
svgItem.setFlag(qt.QGraphicsItem.ItemIsFocusable, False)
- rectItemResizeRect = _GraphicsResizeRectItem(svgItem, self.scene)
+ rectItemResizeRect = _GraphicsResizeRectItem(svgItem, self.scene,
+ keepratio=keepRatio)
rectItemResizeRect.setZValue(2)
self._svgItems.append(item)
- if qt.qVersion() < '5.0':
- textItem = qt.QGraphicsTextItem(title, svgItem, self.scene)
- else:
- textItem = qt.QGraphicsTextItem(title, svgItem)
- textItem.setTextInteractionFlags(qt.Qt.TextEditorInteraction)
- title_offset = 0.5 * textItem.boundingRect().width()
- textItem.setZValue(1)
- textItem.setFlag(qt.QGraphicsItem.ItemIsMovable, True)
-
+ # Comment / legend
dummyComment = 80 * "1"
if qt.qVersion() < '5.0':
commentItem = qt.QGraphicsTextItem(dummyComment, svgItem, self.scene)
else:
commentItem = qt.QGraphicsTextItem(dummyComment, svgItem)
commentItem.setTextInteractionFlags(qt.Qt.TextEditorInteraction)
+ # we scale the text to have the legend box have the same width as the graph
scaleCalculationRect = qt.QRectF(commentItem.boundingRect())
scale = svgItem.boundingRect().width() / scaleCalculationRect.width()
- comment_offset = 0.5 * commentItem.boundingRect().width()
- if commentPosition.upper() == "LEFT":
- x = 1
- else:
- x = 0.5 * svgItem.boundingRect().width() - comment_offset * scale # fixme: centering
- commentItem.moveBy(svgItem.boundingRect().x() + x,
- svgItem.boundingRect().y() + svgItem.boundingRect().height())
+
commentItem.setPlainText(comment)
commentItem.setZValue(1)
@@ -367,17 +357,46 @@ class PrintPreviewDialog(qt.QDialog):
if qt.qVersion() < "5.0":
commentItem.scale(scale, scale)
else:
- # the correct equivalent would be:
- # rectItem.setTransform(qt.QTransform.fromScale(scalex, scaley))
commentItem.setScale(scale)
+
+ # align
+ if commentPosition.upper() == "CENTER":
+ alignment = qt.Qt.AlignCenter
+ elif commentPosition.upper() == "RIGHT":
+ alignment = qt.Qt.AlignRight
+ else:
+ alignment = qt.Qt.AlignLeft
+ commentItem.setTextWidth(commentItem.boundingRect().width())
+ center_format = qt.QTextBlockFormat()
+ center_format.setAlignment(alignment)
+ cursor = commentItem.textCursor()
+ cursor.select(qt.QTextCursor.Document)
+ cursor.mergeBlockFormat(center_format)
+ cursor.clearSelection()
+ commentItem.setTextCursor(cursor)
+ if alignment == qt.Qt.AlignLeft:
+ deltax = 0
+ else:
+ deltax = (svgItem.boundingRect().width() - commentItem.boundingRect().width()) / 2.
+ commentItem.moveBy(svgItem.boundingRect().x() + deltax,
+ svgItem.boundingRect().y() + svgItem.boundingRect().height())
+
+ # Title
+ if qt.qVersion() < '5.0':
+ textItem = qt.QGraphicsTextItem(title, svgItem, self.scene)
+ else:
+ textItem = qt.QGraphicsTextItem(title, svgItem)
+ textItem.setTextInteractionFlags(qt.Qt.TextEditorInteraction)
+ textItem.setZValue(1)
+ textItem.setFlag(qt.QGraphicsItem.ItemIsMovable, True)
+
+ title_offset = 0.5 * textItem.boundingRect().width()
textItem.moveBy(svgItem.boundingRect().x() +
0.5 * svgItem.boundingRect().width() - title_offset * scale,
svgItem.boundingRect().y())
if qt.qVersion() < "5.0":
textItem.scale(scale, scale)
else:
- # the correct equivalent would be:
- # rectItem.setTransform(qt.QTransform.fromScale(scalex, scaley))
textItem.setScale(scale)
def setup(self):
@@ -601,7 +620,8 @@ class _GraphicsResizeRectItem(qt.QGraphicsRectItem):
# following line prevents dragging along the previously selected
# item when resizing another one
scene.clearSelection()
- rect = parent.rect()
+
+ rect = parent.boundingRect()
self._x = rect.x()
self._y = rect.y()
self._w = rect.width()
@@ -655,12 +675,14 @@ class _GraphicsResizeRectItem(qt.QGraphicsRectItem):
else:
scalex = self._newRect.rect().width() / self._w
scaley = self._newRect.rect().height() / self._h
+
if qt.qVersion() < "5.0":
parent.scale(scalex, scaley)
else:
- # the correct equivalent would be:
- # rectItem.setTransform(qt.QTransform.fromScale(scalex, scaley))
- parent.setScale(scalex)
+ # apply the scale to the previous transformation matrix
+ previousTransform = parent.transform()
+ parent.setTransform(
+ previousTransform.scale(scalex, scaley))
self.scene().removeItem(self._newRect)
self._newRect = None
diff --git a/silx/gui/widgets/RangeSlider.py b/silx/gui/widgets/RangeSlider.py
index 0b72e71..0cf195c 100644
--- a/silx/gui/widgets/RangeSlider.py
+++ b/silx/gui/widgets/RangeSlider.py
@@ -31,7 +31,7 @@ from __future__ import absolute_import, division
__authors__ = ["D. Naudet", "T. Vincent"]
__license__ = "MIT"
-__date__ = "02/08/2018"
+__date__ = "26/11/2018"
import numpy as numpy
@@ -40,6 +40,17 @@ from silx.gui import qt, icons, colors
from silx.gui.utils.image import convertArrayToQImage
+class StyleOptionRangeSlider(qt.QStyleOption):
+ def __init__(self):
+ super(StyleOptionRangeSlider, self).__init__()
+ self.minimum = None
+ self.maximum = None
+ self.sliderPosition1 = None
+ self.sliderPosition2 = None
+ self.handlerRect1 = None
+ self.handlerRect2 = None
+
+
class RangeSlider(qt.QWidget):
"""Range slider with 2 thumbs and an optional colored groove.
@@ -86,6 +97,8 @@ class RangeSlider(qt.QWidget):
self.__secondValue = 1.
self.__minValue = 0.
self.__maxValue = 1.
+ self.__hoverRect = qt.QRect()
+ self.__hoverControl = None
self.__focus = None
self.__moving = None
@@ -100,6 +113,7 @@ class RangeSlider(qt.QWidget):
super(RangeSlider, self).__init__(parent)
self.setFocusPolicy(qt.Qt.ClickFocus)
+ self.setAttribute(qt.Qt.WA_Hover)
self.setMinimumSize(qt.QSize(50, 20))
self.setMaximumHeight(20)
@@ -107,6 +121,34 @@ class RangeSlider(qt.QWidget):
# Broadcast value changed signal
self.sigValueChanged.connect(self.__emitPositionChanged)
+ def event(self, event):
+ t = event.type()
+ if t == qt.QEvent.HoverEnter or t == qt.QEvent.HoverLeave or t == qt.QEvent.HoverMove:
+ return self.__updateHoverControl(event.pos())
+ else:
+ return super(RangeSlider, self).event(event)
+
+ def __updateHoverControl(self, pos):
+ hoverControl, hoverRect = self.__findHoverControl(pos)
+ if hoverControl != self.__hoverControl:
+ self.update(self.__hoverRect)
+ self.update(hoverRect)
+ self.__hoverControl = hoverControl
+ self.__hoverRect = hoverRect
+ return True
+ return hoverControl is not None
+
+ def __findHoverControl(self, pos):
+ """Returns the control at the position and it's rect location"""
+ for name in ["first", "second"]:
+ rect = self.__sliderRect(name)
+ if rect.contains(pos):
+ return name, rect
+ rect = self.__drawArea()
+ if rect.contains(pos):
+ return "groove", rect
+ return None, qt.QRect()
+
# Position <-> Value conversion
def __positionToValue(self, position):
@@ -469,10 +511,12 @@ class RangeSlider(qt.QWidget):
super(RangeSlider, self).mouseMoveEvent(event)
if self.__moving is not None:
- position = self.__xPixelToPosition(event.pos().x())
+ delta = self._SLIDER_WIDTH // 2
if self.__moving == 'first':
+ position = self.__xPixelToPosition(event.pos().x() + delta)
self.setFirstPosition(position)
else:
+ position = self.__xPixelToPosition(event.pos().x() - delta)
self.setSecondPosition(position)
def mouseReleaseEvent(self, event):
@@ -564,13 +608,13 @@ class RangeSlider(qt.QWidget):
def __sliderAreaRect(self):
return self.__drawArea().adjusted(self._SLIDER_WIDTH / 2.,
0,
- -self._SLIDER_WIDTH / 2.,
+ -self._SLIDER_WIDTH / 2. + 1,
0)
def __pixMapRect(self):
return self.__sliderAreaRect().adjusted(0,
self._PIXMAP_VOFFSET,
- 0,
+ -1,
-self._PIXMAP_VOFFSET)
def paintEvent(self, event):
@@ -579,33 +623,55 @@ class RangeSlider(qt.QWidget):
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)
+ if self.__pixmap is not None:
+ pixmapRect = self.__pixMapRect()
- 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()
+ option = qt.QStyleOptionProgressBar()
+ option.initFrom(self)
+ option.rect = area
+ option.state = (qt.QStyle.State_Enabled if self.isEnabled()
+ else 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, 0, 1))
+ painter.restore()
+
+ if self.isEnabled():
+ rect = area.adjusted(self._SLIDER_WIDTH // 2,
+ self._PIXMAP_VOFFSET,
+ -self._SLIDER_WIDTH // 2,
+ -self._PIXMAP_VOFFSET + 1)
+ painter.drawPixmap(rect,
+ self.__pixmap,
+ self.__pixmap.rect())
+ else:
+ option = StyleOptionRangeSlider()
+ option.initFrom(self)
+ option.rect = area
+ option.sliderPosition1 = self.__firstValue
+ option.sliderPosition2 = self.__secondValue
+ option.handlerRect1 = self.__sliderRect("first")
+ option.handlerRect2 = self.__sliderRect("second")
+ option.minimum = self.__minValue
+ option.maximum = self.__maxValue
+ option.state = (qt.QStyle.State_Enabled if self.isEnabled()
+ else qt.QStyle.State_None)
+ if self.__hoverControl == "groove":
+ option.state |= qt.QStyle.State_MouseOver
+ elif option.state & qt.QStyle.State_MouseOver:
+ option.state ^= qt.QStyle.State_MouseOver
+ self.drawRangeSliderBackground(painter, option, self)
- 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())
+ # Avoid glitch when moving handles
+ hoverControl = self.__moving or self.__hoverControl
for name in ('first', 'second'):
rect = self.__sliderRect(name)
@@ -613,7 +679,9 @@ class RangeSlider(qt.QWidget):
option.initFrom(self)
option.icon = self.__icons[name]
option.iconSize = rect.size() * 0.7
- if option.state & qt.QStyle.State_MouseOver:
+ if hoverControl == name:
+ option.state |= qt.QStyle.State_MouseOver
+ elif option.state & qt.QStyle.State_MouseOver:
option.state ^= qt.QStyle.State_MouseOver
if self.__focus == name:
option.state |= qt.QStyle.State_HasFocus
@@ -625,3 +693,73 @@ class RangeSlider(qt.QWidget):
def sizeHint(self):
return qt.QSize(200, self.minimumHeight())
+
+ @classmethod
+ def drawRangeSliderBackground(cls, painter, option, widget):
+ """Draw the background of the RangeSlider widget into the painter.
+
+ :param qt.QPainter painter: A painter
+ :param StyleOptionRangeSlider option: Options to draw the widget
+ :param qt.QWidget: The widget which have to be drawn
+ """
+ painter.save()
+ painter.translate(0.5, 0.5)
+
+ backgroundRect = qt.QRect(option.rect)
+ if backgroundRect.height() > 8:
+ center = backgroundRect.center()
+ backgroundRect.setHeight(8)
+ backgroundRect.moveCenter(center)
+
+ selectedRangeRect = qt.QRect(backgroundRect)
+ selectedRangeRect.setLeft(option.handlerRect1.center().x())
+ selectedRangeRect.setRight(option.handlerRect2.center().x())
+
+ highlight = option.palette.color(qt.QPalette.Highlight)
+ activeHighlight = highlight
+ selectedOutline = option.palette.color(qt.QPalette.Highlight)
+
+ buttonColor = option.palette.button().color()
+ val = qt.qGray(buttonColor.rgb())
+ buttonColor = buttonColor.lighter(100 + max(1, (180 - val) // 6))
+ buttonColor.setHsv(buttonColor.hue(), buttonColor.saturation() * 0.75, buttonColor.value())
+
+ grooveColor = qt.QColor()
+ grooveColor.setHsv(buttonColor.hue(),
+ min(255, (int)(buttonColor.saturation())),
+ min(255, (int)(buttonColor.value() * 0.9)))
+
+ selectedInnerContrastLine = qt.QColor(255, 255, 255, 30)
+
+ outline = option.palette.color(qt.QPalette.Background).darker(140)
+ if (option.state & qt.QStyle.State_HasFocus and option.state & qt.QStyle.State_KeyboardFocusChange):
+ outline = highlight.darker(125)
+ if outline.value() > 160:
+ outline.setHsl(highlight.hue(), highlight.saturation(), 160)
+
+ # Draw background groove
+ painter.setRenderHint(qt.QPainter.Antialiasing, True)
+ gradient = qt.QLinearGradient()
+ gradient.setStart(backgroundRect.center().x(), backgroundRect.top())
+ gradient.setFinalStop(backgroundRect.center().x(), backgroundRect.bottom())
+ painter.setPen(qt.QPen(outline))
+ gradient.setColorAt(0, grooveColor.darker(110))
+ gradient.setColorAt(1, grooveColor.lighter(110))
+ painter.setBrush(gradient)
+ painter.drawRoundedRect(backgroundRect.adjusted(1, 1, -2, -2), 1, 1)
+
+ # Draw slider background for the value
+ gradient = qt.QLinearGradient()
+ gradient.setStart(selectedRangeRect.center().x(), selectedRangeRect.top())
+ gradient.setFinalStop(selectedRangeRect.center().x(), selectedRangeRect.bottom())
+ painter.setRenderHint(qt.QPainter.Antialiasing, True)
+ painter.setPen(qt.QPen(selectedOutline))
+ gradient.setColorAt(0, activeHighlight)
+ gradient.setColorAt(1, activeHighlight.lighter(130))
+ painter.setBrush(gradient)
+ painter.drawRoundedRect(selectedRangeRect.adjusted(1, 1, -2, -2), 1, 1)
+ painter.setPen(selectedInnerContrastLine)
+ painter.setBrush(qt.Qt.NoBrush)
+ painter.drawRoundedRect(selectedRangeRect.adjusted(2, 2, -3, -3), 1, 1)
+
+ painter.restore()
diff --git a/silx/gui/widgets/UrlSelectionTable.py b/silx/gui/widgets/UrlSelectionTable.py
new file mode 100644
index 0000000..4ac0381
--- /dev/null
+++ b/silx/gui/widgets/UrlSelectionTable.py
@@ -0,0 +1,164 @@
+# /*##########################################################################
+# Copyright (C) 2017 European Synchrotron Radiation Facility
+#
+# This file is part of the PyMca X-ray Fluorescence Toolkit developed at
+# the ESRF by the Software group.
+#
+# 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.
+#
+#############################################################################*/
+"""Some widget construction to check if a sample moved"""
+
+__author__ = ["H. Payno"]
+__license__ = "MIT"
+__date__ = "19/03/2018"
+
+from silx.gui import qt
+from collections import OrderedDict
+from silx.gui.widgets.TableWidget import TableWidget
+from silx.io.url import DataUrl
+import functools
+import logging
+import os
+
+logger = logging.getLogger(__file__)
+
+
+class UrlSelectionTable(TableWidget):
+ """Table used to select the color channel to be displayed for each"""
+
+ COLUMS_INDEX = OrderedDict([
+ ('url', 0),
+ ('img A', 1),
+ ('img B', 2),
+ ])
+
+ sigImageAChanged = qt.Signal(str)
+ """Signal emitted when the image A change. Param is the image url path"""
+
+ sigImageBChanged = qt.Signal(str)
+ """Signal emitted when the image B change. Param is the image url path"""
+
+ def __init__(self, parent=None):
+ TableWidget.__init__(self, parent)
+ self.clear()
+
+ def clear(self):
+ qt.QTableWidget.clear(self)
+ self.setRowCount(0)
+ self.setColumnCount(len(self.COLUMS_INDEX))
+ self.setHorizontalHeaderLabels(list(self.COLUMS_INDEX.keys()))
+ self.verticalHeader().hide()
+ if hasattr(self.horizontalHeader(), 'setSectionResizeMode'): # Qt5
+ self.horizontalHeader().setSectionResizeMode(0,
+ qt.QHeaderView.Stretch)
+ else: # Qt4
+ self.horizontalHeader().setResizeMode(0, qt.QHeaderView.Stretch)
+
+ self.setSortingEnabled(True)
+ self._checkBoxes = {}
+
+ def addUrl(self, url, **kwargs):
+ """
+
+ :param url:
+ :param args:
+ :return: index of the created items row
+ :rtype int
+ """
+ assert isinstance(url, DataUrl)
+ row = self.rowCount()
+ self.setRowCount(row + 1)
+
+ _item = qt.QTableWidgetItem()
+ _item.setText(os.path.basename(url.path()))
+ _item.setFlags(qt.Qt.ItemIsEnabled | qt.Qt.ItemIsSelectable)
+ self.setItem(row, self.COLUMS_INDEX['url'], _item)
+
+ widgetImgA = qt.QRadioButton(parent=self)
+ widgetImgA.setAutoExclusive(False)
+ self.setCellWidget(row, self.COLUMS_INDEX['img A'], widgetImgA)
+ callbackImgA = functools.partial(self._activeImgAChanged, url.path())
+ widgetImgA.toggled.connect(callbackImgA)
+
+ widgetImgB = qt.QRadioButton(parent=self)
+ widgetImgA.setAutoExclusive(False)
+ self.setCellWidget(row, self.COLUMS_INDEX['img B'], widgetImgB)
+ callbackImgB = functools.partial(self._activeImgBChanged, url.path())
+ widgetImgB.toggled.connect(callbackImgB)
+
+ self._checkBoxes[url.path()] = {'img A': widgetImgA,
+ 'img B': widgetImgB}
+ self.resizeColumnsToContents()
+ return row
+
+ def _activeImgAChanged(self, name):
+ self._updatecheckBoxes('img A', name)
+ self.sigImageAChanged.emit(name)
+
+ def _activeImgBChanged(self, name):
+ self._updatecheckBoxes('img B', name)
+ self.sigImageBChanged.emit(name)
+
+ def _updatecheckBoxes(self, whichImg, name):
+ assert name in self._checkBoxes
+ assert whichImg in self._checkBoxes[name]
+ if self._checkBoxes[name][whichImg].isChecked():
+ for radioUrl in self._checkBoxes:
+ if radioUrl != name:
+ self._checkBoxes[radioUrl][whichImg].blockSignals(True)
+ self._checkBoxes[radioUrl][whichImg].setChecked(False)
+ self._checkBoxes[radioUrl][whichImg].blockSignals(False)
+
+ def getSelection(self):
+ """
+
+ :return: url selected for img A and img B.
+ """
+ imgA = imgB = None
+ for radioUrl in self._checkBoxes:
+ if self._checkBoxes[radioUrl]['img A'].isChecked():
+ imgA = radioUrl
+ if self._checkBoxes[radioUrl]['img B'].isChecked():
+ imgB = radioUrl
+ return imgA, imgB
+
+ def setSelection(self, url_img_a, url_img_b):
+ """
+
+ :param ddict: key: image url, values: list of active channels
+ """
+ for radioUrl in self._checkBoxes:
+ for img in ('img A', 'img B'):
+ self._checkBoxes[radioUrl][img].blockSignals(True)
+ self._checkBoxes[radioUrl][img].setChecked(False)
+ self._checkBoxes[radioUrl][img].blockSignals(False)
+
+ self._checkBoxes[radioUrl][img].blockSignals(True)
+ self._checkBoxes[url_img_a]['img A'].setChecked(True)
+ self._checkBoxes[radioUrl][img].blockSignals(False)
+
+ self._checkBoxes[radioUrl][img].blockSignals(True)
+ self._checkBoxes[url_img_b]['img B'].setChecked(True)
+ self._checkBoxes[radioUrl][img].blockSignals(False)
+ self.sigImageAChanged.emit(url_img_a)
+ self.sigImageBChanged.emit(url_img_b)
+
+ def removeUrl(self, url):
+ raise NotImplementedError("")