14 |
14 |
15 from collections import Counter |
15 from collections import Counter |
16 from operator import attrgetter |
16 from operator import attrgetter |
17 |
17 |
18 from PyQt6.QtCore import ( |
18 from PyQt6.QtCore import ( |
19 pyqtSignal, pyqtSlot, Qt, QAbstractItemModel, QCoreApplication, QModelIndex |
19 pyqtSignal, pyqtSlot, Qt, QAbstractItemModel, QCoreApplication, |
|
20 QModelIndex, QPoint |
20 ) |
21 ) |
21 from PyQt6.QtGui import QBrush, QColor |
22 from PyQt6.QtGui import QBrush, QColor |
22 from PyQt6.QtWidgets import QTreeView |
23 from PyQt6.QtWidgets import QMenu, QTreeView |
23 |
24 |
24 from EricWidgets.EricApplication import ericApp |
25 from EricWidgets.EricApplication import ericApp |
25 |
26 |
26 import Preferences |
27 import Preferences |
27 |
28 |
41 |
42 |
42 Headers = [ |
43 Headers = [ |
43 QCoreApplication.translate("TestResultsModel", "Status"), |
44 QCoreApplication.translate("TestResultsModel", "Status"), |
44 QCoreApplication.translate("TestResultsModel", "Name"), |
45 QCoreApplication.translate("TestResultsModel", "Name"), |
45 QCoreApplication.translate("TestResultsModel", "Message"), |
46 QCoreApplication.translate("TestResultsModel", "Message"), |
46 QCoreApplication.translate("TestResultsModel", "Duration (ms)"), |
47 QCoreApplication.translate("TestResultsModel", "Duration [ms]"), |
47 ] |
48 ] |
48 |
49 |
49 StatusColumn = 0 |
50 StatusColumn = 0 |
50 NameColumn = 1 |
51 NameColumn = 1 |
51 MessageColumn = 2 |
52 MessageColumn = 2 |
140 if duration is None else |
141 if duration is None else |
141 locale.format_string("%.2f", duration, grouping=True) |
142 locale.format_string("%.2f", duration, grouping=True) |
142 ) |
143 ) |
143 elif role == Qt.ItemDataRole.ToolTipRole: |
144 elif role == Qt.ItemDataRole.ToolTipRole: |
144 if idx == TopLevelId and column == TestResultsModel.NameColumn: |
145 if idx == TopLevelId and column == TestResultsModel.NameColumn: |
145 return self.testresults[row].name |
146 return self.__testResults[row].name |
146 elif role == Qt.ItemDataRole.FontRole: |
147 elif role == Qt.ItemDataRole.FontRole: |
147 if idx != TopLevelId: |
148 if idx != TopLevelId: |
148 return Preferences.getEditorOtherFonts("MonospacedFont") |
149 return Preferences.getEditorOtherFonts("MonospacedFont") |
149 elif role == Qt.ItemDataRole.BackgroundRole: |
150 elif role == Qt.ItemDataRole.BackgroundRole: |
150 if idx == TopLevelId: |
151 if idx == TopLevelId: |
154 elif role == Qt.ItemDataRole.TextAlignmentRole: |
155 elif role == Qt.ItemDataRole.TextAlignmentRole: |
155 if idx == TopLevelId and column == TestResultsModel.DurationColumn: |
156 if idx == TopLevelId and column == TestResultsModel.DurationColumn: |
156 return Qt.AlignmentFlag.AlignRight |
157 return Qt.AlignmentFlag.AlignRight |
157 elif role == Qt.ItemDataRole.UserRole: # __IGNORE_WARNING_Y102__ |
158 elif role == Qt.ItemDataRole.UserRole: # __IGNORE_WARNING_Y102__ |
158 if idx == TopLevelId: |
159 if idx == TopLevelId: |
159 testresult = self.testresults[row] |
160 testresult = self.__testResults[row] |
160 return (testresult.filename, testresult.lineno) |
161 return (testresult.filename, testresult.lineno) |
161 |
162 |
162 return None |
163 return None |
163 |
164 |
164 def headerData(self, section, orientation, |
165 def headerData(self, section, orientation, |
425 self.setSortingEnabled(True) |
426 self.setSortingEnabled(True) |
426 |
427 |
427 self.header().setDefaultAlignment(Qt.AlignmentFlag.AlignCenter) |
428 self.header().setDefaultAlignment(Qt.AlignmentFlag.AlignCenter) |
428 self.header().setSortIndicatorShown(False) |
429 self.header().setSortIndicatorShown(False) |
429 |
430 |
|
431 self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu) |
|
432 |
430 # connect signals and slots |
433 # connect signals and slots |
431 self.doubleClicked.connect(self.__gotoTestDefinition) |
434 self.doubleClicked.connect(self.__gotoTestDefinition) |
|
435 self.customContextMenuRequested.connect(self.__showContextMenu) |
432 |
436 |
433 self.header().sortIndicatorChanged.connect(self.sortByColumn) |
437 self.header().sortIndicatorChanged.connect(self.sortByColumn) |
434 self.header().sortIndicatorChanged.connect( |
438 self.header().sortIndicatorChanged.connect( |
435 lambda column, order: self.header().setSortIndicatorShown(True)) |
439 lambda column, order: self.header().setSortIndicatorShown(True)) |
436 |
440 |
477 topLeft = topLeft.parent() |
481 topLeft = topLeft.parent() |
478 while bottomRight.parent().isValid(): |
482 while bottomRight.parent().isValid(): |
479 bottomRight = bottomRight.parent() |
483 bottomRight = bottomRight.parent() |
480 self.spanFirstColumn(topLeft.row(), bottomRight.row()) |
484 self.spanFirstColumn(topLeft.row(), bottomRight.row()) |
481 |
485 |
482 @pyqtSlot(QModelIndex) |
|
483 def __gotoTestDefinition(self, index): |
|
484 """ |
|
485 Private slot to show the test definition. |
|
486 |
|
487 @param index index for the double-clicked item |
|
488 @type QModelIndex |
|
489 """ |
|
490 # TODO: not implemented yet (__gotoTestDefinition) |
|
491 pass |
|
492 |
|
493 def resizeColumns(self): |
486 def resizeColumns(self): |
494 """ |
487 """ |
495 Public method to resize the columns to their contents. |
488 Public method to resize the columns to their contents. |
496 """ |
489 """ |
497 for column in range(self.model().columnCount()): |
490 for column in range(self.model().columnCount()): |
512 model = self.model() |
505 model = self.model() |
513 for row in range(startRow, endRow + 1): |
506 for row in range(startRow, endRow + 1): |
514 index = model.index(row, 0) |
507 index = model.index(row, 0) |
515 for i in range(model.rowCount(index)): |
508 for i in range(model.rowCount(index)): |
516 self.setFirstColumnSpanned(i, index, True) |
509 self.setFirstColumnSpanned(i, index, True) |
|
510 |
|
511 def __canonicalIndex(self, index): |
|
512 """ |
|
513 Private method to create the canonical index for a given index. |
|
514 |
|
515 The canonical index is the index of the first column of the test |
|
516 result entry (i.e. the top-level item). If the index is invalid, |
|
517 None is returned. |
|
518 |
|
519 @param index index to determine the canonical index for |
|
520 @type QModelIndex |
|
521 @return index of the firt column of the associated top-level item index |
|
522 @rtype QModelIndex |
|
523 """ |
|
524 if not index.isValid(): |
|
525 return None |
|
526 |
|
527 while index.parent().isValid(): # find the top-level node |
|
528 index = index.parent() |
|
529 index = index.sibling(index.row(), 0) # go to first column |
|
530 return index |
|
531 |
|
532 @pyqtSlot(QModelIndex) |
|
533 def __gotoTestDefinition(self, index): |
|
534 """ |
|
535 Private slot to show the test definition. |
|
536 |
|
537 @param index index for the double-clicked item |
|
538 @type QModelIndex |
|
539 """ |
|
540 cindex = self.__canonicalIndex(index) |
|
541 filename, lineno = self.model().data(cindex, Qt.ItemDataRole.UserRole) |
|
542 if filename is not None: |
|
543 if lineno is None: |
|
544 lineno = 1 |
|
545 self.goto.emit(filename, lineno) |
|
546 |
|
547 @pyqtSlot(QPoint) |
|
548 def __showContextMenu(self, pos): |
|
549 """ |
|
550 Private slot to show the context menu. |
|
551 |
|
552 @param pos relative position for the context menu |
|
553 @type QPoint |
|
554 """ |
|
555 index = self.indexAt(pos) |
|
556 cindex = self.__canonicalIndex(index) |
|
557 |
|
558 contextMenu = ( |
|
559 self.__createContextMenu(cindex) |
|
560 if cindex else |
|
561 self.__createBackgroundContextMenu() |
|
562 ) |
|
563 contextMenu.exec(self.mapToGlobal(pos)) |
|
564 |
|
565 def __createContextMenu(self, index): |
|
566 """ |
|
567 Private method to create a context menu for the item pointed to by the |
|
568 given index. |
|
569 |
|
570 @param index index of the item |
|
571 @type QModelIndex |
|
572 @return created context menu |
|
573 @rtype QMenu |
|
574 """ |
|
575 menu = QMenu(self) |
|
576 if self.isExpanded(index): |
|
577 menu.addAction(self.tr("Collapse"), |
|
578 lambda: self.collapse(index)) |
|
579 else: |
|
580 act = menu.addAction(self.tr("Expand"), |
|
581 lambda: self.expand(index)) |
|
582 act.setEnabled(self.model().hasChildren(index)) |
|
583 menu.addSeparator() |
|
584 |
|
585 act = menu.addAction(self.tr("Show Source"), |
|
586 lambda: self.__gotoTestDefinition(index)) |
|
587 act.setEnabled( |
|
588 self.model().data(index, Qt.ItemDataRole.UserRole) is not None |
|
589 ) |
|
590 menu.addSeparator() |
|
591 |
|
592 menu.addAction(self.tr("Collapse All"), self.collapseAll) |
|
593 menu.addAction(self.tr("Expand All"), self.expandAll) |
|
594 |
|
595 return menu |
|
596 |
|
597 def __createBackgroundContextMenu(self): |
|
598 """ |
|
599 Private method to create a context menu for the background. |
|
600 |
|
601 @return created context menu |
|
602 @rtype QMenu |
|
603 """ |
|
604 menu = QMenu(self) |
|
605 menu.addAction(self.tr("Collapse All"), self.collapseAll) |
|
606 menu.addAction(self.tr("Expand All"), self.expandAll) |
|
607 |
|
608 return menu |
517 |
609 |
518 # |
610 # |
519 # eflag: noqa = M821, M822 |
611 # eflag: noqa = M821, M822 |