|
1 | | -from AnyQt.QtCore import Qt, QRect |
| 1 | +from __future__ import annotations |
| 2 | + |
| 3 | +from AnyQt.QtCore import Qt, QRect, QSize |
2 | 4 | from AnyQt.QtGui import QBrush, QIcon, QCursor, QPalette, QPainter, QMouseEvent |
3 | 5 | from AnyQt.QtWidgets import ( |
4 | | - QHeaderView, QStyleOptionHeader, QStyle, QApplication |
| 6 | + QHeaderView, QStyleOptionHeader, QStyle, QApplication, QStyleOptionViewItem |
5 | 7 | ) |
6 | 8 |
|
7 | 9 |
|
@@ -225,3 +227,139 @@ def paintSection(self, painter, rect, logicalIndex): |
225 | 227 | self.style().drawControl(QStyle.CE_Header, opt, painter, self) |
226 | 228 |
|
227 | 229 | painter.setBrushOrigin(oldBO) |
| 230 | + |
| 231 | + |
| 232 | +class CheckableHeaderView(HeaderView): |
| 233 | + """ |
| 234 | + A HeaderView with checkable header items. |
| 235 | +
|
| 236 | + The header is checkable if the model defines a `Qt.CheckStateRole` value. |
| 237 | + """ |
| 238 | + __sectionPressed: int = -1 |
| 239 | + |
| 240 | + def paintSection( |
| 241 | + self, painter: QPainter, rect: QRect, logicalIndex: int |
| 242 | + ) -> None: |
| 243 | + opt = QStyleOptionHeader() |
| 244 | + self.initStyleOption(opt) |
| 245 | + self.initStyleOptionForIndex(opt, logicalIndex) |
| 246 | + model = self.model() |
| 247 | + if model is None: |
| 248 | + return # pragma: no cover |
| 249 | + opt.rect = rect |
| 250 | + checkstate = self.sectionCheckState(logicalIndex) |
| 251 | + ischeckable = checkstate is not None |
| 252 | + style = self.style() |
| 253 | + # draw background |
| 254 | + style.drawControl(QStyle.CE_HeaderSection, opt, painter, self) |
| 255 | + text_rect = QRect(rect) |
| 256 | + optindicator = QStyleOptionViewItem() |
| 257 | + optindicator.initFrom(self) |
| 258 | + optindicator.font = self.font() |
| 259 | + optindicator.fontMetrics = opt.fontMetrics |
| 260 | + optindicator.features = QStyleOptionViewItem.HasCheckIndicator | QStyleOptionViewItem.HasDisplay |
| 261 | + optindicator.rect = opt.rect |
| 262 | + indicator_rect = style.subElementRect( |
| 263 | + QStyle.SE_ItemViewItemCheckIndicator, optindicator, self) |
| 264 | + text_rect.setLeft(indicator_rect.right() + 4) |
| 265 | + if ischeckable: |
| 266 | + optindicator.checkState = checkstate |
| 267 | + optindicator.state |= QStyle.State_On if checkstate == Qt.Checked else QStyle.State_Off |
| 268 | + optindicator.rect = indicator_rect |
| 269 | + style.drawPrimitive(QStyle.PE_IndicatorItemViewItemCheck, optindicator, |
| 270 | + painter, self) |
| 271 | + opt.rect = text_rect |
| 272 | + # draw section label |
| 273 | + style.drawControl(QStyle.CE_HeaderLabel, opt, painter, self) |
| 274 | + |
| 275 | + def mousePressEvent(self, event: QMouseEvent) -> None: |
| 276 | + pos = event.pos() |
| 277 | + section = self.logicalIndexAt(pos) |
| 278 | + if section == -1 or not self.isSectionCheckable(section): |
| 279 | + return super().mousePressEvent(event) |
| 280 | + |
| 281 | + if event.button() == Qt.LeftButton: |
| 282 | + opt = self.__viewItemOption(section) |
| 283 | + hitrect = self.style().subElementRect(QStyle.SE_ItemViewItemCheckIndicator, opt, self) |
| 284 | + if hitrect.contains(pos): |
| 285 | + self.__sectionPressed = section |
| 286 | + event.accept() |
| 287 | + return |
| 288 | + return super().mousePressEvent(event) |
| 289 | + |
| 290 | + def mouseReleaseEvent(self, event: QMouseEvent) -> None: |
| 291 | + pos = event.pos() |
| 292 | + section = self.logicalIndexAt(pos) |
| 293 | + if section == -1 or not self.isSectionCheckable(section) \ |
| 294 | + or self.__sectionPressed != section: |
| 295 | + return super().mouseReleaseEvent(event) |
| 296 | + if event.button() == Qt.LeftButton: |
| 297 | + opt = self.__viewItemOption(section) |
| 298 | + hitrect = self.style().subElementRect(QStyle.SE_ItemViewItemCheckIndicator, opt, self) |
| 299 | + if hitrect.contains(pos): |
| 300 | + state = self.sectionCheckState(section) |
| 301 | + newstate = Qt.Checked if state == Qt.Unchecked else Qt.Unchecked |
| 302 | + model = self.model() |
| 303 | + model.setHeaderData( |
| 304 | + section, self.orientation(), newstate, Qt.CheckStateRole) |
| 305 | + return |
| 306 | + return super().mouseReleaseEvent(event) |
| 307 | + |
| 308 | + def isSectionCheckable(self, index: int) -> bool: |
| 309 | + model = self.model() |
| 310 | + if model is None: # pragma: no cover |
| 311 | + return False |
| 312 | + checkstate = model.headerData(index, self.orientation(), Qt.CheckStateRole) |
| 313 | + return checkstate is not None |
| 314 | + |
| 315 | + def sectionCheckState(self, index: int) -> Qt.CheckState | None: |
| 316 | + model = self.model() |
| 317 | + if model is None: # pragma: no cover |
| 318 | + return None |
| 319 | + checkstate = model.headerData(index, self.orientation(), Qt.CheckStateRole) |
| 320 | + if checkstate is None: |
| 321 | + return None |
| 322 | + try: |
| 323 | + return Qt.CheckState(checkstate) |
| 324 | + except TypeError: # pragma: no cover |
| 325 | + return None |
| 326 | + |
| 327 | + def __viewItemOption(self, index: int) -> QStyleOptionViewItem: |
| 328 | + opt = QStyleOptionHeader() |
| 329 | + self.initStyleOption(opt) |
| 330 | + self.initStyleOptionForIndex(opt, index) |
| 331 | + pos = self.sectionViewportPosition(index) |
| 332 | + size = self.sectionSize(index) |
| 333 | + if self.orientation() == Qt.Horizontal: |
| 334 | + rect = QRect(pos, 0, size, self.height()) |
| 335 | + else: |
| 336 | + rect = QRect(0, pos, self.width(), size) |
| 337 | + optindicator = QStyleOptionViewItem() |
| 338 | + optindicator.initFrom(self) |
| 339 | + optindicator.rect = rect |
| 340 | + optindicator.font = self.font() |
| 341 | + optindicator.fontMetrics = opt.fontMetrics |
| 342 | + optindicator.features = QStyleOptionViewItem.HasCheckIndicator |
| 343 | + if not opt.icon.isNull(): |
| 344 | + optindicator.icon = opt.icon |
| 345 | + optindicator.features |= QStyleOptionViewItem.HasDecoration |
| 346 | + return optindicator |
| 347 | + |
| 348 | + def sectionSizeFromContents(self, logicalIndex: int) -> QSize: |
| 349 | + style = self.style() |
| 350 | + opt = QStyleOptionHeader() |
| 351 | + self.initStyleOption(opt) |
| 352 | + self.initStyleOptionForIndex(opt, logicalIndex) |
| 353 | + sh = style.sizeFromContents(QStyle.CT_HeaderSection, opt, |
| 354 | + QSize(), self) |
| 355 | + |
| 356 | + optindicator = QStyleOptionViewItem() |
| 357 | + optindicator.initFrom(self) |
| 358 | + optindicator.font = self.font() |
| 359 | + optindicator.fontMetrics = opt.fontMetrics |
| 360 | + optindicator.features = QStyleOptionViewItem.HasCheckIndicator |
| 361 | + optindicator.rect = opt.rect |
| 362 | + indicator_rect = style.subElementRect( |
| 363 | + QStyle.SE_ItemViewItemCheckIndicator, optindicator, self) |
| 364 | + return QSize(sh.width() + indicator_rect.width() + 4, |
| 365 | + max(sh.height(), indicator_rect.height())) |
0 commit comments