diff --git a/python/graph_list_filter_manager.py b/python/graph_list_filter_manager.py index bce5bf3240827a324ddb1aadf269f77f3f0df74c..efa6480a244a7dbd4d93a1ad4b36d60efc305d26 100644 --- a/python/graph_list_filter_manager.py +++ b/python/graph_list_filter_manager.py @@ -15,8 +15,7 @@ class GraphListFilterManager(QObject): def __init__(self, gui_manager): super().__init__() - self.graph_list_proxy_model = gui_manager.graph_list_view \ - .graph_list_proxy_model + self.graph_list_proxy_model = gui_manager.graph_list_proxy_model self.filter_event_dialog_wrapper = FilterEventDialogWrapper( gui_manager.w, gui_manager.graphs_manager, diff --git a/python/graph_list_selection_model.py b/python/graph_list_selection_model.py index 7d677e5cd6b9319f1e80394ef5a87a20518fa3ce..292da31321172b10b799cc9f697183d35f3c8249 100644 --- a/python/graph_list_selection_model.py +++ b/python/graph_list_selection_model.py @@ -5,7 +5,6 @@ from PySide6.QtCore import Qt from PySide6.QtCore import QModelIndex from PySide6.QtCore import QItemSelectionModel from PySide6.QtWidgets import QAbstractItemView, QTableView, QHeaderView -from graph_list_table_model import COLUMN_COUNT from graph_list_sort_filter_proxy_model import GraphListSortFilterProxyModel from preferences import preferences from graph_item import GraphItem diff --git a/python/graph_list_sort_manager.py b/python/graph_list_sort_manager.py index e4850efc8889091988eb8b625fd11ec59f2ae106..92d8c447ee361921f70d6b0176e6a12f455384d5 100644 --- a/python/graph_list_sort_manager.py +++ b/python/graph_list_sort_manager.py @@ -20,7 +20,8 @@ class GraphListSortManager(QObject): def __init__(self, gui_manager): super().__init__() self.graph_list_proxy_model = \ - gui_manager.graph_list_view.graph_list_proxy_model + gui_manager.graph_list_proxy_model + self.graph_list_table_dialog = gui_manager.graph_list_table_dialog # connect to reset selections on graphs loaded event gui_manager.graphs_manager.signal_graphs_loaded.connect( @@ -63,32 +64,44 @@ class GraphListSortManager(QObject): self.sort_menu, "&Marked", MARK_COLUMN)) # size - self.sort_menu_size = self.sort_menu.addMenu("&Size") - self.sort_menu_size.addAction(self._make_ascending_sort_action( - self.sort_menu_size, "Ascending", SIZE_COLUMN)) - self.sort_menu_size.addAction(self._make_descending_sort_action( - self.sort_menu_size, "Descending", SIZE_COLUMN)) + sort_menu_size = self.sort_menu.addMenu("&Size") + sort_menu_size.addAction(self._make_ascending_sort_action( + sort_menu_size, "Ascending", SIZE_COLUMN)) + sort_menu_size.addAction(self._make_descending_sort_action( + sort_menu_size, "Descending", SIZE_COLUMN)) # probability - self.sort_menu_size = self.sort_menu.addMenu("&Probability") - self.sort_menu_size.addAction(self._make_ascending_sort_action( - self.sort_menu_size, "Ascending", PROBABILITY_COLUMN)) - self.sort_menu_size.addAction(self._make_descending_sort_action( - self.sort_menu_size, "Descending", PROBABILITY_COLUMN)) + sort_menu_probability = self.sort_menu.addMenu("&Probability") + sort_menu_probability.addAction(self._make_ascending_sort_action( + sort_menu_probability, "Ascending", PROBABILITY_COLUMN)) + sort_menu_probability.addAction(self._make_descending_sort_action( + sort_menu_probability, "Descending", PROBABILITY_COLUMN)) # trace index - self.sort_menu_size = self.sort_menu.addMenu("&Trace index") - self.sort_menu_size.addAction(self._make_ascending_sort_action( - self.sort_menu_size, "Ascending", TRACE_INDEX_COLUMN)) - self.sort_menu_size.addAction(self._make_descending_sort_action( - self.sort_menu_size, "Descending", TRACE_INDEX_COLUMN)) - - # button + sort_menu_trace_index = self.sort_menu.addMenu("&Trace index") + sort_menu_trace_index.addAction(self._make_ascending_sort_action( + sort_menu_trace_index, "Ascending", TRACE_INDEX_COLUMN)) + sort_menu_trace_index.addAction(self._make_descending_sort_action( + sort_menu_trace_index, "Descending", TRACE_INDEX_COLUMN)) + + # sort by attribute + action_sort_by_attribute = QAction("Sort by &attribute...", + self.sort_menu) + action_sort_by_attribute.setToolTip("Sort traces by attribute value") + action_sort_by_attribute.triggered.connect(self._sort_by_attribute) + self.sort_menu.addSeparator() + self.sort_menu.addAction(action_sort_by_attribute) + + # sort menu button self.sort_menu_button = mp_menu_button(self.sort_menu, QIcon(":/icons/sort_ascending"), "Sort", "Sort the events list") + @Slot() + def _sort_by_attribute(self): + self.graph_list_table_dialog.show() + @Slot() def _reset_selections(self): self.sort_group.actions()[0].setChecked(True) diff --git a/python/graph_list_table_dialog.py b/python/graph_list_table_dialog.py index c4260a49047c3d6cb7f20c24ecbf55426032c262..8b6a97fc39ff5d052077a47450d8c0889bcded67 100644 --- a/python/graph_list_table_dialog.py +++ b/python/graph_list_table_dialog.py @@ -21,7 +21,7 @@ class GraphListTableDialog(QDialog): self.setFixedSize(self.width(), self.height()) self.setWindowFlags(self.windowFlags() | Qt.Tool) self.setAttribute(Qt.WA_MacAlwaysShowToolWindow) - self.setWindowTitle("Sort traces by attribute") + self.setWindowTitle("Sort Traces by Attribute") # layout self.layout = QVBoxLayout(self) @@ -37,6 +37,7 @@ class GraphListTableDialog(QDialog): QAbstractItemView.SelectionMode.SingleSelection) self.table.setAlternatingRowColors(True) self.table.horizontalHeader().setStretchLastSection(True) + self.table.horizontalHeader().setDefaultAlignment(Qt.AlignLeft) self.table.hideColumn(GRAPH_COLUMN) self.layout.addWidget(self.table) @@ -54,9 +55,6 @@ class GraphListTableDialog(QDialog): # slot graph_list_selection_model.selectionChanged.connect(self._select_row) -#zz -# self.show() - @Slot(QItemSelection, QItemSelection) def _select_row(self, selected, deselected): # get a lsit of the same row index for each column else an empty list diff --git a/python/graph_list_table_model.py b/python/graph_list_table_model.py index 6a474219e439b81081fd352982689b07664828b9..a78fb0a394902356dc56bcf91eba7575dbab0516 100644 --- a/python/graph_list_table_model.py +++ b/python/graph_list_table_model.py @@ -3,6 +3,7 @@ from PySide6.QtCore import Slot # for signal/slot support from PySide6.QtCore import Qt, QAbstractTableModel, QModelIndex from graph_item import GraphItem from settings import settings +from say_info import say_column_names, say_column_map, say_info from mp_logger import log_to_statusbar # graph list model column constants @@ -11,9 +12,8 @@ TRACE_INDEX_COLUMN = 1 # values start at 1 MARK_COLUMN = 2 # "M" or "" PROBABILITY_COLUMN = 3 # float 0.0 to 1.0 SIZE_COLUMN = 4 # int, see implementation -COLUMN_COUNT = 5 # number of columns -_HEADERS = ["Graph", "index", "Mark", "Type 1 P", "Size"] +_HARDCODED_HEADERS = ["Graph", "index", "Mark", "Type 1 P", "Size"] class GraphListTableModel(QAbstractTableModel): """Provides the graph list model for graph list accessors, consisting of @@ -35,20 +35,25 @@ class GraphListTableModel(QAbstractTableModel): """ def __init__(self): + self.headers = list() super().__init__() - graphs = list() + self.graphs = list() + self.headers = list() + self.say_map = say_column_map(self.graphs, len(_HARDCODED_HEADERS)) def set_graph_list(self, graphs): self.beginResetModel() # because self.graphs change self.graphs = graphs + self.headers = _HARDCODED_HEADERS.copy() + self.headers.extend(say_column_names(graphs)) self.endResetModel() def headerData(self, section, orientation, role=Qt.ItemDataRole.DisplayRole): if role == Qt.ItemDataRole.DisplayRole \ and orientation == Qt.Orientation.Horizontal: - return _HEADERS[section] + return self.headers[section] else: return None # QVariant() @@ -57,7 +62,7 @@ class GraphListTableModel(QAbstractTableModel): return len(self.graphs) def columnCount(self, parent=QModelIndex()): - return COLUMN_COUNT + return len(self.headers) def data(self, model_index, role=Qt.ItemDataRole.DisplayRole): @@ -66,28 +71,37 @@ class GraphListTableModel(QAbstractTableModel): row = model_index.row() column = model_index.column() gry_graph = self.graphs[row].gry_graph + + # provide values for hardcoded columns if column == GRAPH_COLUMN: # graph_list_view_delegate renders this return None # QVariant() if column == TRACE_INDEX_COLUMN: return row + if row == 0: + # these columns are for traces only + return "NA" if column == MARK_COLUMN: - if "trace" in gry_graph: - return gry_graph["trace"]["mark"] - else: - return "U" + return gry_graph["trace"]["mark"] if column == PROBABILITY_COLUMN: - if "trace" in gry_graph: - return gry_graph["trace"]["probability"] - else: - return 0 + return gry_graph["trace"]["probability"] if column == SIZE_COLUMN: - if "trace" in gry_graph: - gry_trace = gry_graph["trace"] - return len(gry_trace["nodes"]) + len(gry_trace["edges"]) - else: - return 0 - raise RuntimeError("bad %d"%column) + gry_trace = gry_graph["trace"] + return len(gry_trace["nodes"]) + len(gry_trace["edges"]) + if column >= len(self.headers): + raise RuntimeError("bad %d"%column) + + # provide values for columns dynamically created from SAY nodes + header = self.headers[column] + says = say_info(gry_graph) + if header in says: + # good, says matches a header so return its numeric value + return says[header] + else: + # return warning when say node text does not match a header + return "Unexpected %s"%header + else: + # ignore this ItemDataRole return None # QVariant() diff --git a/python/graph_list_view.py b/python/graph_list_view.py index 812edb993accd19624f67012e15dfb57c9ae1b25..2be81077a6150fc868149df057179c04f43dda0c 100644 --- a/python/graph_list_view.py +++ b/python/graph_list_view.py @@ -5,7 +5,6 @@ from PySide6.QtCore import Qt from PySide6.QtCore import QModelIndex from PySide6.QtCore import QItemSelectionModel from PySide6.QtWidgets import QAbstractItemView, QTableView, QHeaderView -from graph_list_table_model import COLUMN_COUNT from graph_list_sort_filter_proxy_model import GraphListSortFilterProxyModel from graph_list_view_delegate import GraphListViewDelegate from preferences import preferences @@ -24,15 +23,10 @@ class GraphListView(QTableView): signal_preferences_changed): super().__init__(main_splitter) - self.graph_list_proxy_model = graph_list_proxy_model - # the data model - self.setModel(self.graph_list_proxy_model) + self.setModel(graph_list_proxy_model) # table appearance - for i in range(1, COLUMN_COUNT): - self.setColumnHidden(i, True) - self.setColumnWidth(0, 20) # otherwise minimum is 100 self.horizontalHeader().hide() self.verticalHeader().hide() self.setShowGrid(False) @@ -48,21 +42,21 @@ class GraphListView(QTableView): navigation_column_width_manager) self.setItemDelegate(view_delegate) + # connect model reset + graph_list_proxy_model.modelReset.connect(self._hide_unused_columns) + # connect width changed navigation_column_width_manager.signal_navigation_column_width_changed\ .connect(self._changed_width) - - # connect move scrollbar to top when graph is reloaded -#zz graphs_manager.signal_graphs_loaded.connect(self._reset_scrollbar) - graph_list_proxy_model.sourceModelChanged.connect(self._reset_scrollbar) - # connect scroll mode changed signal_preferences_changed.connect(self._set_scroll_mode) @Slot() - def _reset_scrollbar(self): - self.verticalScrollBar().setValue(0) + def _hide_unused_columns(self): + # hide all but the first column so the first column stretches + for i in range(1, self.model().columnCount()): + self.setColumnHidden(i, True) # width changed @Slot(int) diff --git a/python/say_info.py b/python/say_info.py new file mode 100644 index 0000000000000000000000000000000000000000..43cc4cb656972a79fb03b1518baf1a1acf0bf16b --- /dev/null +++ b/python/say_info.py @@ -0,0 +1,46 @@ +import re +from collections import defaultdict +"""Extract SAY titles and numbers from SAY trace nodes.""" + +_pattern = re.compile(r"\s+[0-9.]+\s+") + +# Return {say text, [numbers]} where: +# * say_text is say text with index prepended and numbers removed +# * and numbers is a list of zero or more numbers. +# Say nodes are type="T".""" +def say_info(gry_graph): + if not "trace" in gry_graph: + raise RuntimeError("bad") + + says = defaultdict(list) + for gry_node in gry_graph["trace"]["nodes"]: + if gry_node["type"] == "T": + label = gry_node["label"] + key = _pattern.sub(" ", label) + numbers = _pattern.findall(label) + for i, number in enumerate(numbers, 1): + try: + says["%i: %s"%(i, key)] = float(number) + except Exception as e: + # multiple decimal places can thwart _pattern so manage + # invalid sortable numbers as text + print("say_info.say_info: invalid number: %s"%number) + says["%i: %s"%(i, key)] = number + return says + +# Return a list of "say text" column names. +def say_column_names(graphs): + if len(graphs) >= 2: + says = say_info(graphs[1].gry_graph) + return sorted(says.keys()) + else: + return list() + +# Return a dict of {key="say text", value=column index} +def say_column_map(graphs, start_index): + column_names = say_column_names(graphs) + column_map = dict() + for i, column_name in enumerate(column_names, start_index): + column_map[column_name] = i + return column_map +