# -*- 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 numpy as np
from qwt.qt.QtGui import (QFrame, QScrollArea, QWidget, QVBoxLayout, QPalette,
QApplication, QStyleOption, QStyle, QPixmap,
QPainter, qDrawWinButton)
from qwt.qt.QtCore import Signal, QEvent, QSize, Qt, QRect, QRectF, QPoint
from qwt.text import QwtText, QwtTextLabel
from qwt.dyngrid_layout import QwtDynGridLayout
from qwt.painter import QwtPainter
[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.setText(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
txt = text #TODO: WTF?
txt.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
indent = self.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)
sz = sz.expandedTo(QApplication.globalStrut())
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)
[docs]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 []
[docs]class LegendView(QScrollArea):
def __init__(self, parent):
QScrollArea.__init__(self, parent)
self.gridLayout = None
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)
[docs] 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)
[docs] 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):
tl = self.gridLayout
if tl is None:
return
visibleSize = self.viewport().contentsRect().size()
margins = tl.contentsMargins()
margin_w = margins.left() + margins.right()
minW = int(tl.maxItemWidth()+margin_w)
w = max([visibleSize.width(), minW])
h = max([tl.heightForWidth(w), visibleSize.height()])
vpWidth = self.viewportSize(w, h).width()
if w > vpWidth:
w = max([vpWidth, minW])
h = max([tl.heightForWidth(w), visibleSize.height()])
self.contentsWidget.resize(w, h)
class QwtLegend_PrivateData(object):
def __init__(self):
self.itemMode = QwtLegendData.ReadOnly
self.view = None
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("PyQt_PyObject", int)
checked = Signal("PyQt_PyObject", 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)
[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)
# const QwtDynGridLayout *legendLayout =
# qobject_cast<QwtDynGridLayout *>( contentsWidget()->layout() );
#TODO: not the exact same implementation
legendLayout = self.__data.view.contentsWidget.layout()
if legendLayout is None:
return
left, right, top, bottom = self.getContentsMargins()
layoutRect = QRect()
layoutRect.setLeft(np.ceil(rect.left())+left)
layoutRect.setTop(np.ceil(rect.top())+top)
layoutRect.setRight(np.ceil(rect.right())-right)
layoutRect.setBottom(np.ceil(rect.bottom())-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()
iconRect = QRectF(rect.x()+label.margin(),
rect.center().y()-.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()