Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add CamerasTable to SessionsDock #1671

Merged
merged 27 commits into from
Mar 18, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
d681075
Added CamerasTable, SessionDocks and EditCommand
AdvaithRavishankar Jan 22, 2024
33c4bcb
added updates to app, commands and docks
AdvaithRavishankar Jan 23, 2024
d482bb4
Fixed bugs, drafted tests
AdvaithRavishankar Jan 23, 2024
7f74a8b
Added minor debug fixes to CamerasTableModel
AdvaithRavishankar Jan 23, 2024
3bb071f
Added Initial Tests
AdvaithRavishankar Jan 29, 2024
e24edc9
changed button to be disabled and made table in sessions docks keys
AdvaithRavishankar Jan 29, 2024
88cd019
fixed bugs with table display
AdvaithRavishankar Jan 29, 2024
a728dd4
fixed button functionality
AdvaithRavishankar Feb 6, 2024
f1b6b1b
fixed table updating
AdvaithRavishankar Feb 8, 2024
548caf5
pushed tests draft
AdvaithRavishankar Feb 8, 2024
33d2754
Use GuiState to determine current session
roomrys Mar 16, 2024
f414abb
Nit: Use camera_table instead of table["camera_table"]
roomrys Mar 16, 2024
dd3daae
Use GuiState instead of camcorder sessions list
roomrys Mar 16, 2024
45974e3
Nit: docstring format and lint
roomrys Mar 16, 2024
4f40b6e
Change row name to "camera"
roomrys Mar 16, 2024
47dfe95
Display all cameras (not just unlinked)
roomrys Mar 16, 2024
20d3e60
Reset selected camera to None after unlink video
roomrys Mar 16, 2024
ba0f556
Fix-up cameras table test
roomrys Mar 16, 2024
c493fd6
Fix-up docks test
Mar 18, 2024
907f873
Remove unused imports
Mar 18, 2024
c287df2
Merge branch 'liezl/add-gui-elements-for-sessions' of https://github.…
Mar 18, 2024
45efdab
Fix segmentation fault
Mar 18, 2024
cd8662e
Reorder buttons and tab sessions dock with others
Mar 18, 2024
25e848c
Lint
Mar 18, 2024
236d044
Update cameras model when change in selected session
Mar 18, 2024
4ecbb95
Add missing import
Mar 18, 2024
77283d0
Use "selected_session" in UnlinkVideo command
Mar 18, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion sleap/gui/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -1094,7 +1094,8 @@ def _update_gui_state(self):
has_selected_edge = self.state["selected_edge"] is not None
has_selected_video = self.state["selected_video"] is not None
has_video = self.state["video"] is not None

has_selected_camcorder = self.state["selected_camera_table"] is not None

has_frame_range = bool(self.state["has_frame_range"])
has_unsaved_changes = bool(self.state["has_changes"])
has_videos = self.labels is not None and len(self.labels.videos) > 0
Expand Down Expand Up @@ -1148,6 +1149,7 @@ def _update_gui_state(self):
self._buttons["show video"].setEnabled(has_selected_video)
self._buttons["remove video"].setEnabled(has_video)
self._buttons["delete instance"].setEnabled(has_selected_instance)
self._buttons["unlink video"].setEnabled(has_selected_camcorder)
self.suggestions_dock.suggestions_form_widget.buttons[
"generate_button"
].setEnabled(has_videos)
Expand Down Expand Up @@ -1236,6 +1238,12 @@ def _has_topic(topic_list):
if _has_topic([UpdateTopic.frame, UpdateTopic.project_instances]):
self.state["last_interacted_frame"] = self.state["labeled_frame"]

if _has_topic([UpdateTopic.sessions]):
self.sessions_dock.table["camera_table"].model().items = self.labels.sessions[0]
roomrys marked this conversation as resolved.
Show resolved Hide resolved
roomrys marked this conversation as resolved.
Show resolved Hide resolved

roomrys marked this conversation as resolved.
Show resolved Hide resolved



def plotFrame(self, *args, **kwargs):
"""Plots (or replots) current frame."""
if self.state["video"] is None:
Expand Down
21 changes: 21 additions & 0 deletions sleap/gui/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ class UpdateTopic(Enum):
frame = 8
project = 9
project_instances = 10
sessions = 11


class AppCommand:
Expand Down Expand Up @@ -655,6 +656,9 @@ def openPrereleaseVersion(self):
"""Open the current prerelease version."""
self.execute(OpenPrereleaseVersion)

def unlink_video_from_camera(self):
"""Unlinks video from a camera"""
self.execute(UnlinkVideo)

# File Commands

Expand Down Expand Up @@ -3901,3 +3905,20 @@ def copy_to_clipboard(text: str):
clipboard = QtWidgets.QApplication.clipboard()
clipboard.clear(mode=clipboard.Clipboard)
clipboard.setText(text, mode=clipboard.Clipboard)

class UnlinkVideo(EditCommand):
topics = [UpdateTopic.sessions]

@staticmethod
def do_action(context: CommandContext, params: dict):
camcorder = context.state["selected_camera_table"]
recording_session = camcorder.sessions[0] # potentially buggy

video = camcorder.get_video(recording_session)

if video is not None and recording_session is not None:
recording_session.remove_video(video)

roomrys marked this conversation as resolved.
Show resolved Hide resolved



19 changes: 19 additions & 0 deletions sleap/gui/dataviews.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
from sleap.io.dataset import Labels
from sleap.instance import LabeledFrame, Instance
from sleap.skeleton import Skeleton
from sleap.io.cameras import Camcorder, RecordingSession


class GenericTableModel(QtCore.QAbstractTableModel):
Expand Down Expand Up @@ -652,3 +653,21 @@ def columnCount(self, parent):
def flags(self, index: QtCore.QModelIndex):
"""Overrides Qt method, returns flags (editable etc)."""
return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable

class CamerasTableModel(GenericTableModel):
"""
Table model for for multi-view, showing which cameras
are assigned to which videos
roomrys marked this conversation as resolved.
Show resolved Hide resolved

Args:
obj: 'RecordingSession' which has information of cameras
and paired video
"""
roomrys marked this conversation as resolved.
Show resolved Hide resolved

properties = ("camera", "video")

def object_to_items(self, obj: RecordingSession):
return obj.linked_cameras

def item_to_data(self, obj: RecordingSession, item: Camcorder):
return {'camera': item.name, 'video': obj.get_video(item).filename}
roomrys marked this conversation as resolved.
Show resolved Hide resolved
45 changes: 44 additions & 1 deletion sleap/gui/widgets/docks.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
SkeletonNodesTableModel,
SuggestionsTableModel,
VideosTableModel,
CamerasTableModel,
)
from sleap.gui.dialogs.formbuilder import YamlFormWidget
from sleap.gui.widgets.views import CollapsibleWidget
Expand Down Expand Up @@ -574,9 +575,20 @@ def create_table_edit_buttons(self) -> QWidget:

class SessionsDock(DockWidget):
def __init__(self, main_window: Optional[QMainWindow]):
super().__init__(name="Sessions", main_window=main_window, model_type=None)
self.camera_model_type = CamerasTableModel
super().__init__(name="Sessions", main_window=main_window, model_type=[self.camera_model_type])


def lay_everything_out(self) -> None:
if self.table is None:
self.create_tables()


self.wgt_layout.addWidget(self.camera_table)

video_unlink_button = self.create_video_unlink_button()
self.wgt_layout.addWidget(video_unlink_button)

triangulation_options = self.create_triangulation_options()
self.wgt_layout.addWidget(triangulation_options)

Expand All @@ -601,3 +613,34 @@ def create_triangulation_options(self) -> QWidget:
hbw = QWidget()
hbw.setLayout(hb)
return hbw

def create_models(self) -> GenericTableModel:
main_window = self.main_window
self.camera_model = self.camera_model_type(
items=main_window.state["selected_session"], context=main_window.commands
)

return {"camera_model": self.camera_model}
Copy link

@coderabbitai coderabbitai bot Feb 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The method create_models returns a dictionary instead of a GenericTableModel instance. This does not align with the expected return type for model creation methods in dock widgets. Ensure consistency in return types across the application.

- return {"camera_model": self.camera_model}
+ return self.camera_model

Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.

Suggested change
def create_models(self) -> GenericTableModel:
main_window = self.main_window
self.camera_model = self.camera_model_type(
items=main_window.state["selected_session"], context=main_window.commands
)
return {"camera_model": self.camera_model}
def create_models(self) -> GenericTableModel:
main_window = self.main_window
self.camera_model = self.camera_model_type(
items=main_window.state["selected_session"], context=main_window.commands
)
return self.camera_model

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like your idea of using a dictionary to describe the models that are returned. Let's just update the typehinting:

    def create_models(self) -> Union[GenericTableModel, Dict[str, GenericTableModel]]:

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@roomrys: Acknowledged. Updating the type hinting as suggested introduces flexibility in handling multiple models within dock widgets, which can be particularly useful for complex interfaces. It's important to ensure that other parts of the application that interact with these models are compatible with this approach. This includes reviewing methods like create_tables and any other functionality that relies on the output of create_models to handle both a single model and a dictionary of models seamlessly. Additionally, documentation and possibly some internal logic may need updates to align with this change. If you've considered these aspects and are comfortable with the implications, this update can enhance the application's architecture by accommodating more complex data management scenarios.


def create_tables(self) -> GenericTableView:
if self.model is None:
self.create_models()

main_window = self.main_window
self.camera_table = GenericTableView(
state=main_window.state,
row_name="camera_table",
model=self.camera_model,
)

return {"camera_table": self.camera_table}
Copy link

@coderabbitai coderabbitai bot Feb 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The method create_tables returns a dictionary instead of a GenericTableView instance, which is inconsistent with the expected return type for table creation methods in dock widgets. Adjust to return the table directly.

- return {"camera_table": self.camera_table}
+ return self.camera_table

Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.

Suggested change
if self.model is None:
self.create_models()
main_window = self.main_window
self.camera_table = GenericTableView(
state=main_window.state,
row_name="camera_table",
model=self.camera_model,
)
return {"camera_table": self.camera_table}
if self.model is None:
self.create_models()
main_window = self.main_window
self.camera_table = GenericTableView(
state=main_window.state,
row_name="camera_table",
model=self.camera_model,
)
return self.camera_table

Copy link
Collaborator Author

@roomrys roomrys Feb 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do like how you used this in the _on_data_update callback... could you at least update the type hinting for the return on this function:

    def create_tables(self) -> Union[GenericTableView, Dict[str, GenericTableView]]:

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@roomrys: Absolutely, updating the type hinting for the create_tables method to accurately reflect its return type as GenericTableView makes perfect sense, especially given its usage in the _on_data_update callback. This will enhance code readability and maintainability. Thank you for pointing this out!


def create_video_unlink_button(self) -> QWidget:
main_window = self.main_window

hb = QHBoxLayout()
self.add_button(hb, "Unlink Video", main_window.commands.unlink_video_from_camera)

hbw = QWidget()
hbw.setLayout(hb)
return hbw
43 changes: 43 additions & 0 deletions tests/gui/test_dataviews.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import pytestqt

from sleap.gui.dataviews import *
from sleap.io.cameras import RecordingSession


def test_skeleton_nodes(qtbot, centered_pair_predictions):
Expand Down Expand Up @@ -84,3 +85,45 @@ def test_table_sort_string(qtbot):
# Make sure we can sort with both numbers and strings (i.e., "")
table.model().sort(0)
table.model().sort(1)

def test_camera_table(qtbot, multiview_min_session_labels):
table = CamerasTableModel(items=multiview_min_session_labels.sessions[0])

assert table.columnCount() == 2
assert table.rowCount() == 8

num_rows = table.rowCount()

table = GenericTableView(
row_name="instance",
is_sortable=True,
name_prefix="",
model=CamerasTableModel(items=multiview_min_session_labels.sessions[0]),
)

# Testing if all comcorders are presented in the correct row
camcorders = multiview_min_session_labels.sessions[0].linked_cameras

for i in range(num_rows):
table.selectRow(i)
assert table.getSelectedRowItem() == camcorders[i]
assert table.model().data(table.currentIndex()) == camcorders[i].name

# Testing if a comcorder change is reflected
idxs_to_remove = [1, 2, 7]
for idx in idxs_to_remove:
multiview_min_session_labels.sessions[0].remove_video(camcorders[idx].get_video(multiview_min_session_labels.sessions[0]))
roomrys marked this conversation as resolved.
Show resolved Hide resolved

removed_camcorder = [cam for i, cam in enumerate(camcorders) if i not in idxs_to_remove]

for i in range(num_rows-3):
table.selectRow(i)
assert table.getSelectedRowItem() == camcorders[i]
assert table.model().data(table.currentIndex()) == removed_camcorder[i].name
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test test_camera_table uses a hardcoded value for rowCount() assertion, which might lead to test fragility if the test data changes. Consider deriving the expected row count directly from the test data to make the test more robust.

- assert table.rowCount() == 8
+ assert table.rowCount() == len(multiview_min_session_labels.sessions[0].linked_cameras)

Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation.

Suggested change
def test_camera_table(qtbot, multiview_min_session_labels):
table = CamerasTableModel(items=multiview_min_session_labels.sessions[0])
assert table.columnCount() == 2
assert table.rowCount() == 8
num_rows = table.rowCount()
table = GenericTableView(
row_name="instance",
is_sortable=True,
name_prefix="",
model=CamerasTableModel(items=multiview_min_session_labels.sessions[0]),
)
# Testing if all comcorders are presented in the correct row
camcorders = multiview_min_session_labels.sessions[0].linked_cameras
for i in range(num_rows):
table.selectRow(i)
assert table.getSelectedRowItem() == camcorders[i]
assert table.model().data(table.currentIndex()) == camcorders[i].name
# Testing if a comcorder change is reflected
idxs_to_remove = [1, 2, 7]
for idx in idxs_to_remove:
multiview_min_session_labels.sessions[0].remove_video(camcorders[idx].get_video(multiview_min_session_labels.sessions[0]))
removed_camcorder = [cam for i, cam in enumerate(camcorders) if i not in idxs_to_remove]
for i in range(num_rows-3):
table.selectRow(i)
assert table.getSelectedRowItem() == camcorders[i]
assert table.model().data(table.currentIndex()) == removed_camcorder[i].name
def test_camera_table(qtbot, multiview_min_session_labels):
table = CamerasTableModel(items=multiview_min_session_labels.sessions[0])
assert table.columnCount() == 2
assert table.rowCount() == len(multiview_min_session_labels.sessions[0].linked_cameras)
num_rows = table.rowCount()
table = GenericTableView(
row_name="instance",
is_sortable=True,
name_prefix="",
model=CamerasTableModel(items=multiview_min_session_labels.sessions[0]),
)
# Testing if all comcorders are presented in the correct row
camcorders = multiview_min_session_labels.sessions[0].linked_cameras
for i in range(num_rows):
table.selectRow(i)
assert table.getSelectedRowItem() == camcorders[i]
assert table.model().data(table.currentIndex()) == camcorders[i].name
# Testing if a comcorder change is reflected
idxs_to_remove = [1, 2, 7]
for idx in idxs_to_remove:
multiview_min_session_labels.sessions[0].remove_video(camcorders[idx].get_video(multiview_min_session_labels.sessions[0]))
removed_camcorder = [cam for i, cam in enumerate(camcorders) if i not in idxs_to_remove]
for i in range(num_rows-3):
table.selectRow(i)
assert table.getSelectedRowItem() == camcorders[i]
assert table.model().data(table.currentIndex()) == removed_camcorder[i].name

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The loop for verifying removed camcorders iterates over the original number of rows minus 3, assuming specific camcorders are removed. This approach is fragile and may not accurately reflect the GUI's state after removals. Ensure the test dynamically adjusts based on the actual state of the GUI and the session object.

Copy link
Collaborator Author

@roomrys roomrys Feb 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is testing what you think it is testing (that cameras are removed from table) as the table model is not automatically updated if we just use API calls. Since we are just testing the Table (without the entire SessionsDock - which has the UnlinkVideo button), we can just do the data update ourselves with:

table.model().items =  multiview_min_session_labels.sessions[0]

Also! I would like a test that all the Camcorders in the RecordingSession are displayed (regardless of linked Video), but only linked Videos have a path displayed - otherwise just an empty string for the Video.








Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lint with black test/gui/test_dataview/py or set-up black to formatOnSave in vscode

39 changes: 38 additions & 1 deletion tests/gui/widgets/test_docks.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@
import pytest
from sleap import Labels, Video
from sleap.gui.app import MainWindow
from sleap.gui.commands import OpenSkeleton
from sleap.gui.commands import OpenSkeleton, UnlinkVideo
from sleap.gui.widgets.docks import (
InstancesDock,
SuggestionsDock,
VideosDock,
SkeletonDock,
SessionsDock,
)


Expand Down Expand Up @@ -107,3 +108,39 @@ def test_instances_dock(qtbot):
assert dock.name == "Instances"
assert dock.main_window is main_window
assert dock.wgt_layout is dock.widget().layout()

def test_sessions_dock(qtbot, multiview_min_session_labels):
main_window = MainWindow()
dock = SessionsDock(main_window)

# Testing if dock object loads corretly
assert dock.name == "Sessions"
assert dock.main_window is main_window
assert dock.wgt_layout is dock.widget().layout()

# loading label file
main_window.commands.loadLabelsObject(multiview_min_session_labels)


# Testing if cameras_table is loaded correctly
camcorders = multiview_min_session_labels.sessions[0].linked_cameras

for i in range(8):
main_window.sessions_dock.camera_table.selectRow(i)
assert main_window.sessions_dock.camera_table.getSelectedRowItem() == camcorders[i]
assert main_window.sessions_dock.camera_table.model().data(main_window.sessions_dock.camera_table.currentIndex()) == camcorders[i].name

# Testing if unlink video command works for camera table
idxs_to_remove = [0, 4, 7]
to_remove_camcorders = [cam for i, cam in enumerate(camcorders) if i in idxs_to_remove]
leftover_camcorder = [cam for i, cam in enumerate(camcorders) if i not in idxs_to_remove]

for cam in to_remove_camcorders:
main_window.state["selected_camera_table"] = cam
main_window._buttons["unlink video"].click()


for i in range(5):
main_window.sessions_dock.camera_table.selectRow(i)
assert main_window.sessions_dock.camera_table.getSelectedRowItem() == leftover_camcorder[i]
assert main_window.sessions_dock.camera_table.model().data(main_window.sessions_dock.camera_table.currentIndex()) == leftover_camcorder[i].name
Copy link

@coderabbitai coderabbitai bot Feb 14, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The tests for SessionsDock do not verify the GUI's response to the unlink video action, such as checking if the UI correctly disables/enables buttons based on the state. Ensure tests cover the full range of expected GUI behavior to provide comprehensive coverage.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed on adding a check that the unlink video button is properly disabled.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@roomrys: To add a check for the "unlink video" button being properly disabled, you'll want to extend the test_sessions_dock function with assertions that verify the button's enabled state at key points in your test flow. Here's a general approach:

  1. After loading the label file and before performing any actions, assert that the "unlink video" button is initially disabled, as no camcorder is selected yet.
  2. After selecting a camcorder (e.g., main_window.sessions_dock.camera_table.selectRow(i)), assert that the "unlink video" button becomes enabled.
  3. After unlinking a video from a camcorder, assert that the button is disabled again if no other camcorder is selected or if the selected camcorder does not have a linked video.

You can access the button's enabled state using a property or method specific to the GUI framework you're using. For example, if using PyQt or PySide, you might check the button's isEnabled() method. Here's a simplified example:

# Example using PyQt/PySide, adjust according to your actual GUI framework and button access method
assert not main_window._buttons["unlink video"].isEnabled(), "Button should be disabled initially"

# After selecting a camcorder
main_window.sessions_dock.camera_table.selectRow(i)
assert main_window._buttons["unlink video"].isEnabled(), "Button should be enabled after selecting a camcorder"

# After unlinking a video
main_window._buttons["unlink video"].click()
assert not main_window._buttons["unlink video"].isEnabled(), "Button should be disabled after unlinking"

Make sure to adjust the example to fit your application's structure and the way you access UI elements. This will ensure your tests accurately reflect the intended user interactions and UI behavior.

Loading