# -*- coding: utf-8 -*-
#
# Licensed under the terms of the Qwt License
# Copyright (c) 2002 Uwe Rathmann, for the original C++ code
# Copyright (c) 2015 Pierre Raybaut, for the Python translation/optimization
# (see LICENSE file for more details)
"""
QwtLegend
---------
.. autoclass:: QwtLegendData
:members:
.. autoclass:: QwtLegendLabel
:members:
.. autoclass:: QwtLegend
:members:
"""
import math
from qtpy.QtCore import QEvent, QPoint, QRect, QRectF, QSize, Qt, Signal
# qDrawWinButton,
from qtpy.QtGui import QPainter, QPalette, QPixmap
from qtpy.QtWidgets import (
QApplication,
QFrame,
QScrollArea,
QStyle,
QStyleOption,
QVBoxLayout,
QWidget,
)
from qwt.dyngrid_layout import QwtDynGridLayout
from qwt.painter import QwtPainter
from qwt.text import QwtText, QwtTextLabel
[docs]
class QwtLegendData(object):
"""
Attributes of an entry on a legend
`QwtLegendData` is an abstract container ( like `QAbstractModel` )
to exchange attributes, that are only known between to
the plot item and the legend.
By overloading `QwtPlotItem.legendData()` any other set of attributes
could be used, that can be handled by a modified ( or completely
different ) implementation of a legend.
.. seealso::
:py:class:`qwt.legend.QwtLegend`
.. note::
The stockchart example implements a legend as a tree
with checkable items
"""
# enum Mode
ReadOnly, Clickable, Checkable = list(range(3))
# enum Role
ModeRole, TitleRole, IconRole = list(range(3))
UserRole = 32
def __init__(self):
self.__map = {}
[docs]
def setValues(self, map_):
"""
Set the legend attributes
:param dict map_: Values
.. seealso::
:py:meth:`values()`
"""
self.__map = map_
[docs]
def values(self):
"""
:return: Legend attributes
.. seealso::
:py:meth:`setValues()`
"""
return self.__map
[docs]
def hasRole(self, role):
"""
:param int role: Attribute role
:return: True, when the internal map has an entry for role
"""
return role in self.__map
[docs]
def setValue(self, role, data):
"""
Set an attribute value
:param int role: Attribute role
:param QVariant data: Attribute value
.. seealso::
:py:meth:`value()`
"""
self.__map[role] = data
[docs]
def value(self, role):
"""
:param int role: Attribute role
:return: Attribute value for a specific role
.. seealso::
:py:meth:`setValue()`
"""
return self.__map.get(role)
[docs]
def isValid(self):
"""
:return: True, when the internal map is empty
"""
return len(self.__map) != 0
[docs]
def title(self):
"""
:return: Value of the TitleRole attribute
"""
titleValue = self.value(QwtLegendData.TitleRole)
if isinstance(titleValue, QwtText):
text = titleValue
else:
text = QwtText(titleValue)
return text
[docs]
def icon(self):
"""
:return: Value of the IconRole attribute
"""
return self.value(QwtLegendData.IconRole)
[docs]
def mode(self):
"""
:return: Value of the ModeRole attribute
"""
modeValue = self.value(QwtLegendData.ModeRole)
if isinstance(modeValue, int):
return modeValue
return QwtLegendData.ReadOnly
BUTTONFRAME = 2
MARGIN = 2
def buttonShift(w):
option = QStyleOption()
option.initFrom(w)
ph = w.style().pixelMetric(QStyle.PM_ButtonShiftHorizontal, option, w)
pv = w.style().pixelMetric(QStyle.PM_ButtonShiftVertical, option, w)
return QSize(ph, pv)
class QwtLegendLabel_PrivateData(object):
def __init__(self):
self.itemMode = QwtLegendData.ReadOnly
self.isDown = False
self.spacing = MARGIN
self.legendData = QwtLegendData()
self.icon = QPixmap()
[docs]
class QwtLegendLabel(QwtTextLabel):
"""A widget representing something on a QwtLegend."""
clicked = Signal()
pressed = Signal()
released = Signal()
checked = Signal(bool)
def __init__(self, parent=None):
QwtTextLabel.__init__(self, parent)
self.__data = QwtLegendLabel_PrivateData()
self.setMargin(MARGIN)
self.setIndent(MARGIN)
[docs]
def setData(self, legendData):
"""
Set the attributes of the legend label
:param QwtLegendData legendData: Attributes of the label
.. seealso::
:py:meth:`data()`
"""
self.__data.legendData = legendData
doUpdate = self.updatesEnabled()
self.setUpdatesEnabled(False)
self.setText(legendData.title())
icon = legendData.icon()
if icon is not None:
self.setIcon(icon.toPixmap())
if legendData.hasRole(QwtLegendData.ModeRole):
self.setItemMode(legendData.mode())
if doUpdate:
self.setUpdatesEnabled(True)
self.update()
[docs]
def data(self):
"""
:return: Attributes of the label
.. seealso::
:py:meth:`setData()`, :py:meth:`qwt.plot.QwtPlotItem.legendData()`
"""
return self.__data.legendData
[docs]
def setText(self, text):
"""
Set the text to the legend item
:param qwt.text.QwtText text: Text label
.. seealso::
:py:meth:`text()`
"""
flags = Qt.AlignLeft | Qt.AlignVCenter | Qt.TextExpandTabs | Qt.TextWordWrap
text.setRenderFlags(flags)
QwtTextLabel.setText(self, text)
[docs]
def setItemMode(self, mode):
"""
Set the item mode.
The default is `QwtLegendData.ReadOnly`.
:param int mode: Item mode
.. seealso::
:py:meth:`itemMode()`
"""
if mode != self.__data.itemMode:
self.__data.itemMode = mode
self.__data.isDown = False
self.setFocusPolicy(
Qt.TabFocus if mode != QwtLegendData.ReadOnly else Qt.NoFocus
)
self.setMargin(BUTTONFRAME + MARGIN)
self.updateGeometry()
[docs]
def itemMode(self):
"""
:return: Item mode
.. seealso::
:py:meth:`setItemMode()`
"""
return self.__data.itemMode
[docs]
def setIcon(self, icon):
"""
Assign the icon
:param QPixmap icon: Pixmap representing a plot item
.. seealso::
:py:meth:`icon()`, :py:meth:`qwt.plot.QwtPlotItem.legendIcon()`
"""
self.__data.icon = icon
indent = self.margin() + self.__data.spacing
if icon.width() > 0:
indent += icon.width() + self.__data.spacing
self.setIndent(indent)
[docs]
def icon(self):
"""
:return: Pixmap representing a plot item
.. seealso::
:py:meth:`setIcon()`
"""
return self.__data.icon
[docs]
def setSpacing(self, spacing):
"""
Change the spacing between icon and text
:param int spacing: Spacing
.. seealso::
:py:meth:`spacing()`, :py:meth:`qwt.text.QwtTextLabel.margin()`
"""
spacing = max([spacing, 0])
if spacing != self.__data.spacing:
self.__data.spacing = spacing
mgn = self.contentsMargins()
margin = max([mgn.left(), mgn.top(), mgn.right(), mgn.bottom()])
indent = margin + self.__data.spacing
if self.__data.icon.width() > 0:
indent += self.__data.icon.width() + self.__data.spacing
self.setIndent(indent)
[docs]
def spacing(self):
"""
:return: Spacing between icon and text
.. seealso::
:py:meth:`setSpacing()`
"""
return self.__data.spacing
[docs]
def setChecked(self, on):
"""
Check/Uncheck a the item
:param bool on: check/uncheck
.. seealso::
:py:meth:`isChecked()`, :py:meth:`setItemMode()`
"""
if self.__data.itemMode == QwtLegendData.Checkable:
isBlocked = self.signalsBlocked()
self.blockSignals(True)
self.setDown(on)
self.blockSignals(isBlocked)
[docs]
def isChecked(self):
"""
:return: true, if the item is checked
.. seealso::
:py:meth:`setChecked()`
"""
return self.__data.itemMode == QwtLegendData.Checkable and self.isDown()
[docs]
def setDown(self, down):
"""
Set the item being down
:param bool on: true, if the item is down
.. seealso::
:py:meth:`isDown()`
"""
if down == self.__data.isDown:
return
self.__data.isDown = down
self.update()
if self.__data.itemMode == QwtLegendData.Clickable:
if self.__data.isDown:
self.pressed.emit()
else:
self.released.emit()
self.clicked.emit()
if self.__data.itemMode == QwtLegendData.Checkable:
self.checked.emit(self.__data.isDown)
[docs]
def isDown(self):
"""
:return: true, if the item is down
.. seealso::
:py:meth:`setDown()`
"""
return self.__data.isDown
[docs]
def sizeHint(self):
"""
:return: a size hint
"""
sz = QwtTextLabel.sizeHint(self)
sz.setHeight(max([sz.height(), self.__data.icon.height() + 4]))
if self.__data.itemMode != QwtLegendData.ReadOnly:
sz += buttonShift(self)
return sz
[docs]
def paintEvent(self, e):
cr = self.contentsRect()
painter = QPainter(self)
painter.setClipRegion(e.region())
# if self.__data.isDown:
# qDrawWinButton(
# painter, 0, 0, self.width(), self.height(), self.palette(), True
# )
painter.save()
if self.__data.isDown:
shiftSize = buttonShift(self)
painter.translate(shiftSize.width(), shiftSize.height())
painter.setClipRect(cr)
self.drawContents(painter)
if not self.__data.icon.isNull():
iconRect = QRect(cr)
iconRect.setX(iconRect.x() + self.margin())
if self.__data.itemMode != QwtLegendData.ReadOnly:
iconRect.setX(iconRect.x() + BUTTONFRAME)
iconRect.setSize(self.__data.icon.size())
iconRect.moveCenter(QPoint(iconRect.center().x(), cr.center().y()))
painter.drawPixmap(iconRect, self.__data.icon)
painter.restore()
[docs]
def mousePressEvent(self, e):
if e.button() == Qt.LeftButton:
if self.__data.itemMode == QwtLegendData.Clickable:
self.setDown(True)
return
elif self.__data.itemMode == QwtLegendData.Checkable:
self.setDown(not self.isDown())
return
QwtTextLabel.mousePressEvent(self, e)
[docs]
def mouseReleaseEvent(self, e):
if e.button() == Qt.LeftButton:
if self.__data.itemMode == QwtLegendData.Clickable:
self.setDown(False)
return
elif self.__data.itemMode == QwtLegendData.Checkable:
return
QwtTextLabel.mouseReleaseEvent(self, e)
[docs]
def keyPressEvent(self, e):
if e.key() == Qt.Key_Space:
if self.__data.itemMode == QwtLegendData.Clickable:
if not e.isAutoRepeat():
self.setDown(True)
return
elif self.__data.itemMode == QwtLegendData.Checkable:
if not e.isAutoRepeat():
self.setDown(not self.isDown())
return
QwtTextLabel.keyPressEvent(self, e)
[docs]
def keyReleaseEvent(self, e):
if e.key() == Qt.Key_Space:
if self.__data.itemMode == QwtLegendData.Clickable:
if not e.isAutoRepeat():
self.setDown(False)
return
elif self.__data.itemMode == QwtLegendData.Checkable:
return
QwtTextLabel.keyReleaseEvent(self, e)
class QwtAbstractLegend(QFrame):
def __init__(self, parent):
QFrame.__init__(self, parent)
def renderLegend(self, painter, rect, fillBackground):
raise NotImplementedError
def isEmpty(self):
return 0
def scrollExtent(self, orientation):
return 0
def updateLegend(self, itemInfo, data):
raise NotImplementedError
class Entry(object):
def __init__(self):
self.itemInfo = None
self.widgets = []
class QwtLegendMap(object):
def __init__(self):
self.__entries = []
def isEmpty(self):
return len(self.__entries) == 0
def insert(self, itemInfo, widgets):
for entry in self.__entries:
if entry.itemInfo == itemInfo:
entry.widgets = widgets
return
newEntry = Entry()
newEntry.itemInfo = itemInfo
newEntry.widgets = widgets
self.__entries += [newEntry]
def remove(self, itemInfo):
for entry in self.__entries[:]:
if entry.itemInfo == itemInfo:
self.__entries.remove(entry)
return
def removeWidget(self, widget):
for entry in self.__entries:
while widget in entry.widgets:
entry.widgets.remove(widget)
def itemInfo(self, widget):
if widget is not None:
for entry in self.__entries:
if widget in entry.widgets:
return entry.itemInfo
def legendWidgets(self, itemInfo):
if itemInfo is not None:
for entry in self.__entries:
if entry.itemInfo == itemInfo:
return entry.widgets
return []
class LegendView(QScrollArea):
def __init__(self, parent):
QScrollArea.__init__(self, parent)
self.contentsWidget = QWidget(self)
self.contentsWidget.setObjectName("QwtLegendViewContents")
self.setWidget(self.contentsWidget)
self.setWidgetResizable(False)
self.viewport().setObjectName("QwtLegendViewport")
self.contentsWidget.setAutoFillBackground(False)
self.viewport().setAutoFillBackground(False)
def event(self, event):
if event.type() == QEvent.PolishRequest:
self.setFocusPolicy(Qt.NoFocus)
if event.type() == QEvent.Resize:
cr = self.contentsRect()
w = cr.width()
h = self.contentsWidget.heightForWidth(cr.width())
if h > w:
w -= self.verticalScrollBar().sizeHint().width()
h = self.contentsWidget.heightForWidth(w)
self.contentsWidget.resize(w, h)
return QScrollArea.event(self, event)
def viewportEvent(self, event):
ok = QScrollArea.viewportEvent(self, event)
if event.type() == QEvent.Resize:
self.layoutContents()
return ok
def viewportSize(self, w, h):
sbHeight = self.horizontalScrollBar().sizeHint().height()
sbWidth = self.verticalScrollBar().sizeHint().width()
cw = self.contentsRect().width()
ch = self.contentsRect().height()
vw = cw
vh = ch
if w > vw:
vh -= sbHeight
if h > vh:
vw -= sbWidth
if w > vw and vh == ch:
vh -= sbHeight
return QSize(vw, vh)
def layoutContents(self):
layout = self.contentsWidget.layout()
if layout is None:
return
visibleSize = self.viewport().contentsRect().size()
margins = layout.contentsMargins()
margin_w = margins.left() + margins.right()
minW = int(layout.maxItemWidth() + margin_w)
w = max([visibleSize.width(), minW])
h = max([layout.heightForWidth(w), visibleSize.height()])
vpWidth = self.viewportSize(w, h).width()
if w > vpWidth:
w = max([vpWidth, minW])
h = max([layout.heightForWidth(w), visibleSize.height()])
self.contentsWidget.resize(w, h)
class QwtLegend_PrivateData(object):
def __init__(self):
self.itemMode = QwtLegendData.ReadOnly
self.view = QwtDynGridLayout()
self.itemMap = QwtLegendMap()
[docs]
class QwtLegend(QwtAbstractLegend):
"""
The legend widget
The QwtLegend widget is a tabular arrangement of legend items. Legend
items might be any type of widget, but in general they will be
a QwtLegendLabel.
.. seealso ::
:py:class`qwt.legend.QwtLegendLabel`,
:py:class`qwt.plot.QwtPlotItem`,
:py:class`qwt.plot.QwtPlot`
.. py:class:: QwtLegend([parent=None])
Constructor
:param QWidget parent: Parent widget
.. py:data:: clicked
A signal which is emitted when the user has clicked on
a legend label, which is in `QwtLegendData.Clickable` mode.
:param itemInfo: Info for the item item of the selected legend item
:param index: Index of the legend label in the list of widgets that are associated with the plot item
.. note::
Clicks are disabled as default
.. py:data:: checked
A signal which is emitted when the user has clicked on
a legend label, which is in `QwtLegendData.Checkable` mode
:param itemInfo: Info for the item of the selected legend label
:param index: Index of the legend label in the list of widgets that are associated with the plot item
:param on: True when the legend label is checked
.. note::
Clicks are disabled as default
"""
clicked = Signal(object, int)
checked = Signal(object, bool, int)
def __init__(self, parent=None):
QwtAbstractLegend.__init__(self, parent)
self.setFrameStyle(QFrame.NoFrame)
self.__data = QwtLegend_PrivateData()
self.__data.view = LegendView(self)
self.__data.view.setObjectName("QwtLegendView")
self.__data.view.setFrameStyle(QFrame.NoFrame)
gridLayout = QwtDynGridLayout(self.__data.view.contentsWidget)
gridLayout.setAlignment(Qt.AlignHCenter | Qt.AlignTop)
self.__data.view.gridLayout = gridLayout
self.__data.view.contentsWidget.installEventFilter(self)
layout = QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(self.__data.view)
[docs]
def setMaxColumns(self, numColumns):
"""
Set the maximum number of entries in a row
F.e when the maximum is set to 1 all items are aligned
vertically. 0 means unlimited
:param int numColumns: Maximum number of entries in a row
.. seealso::
:py:meth:`maxColumns()`,
:py:meth:`QwtDynGridLayout.setMaxColumns()`
"""
tl = self.__data.view.gridLayout
if tl is not None:
tl.setMaxColumns(numColumns)
self.updateGeometry()
[docs]
def maxColumns(self):
"""
:return: Maximum number of entries in a row
.. seealso::
:py:meth:`setMaxColumns()`,
:py:meth:`QwtDynGridLayout.maxColumns()`
"""
tl = self.__data.view.gridLayout
if tl is not None:
return tl.maxColumns()
return 0
[docs]
def setDefaultItemMode(self, mode):
"""
Set the default mode for legend labels
Legend labels will be constructed according to the
attributes in a `QwtLegendData` object. When it doesn't
contain a value for the `QwtLegendData.ModeRole` the
label will be initialized with the default mode of the legend.
:param int mode: Default item mode
.. seealso::
:py:meth:`itemMode()`,
:py:meth:`QwtLegendData.value()`,
:py:meth:`QwtPlotItem::legendData()`
... note::
Changing the mode doesn't have any effect on existing labels.
"""
self.__data.itemMode = mode
[docs]
def defaultItemMode(self):
"""
:return: Default item mode
.. seealso::
:py:meth:`setDefaultItemMode()`
"""
return self.__data.itemMode
[docs]
def contentsWidget(self):
"""
The contents widget is the only child of the viewport of
the internal `QScrollArea` and the parent widget of all legend
items.
:return: Container widget of the legend items
"""
return self.__data.view.contentsWidget
[docs]
def updateLegend(self, itemInfo, data):
"""
Update the entries for an item
:param QVariant itemInfo: Info for an item
:param list data: Default item mode
"""
widgetList = self.legendWidgets(itemInfo)
if len(widgetList) != len(data):
contentsLayout = self.__data.view.gridLayout
while len(widgetList) > len(data):
w = widgetList.pop(-1)
contentsLayout.removeWidget(w)
w.hide()
w.deleteLater()
for i in range(len(widgetList), len(data)):
widget = self.createWidget(data[i])
if contentsLayout is not None:
contentsLayout.addWidget(widget)
if self.isVisible():
widget.setVisible(True)
widgetList.append(widget)
if not widgetList:
self.__data.itemMap.remove(itemInfo)
else:
self.__data.itemMap.insert(itemInfo, widgetList)
self.updateTabOrder()
for i in range(len(data)):
self.updateWidget(widgetList[i], data[i])
def updateTabOrder(self):
contentsLayout = self.__data.view.gridLayout
if contentsLayout is not None:
w = None
for i in range(contentsLayout.count()):
item = contentsLayout.itemAt(i)
if w is not None and item.widget():
QWidget.setTabOrder(w, item.widget())
w = item.widget()
[docs]
def sizeHint(self):
"""Return a size hint"""
hint = self.__data.view.contentsWidget.sizeHint()
hint += QSize(2 * self.frameWidth(), 2 * self.frameWidth())
return hint
[docs]
def heightForWidth(self, width):
"""
:param int width: Width
:return: The preferred height, for a width.
"""
width -= 2 * self.frameWidth()
h = self.__data.view.contentsWidget.heightForWidth(width)
if h >= 0:
h += 2 * self.frameWidth()
return h
[docs]
def eventFilter(self, object_, event):
"""
Handle QEvent.ChildRemoved andQEvent.LayoutRequest events
for the contentsWidget().
:param QObject object: Object to be filtered
:param QEvent event: Event
:return: Forwarded to QwtAbstractLegend.eventFilter()
"""
if object_ is self.__data.view.contentsWidget:
if event.type() == QEvent.ChildRemoved:
ce = event # TODO: cast to QChildEvent
if ce.child().isWidgetType():
w = ce.child() # TODO: cast to QWidget
self.__data.itemMap.removeWidget(w)
elif event.type() == QEvent.LayoutRequest:
self.__data.view.layoutContents()
if self.parentWidget() and self.parentWidget().layout() is None:
QApplication.postEvent(
self.parentWidget(), QEvent(QEvent.LayoutRequest)
)
return QwtAbstractLegend.eventFilter(self, object_, event)
def itemClicked(self, widget):
# w = self.sender() #TODO: cast to QWidget
w = widget
if w is not None:
itemInfo = self.__data.itemMap.itemInfo(w)
if itemInfo is not None:
widgetList = self.__data.itemMap.legendWidgets(itemInfo)
if w in widgetList:
index = widgetList.index(w)
self.clicked.emit(itemInfo, index)
def itemChecked(self, on, widget):
# w = self.sender() #TODO: cast to QWidget
w = widget
if w is not None:
itemInfo = self.__data.itemMap.itemInfo(w)
if itemInfo is not None:
widgetList = self.__data.itemMap.legendWidgets(itemInfo)
if w in widgetList:
index = widgetList.index(w)
self.checked.emit(itemInfo, on, index)
[docs]
def renderLegend(self, painter, rect, fillBackground):
"""
Render the legend into a given rectangle.
:param QPainter painter: Painter
:param QRectF rect: Bounding rectangle
:param bool fillBackground: When true, fill rect with the widget background
"""
if self.__data.itemMap.isEmpty():
return
if fillBackground:
if self.autoFillBackground() or self.testAttribute(Qt.WA_StyledBackground):
QwtPainter.drawBackground(painter, rect, self)
legendLayout = self.__data.view.contentsWidget.layout()
if legendLayout is None:
return
margins = self.layout().contentsMargins()
layoutRect = QRect()
layoutRect.setLeft(math.ceil(rect.left()) + margins.left())
layoutRect.setTop(math.ceil(rect.top()) + margins.top())
layoutRect.setRight(math.ceil(rect.right()) - margins.right())
layoutRect.setBottom(math.ceil(rect.bottom()) - margins.bottom())
numCols = legendLayout.columnsForWidth(layoutRect.width())
itemRects = legendLayout.layoutItems(layoutRect, numCols)
index = 0
for i in range(legendLayout.count()):
item = legendLayout.itemAt(i)
w = item.widget()
if w is not None:
painter.save()
painter.setClipRect(itemRects[index], Qt.IntersectClip)
self.renderItem(painter, w, itemRects[index], fillBackground)
index += 1
painter.restore()
[docs]
def renderItem(self, painter, widget, rect, fillBackground):
"""
Render a legend entry into a given rectangle.
:param QPainter painter: Painter
:param QWidget widget: Widget representing a legend entry
:param QRectF rect: Bounding rectangle
:param bool fillBackground: When true, fill rect with the widget background
"""
if fillBackground:
if widget.autoFillBackground() or widget.testAttribute(
Qt.WA_StyledBackground
):
QwtPainter.drawBackground(painter, rect, widget)
label = widget # TODO: cast to QwtLegendLabel
if label is not None:
icon = label.data().icon()
sz = icon.defaultSize()
mgn = label.contentsMargins()
margin = max([mgn.left(), mgn.top(), mgn.right(), mgn.bottom()])
iconRect = QRectF(
rect.x() + margin,
rect.center().y() - 0.5 * sz.height(),
sz.width(),
sz.height(),
)
icon.render(painter, iconRect, Qt.KeepAspectRatio)
titleRect = QRectF(rect)
titleRect.setX(iconRect.right() + 2 * label.spacing())
painter.setFont(label.font())
painter.setPen(label.palette().color(QPalette.Text))
label.drawText(painter, titleRect) # TODO: cast label to QwtLegendLabel
[docs]
def itemInfo(self, widget):
"""
Find the item that is associated to a widget
:param QWidget widget: Widget on the legend
:return: Associated item info
"""
return self.__data.itemMap.itemInfo(widget)
def isEmpty(self):
return self.__data.itemMap.isEmpty()