Skip to content

dataviews

sleap.gui.dataviews

Data table widgets and view models used in GUI app.

Typically you'll need to subclass :py:class:GenericTableModel for your data (unless your data is already a list of dictionaries with keys matching the columns of the table you want), but you can use :py:class:GenericTableView as is. For example::

videos_table = GenericTableView(
    state=self.state,
    row_name="video",
    is_activatable=True,
    model=VideosTableModel(items=self.labels.videos, context=self.commands),
    )

Classes:

Name Description
GenericTableModel

Generic Qt table model to show a list of properties for some items.

GenericTableView

Qt table view for use with GenericTableModel (and subclasses).

LabeledFrameTableModel

Table model for listing instances in labeled frame.

SkeletonEdgesTableModel

Table model for skeleton edges.

SkeletonNodeModel

String list model for source/destination nodes of edges.

SkeletonNodesTableModel
SuggestionsTableModel

GenericTableModel

Bases: QAbstractTableModel

Generic Qt table model to show a list of properties for some items.

Typically this will be used as base class. Subclasses can implement methods: object_to_items: allows conversion from a single object to a list of items which correspond to rows of table. for example, a table which shows skeleton nodes could implement this method and return the list of nodes for skeleton. item_to_data: if each item isn't already a dictionary with keys for columns of table (i.e., properties attribute) and values to show in table, then use this method to convert each item to such a dict.

Note that if you need to convert a single object to a list of dictionaries, you can implement both steps in object_to_items (and use the default implementation of item_to_data which doesn't do any conversion), or you can implement this in two steps using the two methods. It doesn't make much difference which you do.

For editable table, you must implement can_set and set_item methods.

Usually it's simplest to override properties in the subclass, rather than passing as an init arg.

Parameters:

Name Type Description Default
properties Optional[List[str]]

The list of property names (table columns).

None
items Optional[list]

The list of items with said properties (rows).

None
context Optional[CommandContext]

A command context (required for editable items).

None

Methods:

Name Description
can_set

Virtual method, returns whether table cell is editable.

columnCount

Overrides Qt method, returns number of columns (attributes).

data

Overrides Qt method, returns data to show in table.

flags

Overrides Qt method, returns whether item is selectable etc.

get_from_idx

Gets item from QModelIndex.

get_item_color

Virtual method, returns color for given item.

headerData

Overrides Qt method, returns column (attribute) names.

object_to_items

Virtual method, convert object to list of items to show in rows.

rowCount

Overrides Qt method, returns number of rows (items).

setData

Overrides Qt method, dispatch for settable properties.

set_item

Virtual method, used to set value for item in table cell.

sort

Sorts table by given column and order.

Attributes:

Name Type Description
items

Gets or sets list of items to show in table.

original_items

Gets the original items (rather than the dictionary we build from it).

Source code in sleap/gui/dataviews.py
 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
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
class GenericTableModel(QtCore.QAbstractTableModel):
    """
    Generic Qt table model to show a list of properties for some items.

    Typically this will be used as base class. Subclasses can implement methods:
        object_to_items: allows conversion from a single object to a list of
            items which correspond to rows of table. for example, a table
            which shows skeleton nodes could implement this method and return
            the list of nodes for skeleton.
        item_to_data: if each item isn't already a dictionary with keys for
            columns of table (i.e., `properties` attribute) and values to show
            in table, then use this method to convert each item to such a dict.

    Note that if you need to convert a single object to a list of dictionaries,
    you can implement both steps in `object_to_items` (and use the default
    implementation of `item_to_data` which doesn't do any conversion), or you
    can implement this in two steps using the two methods. It doesn't make
    much difference which you do.

    For editable table, you must implement `can_set` and `set_item` methods.

    Usually it's simplest to override `properties` in the subclass, rather
    than passing as an init arg.

    Args:
        properties: The list of property names (table columns).
        items: The list of items with said properties (rows).
        context: A command context (required for editable items).
    """

    properties = None
    show_row_numbers: bool = True

    def __init__(
        self,
        items: Optional[list] = None,
        properties: Optional[List[str]] = None,
        context: Optional[CommandContext] = None,
    ):
        super(GenericTableModel, self).__init__()
        self.properties = properties or self.properties or []
        self.context = context
        self.items = items

    def object_to_items(self, item_list):
        """Virtual method, convert object to list of items to show in rows."""
        return item_list

    @property
    def items(self):
        """Gets or sets list of items to show in table."""
        return self._data

    @items.setter
    def items(self, obj):
        if not obj:
            self.beginResetModel()
            self._data = []
            self.endResetModel()
            return

        self.obj = obj
        item_list = self.object_to_items(obj)

        self.beginResetModel()
        if hasattr(self, "item_to_data"):
            self._data = []
            for item in item_list:
                item_data = self.item_to_data(obj, item)
                item_data["_original_item"] = item
                self._data.append(item_data)
        else:
            self._data = item_list
        self.endResetModel()

    @property
    def original_items(self):
        """
        Gets the original items (rather than the dictionary we build from it).
        """
        try:
            return [datum["_original_item"] for datum in self._data]
        except Exception:
            return self._data

    def get_item_color(self, item: Any, key: str):
        """Virtual method, returns color for given item."""
        return None

    def data(self, index: QtCore.QModelIndex, role=QtCore.Qt.DisplayRole):
        """Overrides Qt method, returns data to show in table."""
        if not index.isValid():
            return None

        idx = index.row()
        key = self.properties[index.column()]

        if idx >= self.rowCount():
            return None

        item = self.items[idx]
        if role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole:
            if isinstance(item, dict) and key in item:
                return item[key]

            if hasattr(item, key):
                return getattr(item, key)

        elif role == QtCore.Qt.ForegroundRole:
            return self.get_item_color(self.original_items[idx], key)

        elif role == QtCore.Qt.ToolTipRole:
            if isinstance(item, dict) and key in item:
                return item[key]

            if hasattr(item, key):
                return getattr(item, key)

        return None

    def setData(self, index: QtCore.QModelIndex, value: str, role=QtCore.Qt.EditRole):
        """Overrides Qt method, dispatch for settable properties."""
        if role == QtCore.Qt.EditRole:
            item, key = self.get_from_idx(index)

            # If nothing changed of the item, return true. (Issue #1013)
            if isinstance(item, dict):
                item_value = item.get(key, None)
            elif hasattr(item, key):
                item_value = getattr(item, key)
            else:
                item_value = None

            if (item_value is not None) and (item_value == value):
                return True

            # Otherwise set the item
            if self.can_set(item, key):
                self.set_item(item, key, value)
                self.dataChanged.emit(index, index)
                return True

        return False

    def rowCount(self, parent=None):
        """Overrides Qt method, returns number of rows (items)."""
        return len(self._data)

    def columnCount(self, parent=None):
        """Overrides Qt method, returns number of columns (attributes)."""
        return len(self.properties)

    def headerData(
        self, idx: int, orientation: QtCore.Qt.Orientation, role=QtCore.Qt.DisplayRole
    ):
        """Overrides Qt method, returns column (attribute) names."""
        if role == QtCore.Qt.DisplayRole:
            if orientation == QtCore.Qt.Horizontal:
                col_str = str(self.properties[idx])
                # use title case if key is lowercase
                if col_str == col_str.lower():
                    return col_str.title()
                # otherwise leave case as is
                return col_str
            elif orientation == QtCore.Qt.Vertical:
                # Add 1 to the row index so that we index from 1 instead of 0
                if self.show_row_numbers:
                    return str(idx + 1)
                return None

        return None

    def sort(
        self,
        column_idx: int,
        order: QtCore.Qt.SortOrder = QtCore.Qt.SortOrder.AscendingOrder,
    ):
        """
        Sorts table by given column and order.

        Correctly sorts numeric string (i.e., "123.45") numerically rather
        than alphabetically. Has logic for correctly sorting video frames by
        video then frame index.
        """
        prop = self.properties[column_idx]
        reverse = order == QtCore.Qt.SortOrder.DescendingOrder

        sort_function = itemgetter(prop)
        if prop in ("video", "frame"):
            if "video" in self.properties and "frame" in self.properties:
                sort_function = itemgetter("video", "frame")

        def string_safe_sort(x):
            sort_val = sort_function(x)
            try:
                return float(sort_val)
            except ValueError:
                return -np.inf
            except TypeError:
                return sort_val

        self.beginResetModel()
        self._data.sort(key=string_safe_sort, reverse=reverse)
        self.endResetModel()

    def get_from_idx(self, index: QtCore.QModelIndex):
        """Gets item from QModelIndex."""
        if not index.isValid():
            return None, None
        item = self.original_items[index.row()]
        key = self.properties[index.column()]
        return item, key

    def can_set(self, item, key):
        """Virtual method, returns whether table cell is editable."""
        return False

    def set_item(self, item, key, value):
        """Virtual method, used to set value for item in table cell."""
        pass

    def flags(self, index: QtCore.QModelIndex):
        """Overrides Qt method, returns whether item is selectable etc."""
        flags = QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable

        item, key = self.get_from_idx(index)
        if self.can_set(item, key):
            flags |= QtCore.Qt.ItemIsEditable
        return flags

items property writable

Gets or sets list of items to show in table.

original_items property

Gets the original items (rather than the dictionary we build from it).

can_set(item, key)

Virtual method, returns whether table cell is editable.

Source code in sleap/gui/dataviews.py
250
251
252
def can_set(self, item, key):
    """Virtual method, returns whether table cell is editable."""
    return False

columnCount(parent=None)

Overrides Qt method, returns number of columns (attributes).

Source code in sleap/gui/dataviews.py
185
186
187
def columnCount(self, parent=None):
    """Overrides Qt method, returns number of columns (attributes)."""
    return len(self.properties)

data(index, role=QtCore.Qt.DisplayRole)

Overrides Qt method, returns data to show in table.

Source code in sleap/gui/dataviews.py
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
def data(self, index: QtCore.QModelIndex, role=QtCore.Qt.DisplayRole):
    """Overrides Qt method, returns data to show in table."""
    if not index.isValid():
        return None

    idx = index.row()
    key = self.properties[index.column()]

    if idx >= self.rowCount():
        return None

    item = self.items[idx]
    if role == QtCore.Qt.DisplayRole or role == QtCore.Qt.EditRole:
        if isinstance(item, dict) and key in item:
            return item[key]

        if hasattr(item, key):
            return getattr(item, key)

    elif role == QtCore.Qt.ForegroundRole:
        return self.get_item_color(self.original_items[idx], key)

    elif role == QtCore.Qt.ToolTipRole:
        if isinstance(item, dict) and key in item:
            return item[key]

        if hasattr(item, key):
            return getattr(item, key)

    return None

flags(index)

Overrides Qt method, returns whether item is selectable etc.

Source code in sleap/gui/dataviews.py
258
259
260
261
262
263
264
265
def flags(self, index: QtCore.QModelIndex):
    """Overrides Qt method, returns whether item is selectable etc."""
    flags = QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable

    item, key = self.get_from_idx(index)
    if self.can_set(item, key):
        flags |= QtCore.Qt.ItemIsEditable
    return flags

get_from_idx(index)

Gets item from QModelIndex.

Source code in sleap/gui/dataviews.py
242
243
244
245
246
247
248
def get_from_idx(self, index: QtCore.QModelIndex):
    """Gets item from QModelIndex."""
    if not index.isValid():
        return None, None
    item = self.original_items[index.row()]
    key = self.properties[index.column()]
    return item, key

get_item_color(item, key)

Virtual method, returns color for given item.

Source code in sleap/gui/dataviews.py
122
123
124
def get_item_color(self, item: Any, key: str):
    """Virtual method, returns color for given item."""
    return None

headerData(idx, orientation, role=QtCore.Qt.DisplayRole)

Overrides Qt method, returns column (attribute) names.

Source code in sleap/gui/dataviews.py
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
def headerData(
    self, idx: int, orientation: QtCore.Qt.Orientation, role=QtCore.Qt.DisplayRole
):
    """Overrides Qt method, returns column (attribute) names."""
    if role == QtCore.Qt.DisplayRole:
        if orientation == QtCore.Qt.Horizontal:
            col_str = str(self.properties[idx])
            # use title case if key is lowercase
            if col_str == col_str.lower():
                return col_str.title()
            # otherwise leave case as is
            return col_str
        elif orientation == QtCore.Qt.Vertical:
            # Add 1 to the row index so that we index from 1 instead of 0
            if self.show_row_numbers:
                return str(idx + 1)
            return None

    return None

object_to_items(item_list)

Virtual method, convert object to list of items to show in rows.

Source code in sleap/gui/dataviews.py
81
82
83
def object_to_items(self, item_list):
    """Virtual method, convert object to list of items to show in rows."""
    return item_list

rowCount(parent=None)

Overrides Qt method, returns number of rows (items).

Source code in sleap/gui/dataviews.py
181
182
183
def rowCount(self, parent=None):
    """Overrides Qt method, returns number of rows (items)."""
    return len(self._data)

setData(index, value, role=QtCore.Qt.EditRole)

Overrides Qt method, dispatch for settable properties.

Source code in sleap/gui/dataviews.py
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
def setData(self, index: QtCore.QModelIndex, value: str, role=QtCore.Qt.EditRole):
    """Overrides Qt method, dispatch for settable properties."""
    if role == QtCore.Qt.EditRole:
        item, key = self.get_from_idx(index)

        # If nothing changed of the item, return true. (Issue #1013)
        if isinstance(item, dict):
            item_value = item.get(key, None)
        elif hasattr(item, key):
            item_value = getattr(item, key)
        else:
            item_value = None

        if (item_value is not None) and (item_value == value):
            return True

        # Otherwise set the item
        if self.can_set(item, key):
            self.set_item(item, key, value)
            self.dataChanged.emit(index, index)
            return True

    return False

set_item(item, key, value)

Virtual method, used to set value for item in table cell.

Source code in sleap/gui/dataviews.py
254
255
256
def set_item(self, item, key, value):
    """Virtual method, used to set value for item in table cell."""
    pass

sort(column_idx, order=QtCore.Qt.SortOrder.AscendingOrder)

Sorts table by given column and order.

Correctly sorts numeric string (i.e., "123.45") numerically rather than alphabetically. Has logic for correctly sorting video frames by video then frame index.

Source code in sleap/gui/dataviews.py
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
def sort(
    self,
    column_idx: int,
    order: QtCore.Qt.SortOrder = QtCore.Qt.SortOrder.AscendingOrder,
):
    """
    Sorts table by given column and order.

    Correctly sorts numeric string (i.e., "123.45") numerically rather
    than alphabetically. Has logic for correctly sorting video frames by
    video then frame index.
    """
    prop = self.properties[column_idx]
    reverse = order == QtCore.Qt.SortOrder.DescendingOrder

    sort_function = itemgetter(prop)
    if prop in ("video", "frame"):
        if "video" in self.properties and "frame" in self.properties:
            sort_function = itemgetter("video", "frame")

    def string_safe_sort(x):
        sort_val = sort_function(x)
        try:
            return float(sort_val)
        except ValueError:
            return -np.inf
        except TypeError:
            return sort_val

    self.beginResetModel()
    self._data.sort(key=string_safe_sort, reverse=reverse)
    self.endResetModel()

GenericTableView

Bases: QTableView

Qt table view for use with GenericTableModel (and subclasses).

Uses the :py:class:GuiState object to keep track of which row/item is selected. If the row_name attribute is "foo", then a "foo_selected" state will be item corresponding to the currently selected row in table (and the table will select the row if this state is updated by something else). When is_activatable is True, then a "foo" state will also be set to the item when a row is activated--typically by being double-clicked. This state can then be used to trigger something else outside the table.

Note that by default "selected_" is used for the state key, e.g., "selected_foo", but you can set the name_prefix attribute/init arg if for some reason you need this to be different. For instance, the table of instances in the GUI sets this to "" so that the row for an instance is automatically selected when state["instance"] is set outside the table.

"ellipsis_left" can be used to make the TableView truncate cell content on the left instead of the right side. By default, the argument is set to False, i.e. truncation on the right side, which is also the default for QTableView.

Methods:

Name Description
activateSelected

Activate item currently selected in table.

getSelectedRowItem

Return item corresponding to currently selected row.

selectRow

Select row corresponding to index.

selectRowItem

Select row corresponding to item.

selectionChanged

Custom event handler.

Source code in sleap/gui/dataviews.py
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
class GenericTableView(QtWidgets.QTableView):
    """
    Qt table view for use with `GenericTableModel` (and subclasses).

    Uses the :py:class:`GuiState` object to keep track of which row/item is
    selected. If the `row_name` attribute is "foo", then a "foo_selected"
    state will be item corresponding to the currently selected row in table
    (and the table will select the row if this state is updated by something
    else). When `is_activatable` is True, then a "foo" state will also be
    set to the item when a row is activated--typically by being double-clicked.
    This state can then be used to trigger something else outside the table.

    Note that by default "selected_" is used for the state key, e.g.,
    "selected_foo", but you can set the `name_prefix` attribute/init arg if
    for some reason you need this to be different. For instance, the table
    of instances in the GUI sets this to "" so that the row for an instance
    is automatically selected when `state["instance"]` is set outside the table.

    "ellipsis_left" can be used to make the TableView truncate cell content on
    the left instead of the right side. By default, the argument is set to
    False, i.e. truncation on the right side, which is also the default for
    QTableView.
    """

    row_name: Optional[str] = None
    name_prefix: str = "selected_"
    is_activatable: bool = False
    is_sortable: bool = False

    def __init__(
        self,
        model: QtCore.QAbstractTableModel,
        state: GuiState = None,
        row_name: Optional[str] = None,
        name_prefix: Optional[str] = None,
        is_sortable: bool = False,
        is_activatable: bool = False,
        ellipsis_left: bool = False,
        multiple_selection: bool = False,
    ):
        super(GenericTableView, self).__init__()

        self.state = state or GuiState()
        self.row_name = row_name or self.row_name
        self.name_prefix = name_prefix if name_prefix is not None else self.name_prefix
        self.is_sortable = is_sortable or self.is_sortable
        self.is_activatable = is_activatable or self.is_activatable
        self.multiple_selection = multiple_selection

        self.setModel(model)

        if ellipsis_left:
            self.setTextElideMode(QtCore.Qt.ElideLeft)
            self.setWordWrap(False)
        self.horizontalHeader().setStretchLastSection(True)
        self.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
        if self.multiple_selection:
            self.setSelectionMode(QtWidgets.QAbstractItemView.ExtendedSelection)
        else:
            self.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection)
        self.setSortingEnabled(self.is_sortable)

        self.doubleClicked.connect(self.activateSelected)
        if self.row_name:
            self.state.connect(self.name_prefix + self.row_name, self.selectRowItem)

    def selectionChanged(self, new, old):
        """Custom event handler."""
        super(GenericTableView, self).selectionChanged(new, old)

        if self.row_name:
            item = self.getSelectedRowItem()
            self.state[self.name_prefix + self.row_name] = item

    def activateSelected(self, *args):
        """Activate item currently selected in table.

        "Activate" means that the relevant :py:class:`GuiState` state variable
        is set to the currently selected item.
        """
        if self.is_activatable:
            self.state[self.row_name] = self.getSelectedRowItem()

    def selectRowItem(self, item: Any):
        """Select row corresponding to item.

        If the table model converts items to dictionaries (using `item_to_data`
        method), then `item` argument should be the original item, not the
        converted dict.
        """
        if not item:
            return

        idx = self.model().original_items.index(item)
        table_row_idx = self.model().createIndex(idx, 0)
        self.setCurrentIndex(table_row_idx)

        if self.row_name:
            self.state[self.name_prefix + self.row_name] = item

    def selectRow(self, idx: int):
        """Select row corresponding to index."""
        self.selectRowItem(self.model().original_items[idx])

    def getSelectedRowItem(self) -> Any:
        """Return item corresponding to currently selected row.

        Note that if the table model converts items to dictionaries (using
        `item_to_data` method), then returned item will be the original item,
        not the converted dict.
        """
        idx = self.currentIndex()

        if self.multiple_selection:
            idx_temp = set([x.row() for x in self.selectedIndexes()])
            self.state[f"selected_batch_{self.row_name}"] = idx_temp

        if not idx.isValid():
            return None
        return self.model().original_items[idx.row()]

activateSelected(*args)

Activate item currently selected in table.

"Activate" means that the relevant :py:class:GuiState state variable is set to the currently selected item.

Source code in sleap/gui/dataviews.py
342
343
344
345
346
347
348
349
def activateSelected(self, *args):
    """Activate item currently selected in table.

    "Activate" means that the relevant :py:class:`GuiState` state variable
    is set to the currently selected item.
    """
    if self.is_activatable:
        self.state[self.row_name] = self.getSelectedRowItem()

getSelectedRowItem()

Return item corresponding to currently selected row.

Note that if the table model converts items to dictionaries (using item_to_data method), then returned item will be the original item, not the converted dict.

Source code in sleap/gui/dataviews.py
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
def getSelectedRowItem(self) -> Any:
    """Return item corresponding to currently selected row.

    Note that if the table model converts items to dictionaries (using
    `item_to_data` method), then returned item will be the original item,
    not the converted dict.
    """
    idx = self.currentIndex()

    if self.multiple_selection:
        idx_temp = set([x.row() for x in self.selectedIndexes()])
        self.state[f"selected_batch_{self.row_name}"] = idx_temp

    if not idx.isValid():
        return None
    return self.model().original_items[idx.row()]

selectRow(idx)

Select row corresponding to index.

Source code in sleap/gui/dataviews.py
368
369
370
def selectRow(self, idx: int):
    """Select row corresponding to index."""
    self.selectRowItem(self.model().original_items[idx])

selectRowItem(item)

Select row corresponding to item.

If the table model converts items to dictionaries (using item_to_data method), then item argument should be the original item, not the converted dict.

Source code in sleap/gui/dataviews.py
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
def selectRowItem(self, item: Any):
    """Select row corresponding to item.

    If the table model converts items to dictionaries (using `item_to_data`
    method), then `item` argument should be the original item, not the
    converted dict.
    """
    if not item:
        return

    idx = self.model().original_items.index(item)
    table_row_idx = self.model().createIndex(idx, 0)
    self.setCurrentIndex(table_row_idx)

    if self.row_name:
        self.state[self.name_prefix + self.row_name] = item

selectionChanged(new, old)

Custom event handler.

Source code in sleap/gui/dataviews.py
334
335
336
337
338
339
340
def selectionChanged(self, new, old):
    """Custom event handler."""
    super(GenericTableView, self).selectionChanged(new, old)

    if self.row_name:
        item = self.getSelectedRowItem()
        self.state[self.name_prefix + self.row_name] = item

LabeledFrameTableModel

Bases: GenericTableModel

Table model for listing instances in labeled frame.

Allows editing track names.

Parameters:

Name Type Description Default
labeled_frame

LabeledFrame to show

required
labels

Labels datasource

required
Source code in sleap/gui/dataviews.py
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
class LabeledFrameTableModel(GenericTableModel):
    """Table model for listing instances in labeled frame.

    Allows editing track names.

    Args:
        labeled_frame: `LabeledFrame` to show
        labels: `Labels` datasource
    """

    properties = ("points", "track", "score", "skeleton")

    def object_to_items(self, labeled_frame: LabeledFrame):
        if not labeled_frame:
            return []
        return get_instances_to_show(labeled_frame)

    def item_to_data(self, obj, item):
        instance = item

        points = (
            f"{len(get_nodes_from_instance(instance))}/{len(instance.skeleton.nodes)}"
        )
        track_name = instance.track.name if instance.track else ""
        score = ""
        if hasattr(instance, "score"):
            score = str(round(instance.score, 2))

        return dict(
            points=points,
            track=track_name,
            score=score,
            skeleton=instance.skeleton.name,
        )

    def get_item_color(self, item: Any, key: str):
        if key == "track" and item.track is not None:
            track = item.track
            return QtGui.QColor(*self.context.app.color_manager.get_track_color(track))
        return None

    def can_set(self, item, key):
        if key == "track" and item.track is not None:
            return True

    def set_item(self, item, key, value):
        if key == "track":
            self.context.setTrackName(item.track, value)

SkeletonEdgesTableModel

Bases: GenericTableModel

Table model for skeleton edges.

Source code in sleap/gui/dataviews.py
451
452
453
454
455
456
457
458
459
460
461
462
463
464
class SkeletonEdgesTableModel(GenericTableModel):
    """Table model for skeleton edges."""

    properties = ("source", "destination")

    def object_to_items(self, skeleton: Skeleton):
        items = []
        self.skeleton = skeleton
        if hasattr(skeleton, "edges"):
            items = [
                dict(source=edge[0].name, destination=edge[1].name)
                for edge in skeleton.edges
            ]
        return items

SkeletonNodeModel

Bases: QStringListModel

String list model for source/destination nodes of edges.

Parameters:

Name Type Description Default
skeleton Skeleton

The skeleton for which to list nodes.

required
src_node Callable

If given, then we assume that this model is being used for edge destination node. Otherwise, we assume that this model is being used for an edge source node. If given, then this should be function that will return the selected edge source node.

None

Methods:

Name Description
columnCount

Overrides Qt method, returns number of columns (1).

data

Overrides Qt method, returns data for given row.

flags

Overrides Qt method, returns flags (editable etc).

rowCount

Overrides Qt method, returns number of rows.

Attributes:

Name Type Description
skeleton

Gets or sets current skeleton.

Source code in sleap/gui/dataviews.py
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
class SkeletonNodeModel(QtCore.QStringListModel):
    """
    String list model for source/destination nodes of edges.

    Args:
        skeleton: The skeleton for which to list nodes.
        src_node: If given, then we assume that this model is being used for
            edge destination node. Otherwise, we assume that this model is
            being used for an edge source node.
            If given, then this should be function that will return the
            selected edge source node.
    """

    def __init__(self, skeleton: Skeleton, src_node: Callable = None):
        super(SkeletonNodeModel, self).__init__()
        self._src_node = src_node
        self.skeleton = skeleton

    @property
    def skeleton(self):
        """Gets or sets current skeleton."""
        return self._skeleton

    @skeleton.setter
    def skeleton(self, val):
        self.beginResetModel()

        self._skeleton = val
        # if this is a dst node, then determine list based on source node
        if self._src_node is not None:
            self._node_list = self._valid_dst()
        # otherwise, show all nodes for skeleton
        else:
            self._node_list = self.skeleton.node_names

        self.endResetModel()

    def _valid_dst(self):
        # get source node using callback
        src_node = self._src_node()

        def is_valid_dst(node):
            # node cannot be dst of itself
            if node == src_node:
                return False
            # node cannot be dst if it's already dst of this src
            if (src_node, node) in self.skeleton.edge_names:
                return False
            return True

        # Filter down to valid destination nodes
        valid_dst_nodes = list(filter(is_valid_dst, self.skeleton.node_names))

        return valid_dst_nodes

    def data(self, index: QtCore.QModelIndex, role=QtCore.Qt.DisplayRole):
        """Overrides Qt method, returns data for given row."""
        if role == QtCore.Qt.DisplayRole and index.isValid():
            idx = index.row()
            return self._node_list[idx]

        return None

    def rowCount(self, parent):
        """Overrides Qt method, returns number of rows."""
        return len(self._node_list)

    def columnCount(self, parent):
        """Overrides Qt method, returns number of columns (1)."""
        return 1

    def flags(self, index: QtCore.QModelIndex):
        """Overrides Qt method, returns flags (editable etc)."""
        return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable

skeleton property writable

Gets or sets current skeleton.

columnCount(parent)

Overrides Qt method, returns number of columns (1).

Source code in sleap/gui/dataviews.py
668
669
670
def columnCount(self, parent):
    """Overrides Qt method, returns number of columns (1)."""
    return 1

data(index, role=QtCore.Qt.DisplayRole)

Overrides Qt method, returns data for given row.

Source code in sleap/gui/dataviews.py
656
657
658
659
660
661
662
def data(self, index: QtCore.QModelIndex, role=QtCore.Qt.DisplayRole):
    """Overrides Qt method, returns data for given row."""
    if role == QtCore.Qt.DisplayRole and index.isValid():
        idx = index.row()
        return self._node_list[idx]

    return None

flags(index)

Overrides Qt method, returns flags (editable etc).

Source code in sleap/gui/dataviews.py
672
673
674
def flags(self, index: QtCore.QModelIndex):
    """Overrides Qt method, returns flags (editable etc)."""
    return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable

rowCount(parent)

Overrides Qt method, returns number of rows.

Source code in sleap/gui/dataviews.py
664
665
666
def rowCount(self, parent):
    """Overrides Qt method, returns number of rows."""
    return len(self._node_list)

SkeletonNodesTableModel

Bases: GenericTableModel

Methods:

Name Description
object_to_items

Converts given skeleton to list of nodes to show in table.

Source code in sleap/gui/dataviews.py
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
class SkeletonNodesTableModel(GenericTableModel):
    properties = ("name", "symmetry")

    def object_to_items(self, skeleton: Skeleton):
        """Converts given skeleton to list of nodes to show in table."""
        items = skeleton.nodes
        self.skeleton = skeleton
        return items

    def item_to_data(self, obj, item):
        return dict(name=item.name, symmetry=get_symmetry_node(obj, item.name))

    def can_set(self, item, key):
        return True

    def set_item(self, item, key, value):
        if key == "name" and value:
            self.context.setNodeName(skeleton=self.obj, node=item, name=value)
        elif key == "symmetry":
            self.context.setNodeSymmetry(skeleton=self.obj, node=item, symmetry=value)

object_to_items(skeleton)

Converts given skeleton to list of nodes to show in table.

Source code in sleap/gui/dataviews.py
432
433
434
435
436
def object_to_items(self, skeleton: Skeleton):
    """Converts given skeleton to list of nodes to show in table."""
    items = skeleton.nodes
    self.skeleton = skeleton
    return items

SuggestionsTableModel

Bases: GenericTableModel

Methods:

Name Description
sort

Sorts table by given column and order.

Source code in sleap/gui/dataviews.py
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
class SuggestionsTableModel(GenericTableModel):
    properties = ("video", "frame", "group", "labeled", "mean score")

    def item_to_data(self, obj, item):
        labels = self.context.labels
        item_dict = dict()

        item_dict["SuggestionFrame"] = item

        video_idx = labels.videos.index(item.video) + 1
        video_name = os.path.basename(item.video.filename)
        video_string = f"{video_idx}: {video_name}"

        item_dict["group"] = "0"
        item_dict["group_int"] = 0
        item_dict["video"] = video_string
        item_dict["frame"] = int(item.frame_idx) + 1  # start at frame 1 rather than 0

        # show how many labeled instances are in this frame
        lf = labels.find(item.video, item.frame_idx)
        lf = lf[0] if lf else None
        val = 0 if lf is None else len(lf.user_instances)
        val = str(val) if val > 0 else ""
        item_dict["labeled"] = val

        # calculate score for frame
        scores = [
            inst.score
            for lf in labels.find(item.video, item.frame_idx)
            for inst in lf
            if hasattr(inst, "score")
        ]
        val = float(sum(scores) / len(scores)) if scores else ""
        item_dict["mean score"] = val

        return item_dict

    def sort(self, column_idx: int, order: QtCore.Qt.SortOrder):
        """Sorts table by given column and order."""
        prop = self.properties[column_idx]
        reverse = order == QtCore.Qt.SortOrder.DescendingOrder

        if prop != "group":
            super(SuggestionsTableModel, self).sort(column_idx, order)
        else:
            if not reverse:
                # Use group_int (int) instead of group (str).
                self.beginResetModel()
                self._data.sort(key=itemgetter("group_int"))
                self.endResetModel()

            else:
                # Instead of a reverse sort order on groups, we'll interleave the
                # items so that we get the earliest item from each group, then the
                # second item from each group, and so on.

                # Make a decorated list of items with positions in group (plus the
                # secondary sort keys: group, video, and frame)
                self._data.sort(key=itemgetter("group_int"))
                decorated_data = []
                last_group = object()
                for item in self._data:
                    if last_group != item["group_int"]:
                        group_i = 0
                    decorated_data.append(
                        (group_i, item["group_int"], item["video"], item["frame"], item)
                    )
                    last_group = item["group_int"]
                    group_i += 1

                # Sort decorated list
                decorated_data.sort()

                # Undecorate the list and update table
                self.beginResetModel()
                self._data = [item for (*_, item) in decorated_data]
                self.endResetModel()

        # Update order in project (so order can be saved and affects what we
        # consider previous/next suggestion for navigation).
        resorted_suggestions = [item["SuggestionFrame"] for item in self._data]
        self.context.labels.suggestions = resorted_suggestions

sort(column_idx, order)

Sorts table by given column and order.

Source code in sleap/gui/dataviews.py
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
def sort(self, column_idx: int, order: QtCore.Qt.SortOrder):
    """Sorts table by given column and order."""
    prop = self.properties[column_idx]
    reverse = order == QtCore.Qt.SortOrder.DescendingOrder

    if prop != "group":
        super(SuggestionsTableModel, self).sort(column_idx, order)
    else:
        if not reverse:
            # Use group_int (int) instead of group (str).
            self.beginResetModel()
            self._data.sort(key=itemgetter("group_int"))
            self.endResetModel()

        else:
            # Instead of a reverse sort order on groups, we'll interleave the
            # items so that we get the earliest item from each group, then the
            # second item from each group, and so on.

            # Make a decorated list of items with positions in group (plus the
            # secondary sort keys: group, video, and frame)
            self._data.sort(key=itemgetter("group_int"))
            decorated_data = []
            last_group = object()
            for item in self._data:
                if last_group != item["group_int"]:
                    group_i = 0
                decorated_data.append(
                    (group_i, item["group_int"], item["video"], item["frame"], item)
                )
                last_group = item["group_int"]
                group_i += 1

            # Sort decorated list
            decorated_data.sort()

            # Undecorate the list and update table
            self.beginResetModel()
            self._data = [item for (*_, item) in decorated_data]
            self.endResetModel()

    # Update order in project (so order can be saved and affects what we
    # consider previous/next suggestion for navigation).
    resorted_suggestions = [item["SuggestionFrame"] for item in self._data]
    self.context.labels.suggestions = resorted_suggestions