1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243
|
from .parameterTypes import GroupParameterItem
from ..Qt import QtCore, QtWidgets, QtGui, mkQApp
from ..widgets.TreeWidget import TreeWidget
from .ParameterItem import ParameterItem
class ParameterTree(TreeWidget):
"""Widget used to display or control data from a hierarchy of Parameters"""
def __init__(self, parent=None, showHeader=True):
"""
============== ========================================================
**Arguments:**
parent (QWidget) An optional parent widget
showHeader (bool) If True, then the QTreeView header is displayed.
============== ========================================================
"""
TreeWidget.__init__(self, parent)
self.setVerticalScrollMode(self.ScrollMode.ScrollPerPixel)
self.setHorizontalScrollMode(self.ScrollMode.ScrollPerPixel)
self.setAnimated(False)
self.setColumnCount(2)
self.setHeaderLabels(["Parameter", "Value"])
self.paramSet = None
self.header().setSectionResizeMode(QtWidgets.QHeaderView.ResizeMode.ResizeToContents)
self.setHeaderHidden(not showHeader)
self.itemChanged.connect(self.itemChangedEvent)
self.itemExpanded.connect(self.itemExpandedEvent)
self.itemCollapsed.connect(self.itemCollapsedEvent)
self.lastSel = None
self.setRootIsDecorated(False)
self.setAlternatingRowColors(True)
self._updatePalette(self.palette())
def setParameters(self, param, showTop=True):
"""
Set the top-level :class:`Parameter <pyqtgraph.parametertree.Parameter>`
to be displayed in this ParameterTree.
If *showTop* is False, then the top-level parameter is hidden and only
its children will be visible. This is a convenience method equivalent
to::
tree.clear()
tree.addParameters(param, showTop)
"""
self.clear()
self.addParameters(param, showTop=showTop)
def addParameters(self, param, root=None, depth=0, showTop=True):
"""
Adds one top-level :class:`Parameter <pyqtgraph.parametertree.Parameter>`
to the view.
============== ==========================================================
**Arguments:**
param The :class:`Parameter <pyqtgraph.parametertree.Parameter>`
to add.
root The item within the tree to which *param* should be added.
By default, *param* is added as a top-level item.
showTop If False, then *param* will be hidden, and only its
children will be visible in the tree.
============== ==========================================================
"""
item = param.makeTreeItem(depth=depth)
if root is None:
root = self.invisibleRootItem()
## Hide top-level item
if not showTop:
item.setText(0, '')
item.setSizeHint(0, QtCore.QSize(1,1))
item.setSizeHint(1, QtCore.QSize(1,1))
depth -= 1
root.addChild(item)
item.treeWidgetChanged()
for ch in param:
self.addParameters(ch, root=item, depth=depth+1)
def clear(self):
"""
Remove all parameters from the tree.
"""
self.invisibleRootItem().takeChildren()
def focusNext(self, item, forward=True):
"""Give input focus to the next (or previous) item after *item*
"""
while True:
parent = item.parent()
if parent is None:
return
nextItem = self.nextFocusableChild(parent, item, forward=forward)
if nextItem is not None:
nextItem.setFocus()
self.setCurrentItem(nextItem)
return
item = parent
def focusPrevious(self, item):
self.focusNext(item, forward=False)
def nextFocusableChild(self, root, startItem=None, forward=True):
if startItem is None:
if forward:
index = 0
else:
index = root.childCount()-1
else:
if forward:
index = root.indexOfChild(startItem) + 1
else:
index = root.indexOfChild(startItem) - 1
if forward:
inds = list(range(index, root.childCount()))
else:
inds = list(range(index, -1, -1))
for i in inds:
item = root.child(i)
if hasattr(item, 'isFocusable') and item.isFocusable():
return item
else:
item = self.nextFocusableChild(item, forward=forward)
if item is not None:
return item
return None
def contextMenuEvent(self, ev):
item = self.currentItem()
if hasattr(item, 'contextMenuEvent'):
item.contextMenuEvent(ev)
def _updatePalette(self, palette: QtGui.QPalette) -> None:
app = mkQApp()
# Docs say to use the following methods
# QApplication.instance().styleHints().colorScheme() == QtCore.Qt.ColorScheme.Dark
# but on macOS with Qt 6.7 this is giving opposite results (says color sceme is light
# when it is dark and vice versa). This was not observed in the ExampleApp, but was
# observed with the ParameterTree. We fall back to the "legacy" method of determining
# if the color theme is dark or light from QPalette
windowTextLightness = palette.color(QtGui.QPalette.ColorRole.WindowText).lightness()
windowLightness = palette.color(QtGui.QPalette.ColorRole.Window).lightness()
darkMode = windowTextLightness > windowLightness
app.setProperty('darkMode', darkMode)
for group in [
QtGui.QPalette.ColorGroup.Disabled,
QtGui.QPalette.ColorGroup.Active,
QtGui.QPalette.ColorGroup.Inactive
]:
baseColor = palette.color(
group,
QtGui.QPalette.ColorRole.Base
)
if app.property("darkMode"):
alternateColor = baseColor.lighter(180)
else:
alternateColor = baseColor.darker(110)
# apparently colors are transparent here by default!
alternateColor.setAlpha(255)
palette.setColor(
group,
QtGui.QPalette.ColorRole.AlternateBase,
alternateColor
)
self.setPalette(palette)
return None
def event(self, event: QtCore.QEvent) -> bool:
if event.type() == QtCore.QEvent.Type.FontChange:
for item in self.listAllItems():
if isinstance(item, GroupParameterItem):
item.updateDepth(item.depth)
elif event.type() == QtCore.QEvent.Type.ApplicationPaletteChange:
app = mkQApp()
self._updatePalette(app.palette())
elif event.type() == QtCore.QEvent.Type.PaletteChange:
# For Windows to effectively change all the rows we
# need to catch QEvent.Type.PaletteChange event as well
self._updatePalette(self.palette())
return super().event(event)
def itemChangedEvent(self, item, col):
if hasattr(item, 'columnChangedEvent'):
item.columnChangedEvent(col)
def itemExpandedEvent(self, item):
if hasattr(item, 'expandedChangedEvent'):
item.expandedChangedEvent(True)
def itemCollapsedEvent(self, item):
if hasattr(item, 'expandedChangedEvent'):
item.expandedChangedEvent(False)
def selectionChanged(self, *args):
sel = self.selectedItems()
if len(sel) != 1:
sel = None
if self.lastSel is not None and isinstance(self.lastSel, ParameterItem):
self.lastSel.selected(False)
if sel is None:
self.lastSel = None
return
self.lastSel = sel[0]
if hasattr(sel[0], 'selected'):
sel[0].selected(True)
return super().selectionChanged(*args)
# commented out due to being unreliable
# def wheelEvent(self, ev):
# self.clearSelection()
# return super().wheelEvent(ev)
def sizeHint(self):
w, h = 0, 0
ind = self.indentation()
for x in self.listAllItems():
if x.isHidden():
continue
try:
depth = x.depth
except AttributeError:
depth = 0
s0 = x.sizeHint(0)
s1 = x.sizeHint(1)
w = max(w, depth * ind + max(0, s0.width()) + max(0, s1.width()))
h += max(0, s0.height(), s1.height())
# typ = x.param.opts['type'] if isinstance(x, ParameterItem) else x
# print(typ, depth * ind, (s0.width(), s0.height()), (s1.width(), s1.height()), (w, h))
# todo: find out if this alternative can be made to work (currently fails when color or colormap are present)
# print('custom', (w, h))
# w = self.sizeHintForColumn(0) + self.sizeHintForColumn(1)
# h = self.viewportSizeHint().height()
# print('alternative', (w, h))
if not self.header().isHidden():
h += self.header().height()
return QtCore.QSize(w, h)
|