-
Notifications
You must be signed in to change notification settings - Fork 102
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
Add CamerasTable
to SessionsDock
#1671
Conversation
WalkthroughThe recent updates focus on enhancing the user interface and functionality of a GUI application, specifically by improving session management and camera-video linkage. New features include the ability to unlink videos from cameras, better handling of camera selections in the GUI, and updates to data views and widgets to support these changes. The modifications span across app initialization, command actions, and GUI components, aiming to provide a more intuitive and flexible user experience. Changes
Poem
Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media? TipsChatThere are 3 ways to chat with CodeRabbit:
Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (invoked as PR comments)
Additionally, you can add CodeRabbit Configration File (
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is an error in the on_data_update
call, which is likely why the cameras table was not updating when I load a project with a RecordingSession
.
sleap/gui/app.py
Outdated
@@ -1236,6 +1236,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.model().items = self.labels.videos |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we may need to specify which table in the list of tables here, perhaps self.sessions_dock.camera_table
(see what is in self.sessions_dock.table
below).
sleap/sleap/gui/widgets/docks.py
Line 70 in 7f74a8b
self.table = self.create_tables() |
sleap/sleap/gui/widgets/docks.py
Lines 625 to 636 in 7f74a8b
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", | |
model=self.camera_model, | |
) | |
return [self.camera_table] |
I received the following error message with the current implementation:
Happy SLEAPing! :)
Restoring GUI state...
Traceback (most recent call last):
File "d:\social-leap-estimates-animal-poses\source\sleap\sleap\gui\commands.py", line 290, in openProject
self.execute(OpenProject, filename=filename, first_open=first_open)
File "d:\social-leap-estimates-animal-poses\source\sleap\sleap\gui\commands.py", line 244, in execute
command().execute(context=self, params=kwargs)
File "d:\social-leap-estimates-animal-poses\source\sleap\sleap\gui\commands.py", line 140, in execute
self.do_with_signal(context, params)
File "d:\social-leap-estimates-animal-poses\source\sleap\sleap\gui\commands.py", line 164, in do_with_signal
cls.do_action(context, params)
File "d:\social-leap-estimates-animal-poses\source\sleap\sleap\gui\commands.py", line 759, in do_action
new_window.commands.loadProjectFile(filename)
File "d:\social-leap-estimates-animal-poses\source\sleap\sleap\gui\commands.py", line 275, in loadProjectFile
self.execute(LoadProjectFile, filename=filename)
File "d:\social-leap-estimates-animal-poses\source\sleap\sleap\gui\commands.py", line 244, in execute
command().execute(context=self, params=kwargs)
File "d:\social-leap-estimates-animal-poses\source\sleap\sleap\gui\commands.py", line 140, in execute
self.do_with_signal(context, params)
File "d:\social-leap-estimates-animal-poses\source\sleap\sleap\gui\commands.py", line 164, in do_with_signal
cls.do_action(context, params)
File "d:\social-leap-estimates-animal-poses\source\sleap\sleap\gui\commands.py", line 710, in do_action
context.app.on_data_update([UpdateTopic.project, UpdateTopic.all])
File "d:\social-leap-estimates-animal-poses\source\sleap\sleap\gui\app.py", line 1240, in on_data_update
self.sessions_dock.table.model().items = self.labels.videos
AttributeError: 'list' object has no attribute 'model'
sleap/gui/commands.py
Outdated
def do_action(context: CommandContext, params: dict): | ||
video = params.get("video", None) | ||
cam = params.get("Camcorder", None) | ||
recording_session = params.get("RecordingSession", None) | ||
|
||
if video is not None and recording_session is not None \ | ||
and cam is not None: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we should just handle this by disabling the Unlink Button if the GUI is not in the correct state.
Something similar to what the "Show Video" button uses:
Line 1126 in 60a441f
self._buttons["show video"].setEnabled(has_selected_video) |
Note that any item that is selected in a table will by default be set in the GuiState by prepending a "selected_" to the TableView.row_name
:
Lines 278 to 291 in 60a441f
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_" |
In this case, you've used a row_name="camera"
:
sleap/sleap/gui/widgets/docks.py
Lines 630 to 634 in 7f74a8b
self.camera_table = GenericTableView( | |
state=main_window.state, | |
row_name="camera", | |
model=self.camera_model, | |
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
To update the table, we need to add some update topics to the UnlinkVideo
class.
sleap/gui/commands.py
Outdated
class UnlinkVideo(EditCommand): | ||
@staticmethod |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We are missing the callback topics
here.
class UnlinkVideo(EditCommand): | |
@staticmethod | |
class UnlinkVideo(EditCommand): | |
topics = [UpdateTopic.sessions] | |
@staticmethod |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Review Status
Actionable comments generated: 10
Configuration used: CodeRabbit UI
Files selected for processing (6)
- sleap/gui/app.py (3 hunks)
- sleap/gui/commands.py (3 hunks)
- sleap/gui/dataviews.py (2 hunks)
- sleap/gui/widgets/docks.py (3 hunks)
- tests/gui/test_dataviews.py (2 hunks)
- tests/gui/widgets/test_docks.py (2 hunks)
Additional comments: 4
sleap/gui/widgets/docks.py (1)
- 578-579: The
SessionsDock
constructor initializesmodel_type
with a list containingself.camera_model_type
, butDockWidget
's constructor seems to expect either a single model type orNone
. Verify if passing a list is intended and correctly handled in the superclass.sleap/gui/dataviews.py (1)
- 669-670: The method
object_to_items
usesobj.linked_cameras
, which implies thatRecordingSession
has alinked_cameras
attribute. Ensure that this attribute exists and correctly returns a list ofCamcorder
objects paired with their respectiveVideo
objects. Iflinked_cameras
is a method or property that computes this pairing dynamically, its implementation needs to be efficient to avoid performance issues in the GUI.sleap/gui/app.py (1)
- 1097-1098: The addition of
has_selected_camcorder
checks the presence of a selected camera in the table and uses this to enable the "Unlink Video" button. Ensure that the stateselected_camera_table
is correctly updated elsewhere in the code to reflect the current selection status accurately.sleap/gui/commands.py (1)
- 3917-3920: The method
camcorder.get_video(recording_session)
and subsequent removal of the video from the session (recording_session.remove_video(video)
) are not standard operations in the provided context. Ensure these methods are correctly implemented in their respective classes and handle cases where the video might not be found or already unlinked.
tests/gui/test_dataviews.py
Outdated
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 |
There was a problem hiding this comment.
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.
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 |
tests/gui/test_dataviews.py
Outdated
assert table.getSelectedRowItem() == camcorders[i] | ||
assert table.model().data(table.currentIndex()) == removed_camcorder[i].name |
There was a problem hiding this comment.
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.
tests/gui/widgets/test_docks.py
Outdated
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 |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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:
- 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.
- After selecting a camcorder (e.g.,
main_window.sessions_dock.camera_table.selectRow(i)
), assert that the "unlink video" button becomes enabled. - 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.
sleap/gui/widgets/docks.py
Outdated
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} |
There was a problem hiding this comment.
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.
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 |
There was a problem hiding this comment.
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]]:
There was a problem hiding this comment.
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.
sleap/gui/widgets/docks.py
Outdated
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} |
There was a problem hiding this comment.
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.
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 |
There was a problem hiding this comment.
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]]:
There was a problem hiding this comment.
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!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Table works great. We just have one more piece to do which would require using the GuiState
for the table instead of directly using the list. To accomplish this, we would also need to set the GuiState
for "session"
when loading the Labels
object and when loading a Video
that is part of a RecordingSession
.
tests/gui/test_dataviews.py
Outdated
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 |
There was a problem hiding this comment.
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 Camcorder
s in the RecordingSession
are displayed (regardless of linked Video
), but only linked Video
s have a path displayed - otherwise just an empty string for the Video
.
tests/gui/test_dataviews.py
Outdated
|
||
|
||
|
||
|
||
|
||
|
||
|
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Review Status
Actionable comments generated: 1
Configuration used: CodeRabbit UI
Files selected for processing (7)
- sleap/gui/app.py (4 hunks)
- sleap/gui/commands.py (4 hunks)
- sleap/gui/dataviews.py (2 hunks)
- sleap/gui/widgets/docks.py (3 hunks)
- sleap/io/cameras.py (2 hunks)
- tests/gui/test_dataviews.py (2 hunks)
- tests/gui/widgets/test_docks.py (2 hunks)
Additional comments: 20
tests/gui/test_dataviews.py (1)
- 90-141: The test function
test_camera_table
is well-structured and covers essential aspects of theCamerasTable
functionality. It tests the presentation of camcorders and their linked videos, as well as the update mechanism when camcorders are removed. However, consider adding more detailed comments within the test to explain the purpose of each assertion block, especially for future maintainability and readability.tests/gui/widgets/test_docks.py (1)
- 112-146: The test function
test_sessions_dock
effectively validates theSessionsDock
functionality, including the loading of theCamerasTable
and the operation of the "Unlink Video" button. It's comprehensive and covers key aspects of theSessionsDock
. To enhance readability and maintainability, consider adding comments to describe the purpose and expected outcome of each major test block, especially for complex interactions like unlinking videos.sleap/gui/widgets/docks.py (5)
- 34-34: The import of
CamerasTableModel
is correctly added to support the newSessionsDock
functionality.- 578-579: The modifications in the
SessionsDock
constructor, including the addition of thecamera_model_type
attribute and the adjustment ofmodel_type
to accept a list, are appropriate for supporting multiple models within the dock.- 617-623: The implementation of the
create_models
method inSessionsDock
, returning a dictionary of models, aligns with the discussed approach to accommodate multiple models. Ensure that the type hinting and documentation are updated to reflect this flexibility and that other parts of the application that interact with these models are compatible with this approach.- 625-636: The implementation of the
create_tables
method inSessionsDock
, returning a dictionary containing thecamera_table
, is consistent with the approach to handle multiple models and tables. This flexibility enhances the dock's functionality and allows for more complex interactions.- 638-646: The
create_video_unlink_button
method inSessionsDock
correctly adds a button for unlinking videos from cameras. This functionality is crucial for managing video-camera assignments within the GUI application.sleap/gui/dataviews.py (5)
- 33-33: The import statement for
Camcorder
andRecordingSession
is correctly added and necessary for theCamerasTableModel
class.- 658-664: The class definition and docstring for
CamerasTableModel
are clear and informative.- 666-666: The
properties
definition inCamerasTableModel
is correctly set up for the intended table columns.- 668-669: The
object_to_items
method inCamerasTableModel
is correctly implemented for listing cameras in aRecordingSession
.- 671-674: The
item_to_data
method inCamerasTableModel
is correctly implemented for convertingCamcorder
items to a dictionary format suitable for the table model. It appropriately handles cases where a video might not be linked to a camera.sleap/io/cameras.py (2)
- 45-45: Changing the log level from
warning
todebug
in theget_video
method of theCamcorder
class is appropriate for cases where a session not being found is expected behavior.- 465-465: Adjusting the log level from
warning
todebug
in theget_video
method of theRecordingSession
class is suitable for situations where a camcorder not being linked to a video is part of normal operation.sleap/gui/app.py (4)
- 148-148: Initialization of
session
state variable toNone
is consistent with the pattern of state initialization in this section. This change aligns with the PR objectives related to managing recording sessions.- 1098-1098: The variable
has_selected_camcorder
is derived from the state ofselected_camera
. Please verify the naming convention for consistency across the application, as the PR mentions managing video-camera assignments but usesselected_camcorder
here.- 1153-1153: Enabling the "unlink video" button based on the selection of a camcorder aligns with the PR objectives. Ensure that the naming inconsistency between
selected_camera
andselected_camcorder
is addressed for clarity.- 1242-1246: Updating the
SessionsDock
to use thesession
state variable aligns with the PR objectives. The TODO comment suggests a future change to use "selected_session" instead, which should be addressed in subsequent updates.sleap/gui/commands.py (2)
- 85-85: Adding a new
UpdateTopic
enum value forsessions
is appropriate for signaling updates related to session changes. This ensures that any UI components listening for session updates can react accordingly.- 659-662: The method
unlink_video_from_camera
in theCommandContext
class provides a clear and concise way to execute theUnlinkVideo
command. This method enhances the modularity of the command execution process.
sleap/gui/commands.py
Outdated
class UnlinkVideo(EditCommand): | ||
topics = [UpdateTopic.sessions] | ||
|
||
@staticmethod | ||
def do_action(context: CommandContext, params: dict): | ||
camcorder = context.state["selected_camera"] | ||
# TODO(LM): replace with state["selected_session"] | ||
recording_session = context.state["session"] | ||
|
||
video = camcorder.get_video(recording_session) | ||
|
||
if video is not None and recording_session is not None: | ||
recording_session.remove_video(video) | ||
|
||
# Reset the selected camera | ||
context.state["selected_camera"] = None |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The UnlinkVideo
class correctly implements the do_action
method to unlink a video from a camera within a recording session. However, there are a few areas for improvement:
- The method assumes that
context.state["selected_camera"]
andcontext.state["session"]
are always set and valid. It would be safer to add checks to ensure these values are notNone
before proceeding with the unlink operation. - After unlinking the video, it resets
context.state["selected_camera"]
toNone
. This action might not be intuitive for the user, especially if they expect the selected camera to remain selected after the operation. Consider whether this behavior aligns with the user interface expectations. - There's a TODO comment regarding replacing
context.state["session"]
withstate["selected_session"]
. It's important to address this TODO to ensure consistency in how sessions are referenced throughout the application.
@staticmethod
def do_action(context: CommandContext, params: dict):
+ if "selected_camera" not in context.state or "session" not in context.state:
+ logger.error("Selected camera or session not set.")
+ return
camcorder = context.state["selected_camera"]
recording_session = context.state["session"]
+ if camcorder is None or recording_session is None:
+ logger.error("Selected camera or session is None.")
+ return
video = camcorder.get_video(recording_session)
if video is not None:
recording_session.remove_video(video)
- # Reset the selected camera
- context.state["selected_camera"] = None
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.
class UnlinkVideo(EditCommand): | |
topics = [UpdateTopic.sessions] | |
@staticmethod | |
def do_action(context: CommandContext, params: dict): | |
camcorder = context.state["selected_camera"] | |
# TODO(LM): replace with state["selected_session"] | |
recording_session = context.state["session"] | |
video = camcorder.get_video(recording_session) | |
if video is not None and recording_session is not None: | |
recording_session.remove_video(video) | |
# Reset the selected camera | |
context.state["selected_camera"] = None | |
class UnlinkVideo(EditCommand): | |
topics = [UpdateTopic.sessions] | |
@staticmethod | |
def do_action(context: CommandContext, params: dict): | |
if "selected_camera" not in context.state or "session" not in context.state: | |
logger.error("Selected camera or session not set.") | |
return | |
camcorder = context.state["selected_camera"] | |
recording_session = context.state["session"] | |
if camcorder is None or recording_session is None: | |
logger.error("Selected camera or session is None.") | |
return | |
video = camcorder.get_video(recording_session) | |
if video is not None: | |
recording_session.remove_video(video) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Review Status
Actionable comments generated: 1
Configuration used: CodeRabbit UI
Files selected for processing (2)
- tests/gui/test_dataviews.py (2 hunks)
- tests/gui/widgets/test_docks.py (2 hunks)
Files skipped from review as they are similar to previous changes (1)
- tests/gui/test_dataviews.py
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Review Status
Actionable comments generated: 1
Configuration used: CodeRabbit UI
Files selected for processing (7)
- sleap/gui/app.py (5 hunks)
- sleap/gui/commands.py (3 hunks)
- sleap/gui/dataviews.py (2 hunks)
- sleap/gui/widgets/docks.py (5 hunks)
- sleap/io/cameras.py (2 hunks)
- tests/gui/test_dataviews.py (1 hunks)
- tests/gui/widgets/test_docks.py (2 hunks)
Files skipped from review as they are similar to previous changes (6)
- sleap/gui/app.py
- sleap/gui/commands.py
- sleap/gui/dataviews.py
- sleap/gui/widgets/docks.py
- sleap/io/cameras.py
- tests/gui/test_dataviews.py
def test_sessions_dock_cameras_table(qtbot, multiview_min_session_labels): | ||
labels = multiview_min_session_labels | ||
session = labels.sessions[0] | ||
camcorders = session.camera_cluster.cameras | ||
main_window = MainWindow(labels=labels) | ||
assert main_window.state["session"] == session | ||
|
||
dock = main_window.sessions_dock | ||
table = dock.camera_table | ||
|
||
# Testing if cameras_table is loaded correctly | ||
|
||
# Test if all comcorders are presented in the correct row | ||
for i, cam in enumerate(camcorders): | ||
table.selectRow(i) | ||
|
||
# Check first column | ||
assert table.getSelectedRowItem() == cam | ||
assert table.model().data(table.currentIndex()) == cam.name | ||
|
||
# Check second column | ||
index = table.model().index(i, 1) | ||
linked_video_filename = cam.get_video(session).filename | ||
assert table.model().data(index) == linked_video_filename | ||
|
||
# Test if a comcorder change is reflected | ||
idxs_to_remove = [1, 2, 7] | ||
for idx in idxs_to_remove: | ||
main_window.state["selected_camera"] = camcorders[idx] | ||
main_window._buttons["unlink video"].click() | ||
|
||
for i, cam in enumerate(camcorders): | ||
table.selectRow(i) | ||
|
||
# Check first column | ||
assert table.getSelectedRowItem() == camcorders[i] | ||
assert table.model().data(table.currentIndex()) == camcorders[i].name | ||
|
||
# Check second column | ||
index = table.model().index(i, 1) | ||
linked_video = camcorders[i].get_video(session) | ||
if i in idxs_to_remove: | ||
assert table.model().data(index) == "" | ||
else: | ||
linked_video_filename = linked_video.filename | ||
assert table.model().data(index) == linked_video_filename |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test function test_sessions_dock_cameras_table
is well-structured and covers important aspects of the SessionsDock
cameras table functionality. However, there are a few areas where improvements can be made:
-
Typographical Error: There's a minor typographical error in the comments. The word "comcorders" should be corrected to "camcorders" for clarity and correctness. This occurs in lines 134 and 147.
-
Test Coverage for Unlink Video: The test covers the scenario where videos are unlinked from camcorders, but it might be beneficial to also verify that the
UnlinkVideo
command is correctly updating the underlying data model. This could involve checking that thesession
object reflects the changes made by theUnlinkVideo
command. -
Magic Numbers: The test uses magic numbers (e.g.,
idxs_to_remove = [1, 2, 7]
) without explanation. It would improve readability and maintainability to either document why these specific indices are chosen or to derive these indices programmatically based on the test setup. -
Error Handling: Consider adding tests for error scenarios, such as attempting to unlink a video from a camcorder that doesn't have a linked video. This would ensure robustness in handling edge cases.
- # Test if all comcorders are presented in the correct row
+ # Test if all camcorders are presented in the correct row
Consider enhancing the test to verify that the UnlinkVideo
command updates the underlying data model as expected.
Document the rationale behind the choice of indices in idxs_to_remove
or derive them programmatically to avoid magic numbers.
Would you like assistance in adding tests for error scenarios, such as handling unlink operations for camcorders without linked videos?
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.
def test_sessions_dock_cameras_table(qtbot, multiview_min_session_labels): | |
labels = multiview_min_session_labels | |
session = labels.sessions[0] | |
camcorders = session.camera_cluster.cameras | |
main_window = MainWindow(labels=labels) | |
assert main_window.state["session"] == session | |
dock = main_window.sessions_dock | |
table = dock.camera_table | |
# Testing if cameras_table is loaded correctly | |
# Test if all comcorders are presented in the correct row | |
for i, cam in enumerate(camcorders): | |
table.selectRow(i) | |
# Check first column | |
assert table.getSelectedRowItem() == cam | |
assert table.model().data(table.currentIndex()) == cam.name | |
# Check second column | |
index = table.model().index(i, 1) | |
linked_video_filename = cam.get_video(session).filename | |
assert table.model().data(index) == linked_video_filename | |
# Test if a comcorder change is reflected | |
idxs_to_remove = [1, 2, 7] | |
for idx in idxs_to_remove: | |
main_window.state["selected_camera"] = camcorders[idx] | |
main_window._buttons["unlink video"].click() | |
for i, cam in enumerate(camcorders): | |
table.selectRow(i) | |
# Check first column | |
assert table.getSelectedRowItem() == camcorders[i] | |
assert table.model().data(table.currentIndex()) == camcorders[i].name | |
# Check second column | |
index = table.model().index(i, 1) | |
linked_video = camcorders[i].get_video(session) | |
if i in idxs_to_remove: | |
assert table.model().data(index) == "" | |
else: | |
linked_video_filename = linked_video.filename | |
assert table.model().data(index) == linked_video_filename | |
def test_sessions_dock_cameras_table(qtbot, multiview_min_session_labels): | |
labels = multiview_min_session_labels | |
session = labels.sessions[0] | |
camcorders = session.camera_cluster.cameras | |
main_window = MainWindow(labels=labels) | |
assert main_window.state["session"] == session | |
dock = main_window.sessions_dock | |
table = dock.camera_table | |
# Testing if cameras_table is loaded correctly | |
# Test if all camcorders are presented in the correct row | |
for i, cam in enumerate(camcorders): | |
table.selectRow(i) | |
# Check first column | |
assert table.getSelectedRowItem() == cam | |
assert table.model().data(table.currentIndex()) == cam.name | |
# Check second column | |
index = table.model().index(i, 1) | |
linked_video_filename = cam.get_video(session).filename | |
assert table.model().data(index) == linked_video_filename | |
# Test if a comcorder change is reflected | |
idxs_to_remove = [1, 2, 7] | |
for idx in idxs_to_remove: | |
main_window.state["selected_camera"] = camcorders[idx] | |
main_window._buttons["unlink video"].click() | |
for i, cam in enumerate(camcorders): | |
table.selectRow(i) | |
# Check first column | |
assert table.getSelectedRowItem() == camcorders[i] | |
assert table.model().data(table.currentIndex()) == camcorders[i].name | |
# Check second column | |
index = table.model().index(i, 1) | |
linked_video = camcorders[i].get_video(session) | |
if i in idxs_to_remove: | |
assert table.model().data(index) == "" | |
else: | |
linked_video_filename = linked_video.filename | |
assert table.model().data(index) == linked_video_filename |
145a1d1
into
liezl/add-gui-elements-for-sessions
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Review Status
Actionable comments generated: 3
Configuration used: CodeRabbit UI
Files selected for processing (5)
- sleap/gui/app.py (5 hunks)
- sleap/gui/commands.py (3 hunks)
- sleap/gui/dataviews.py (2 hunks)
- sleap/gui/widgets/docks.py (5 hunks)
- tests/gui/widgets/test_docks.py (2 hunks)
Files skipped from review as they are similar to previous changes (4)
- sleap/gui/app.py
- sleap/gui/commands.py
- sleap/gui/dataviews.py
- sleap/gui/widgets/docks.py
Additional comments: 1
tests/gui/widgets/test_docks.py (1)
- 125-170: The test function
test_sessions_dock_cameras_table
covers important aspects of theSessionsDock
cameras table functionality. However, consider enhancing the test to verify that theUnlinkVideo
command updates the underlying data model as expected.Consider adding assertions to verify that the
session
object reflects the changes made by theUnlinkVideo
command.
|
||
# Testing if cameras_table is loaded correctly | ||
|
||
# Test if all comcorders are presented in the correct row |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's a typographical error in the comment: "comcorders" should be corrected to "camcorders" for clarity and correctness.
- # Test if all comcorders are presented in the correct row
+ # Test if all camcorders are presented in the correct row
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.
# Test if all comcorders are presented in the correct row | |
# Test if all camcorders are presented in the correct row |
linked_video_filename = cam.get_video(session).filename | ||
assert table.model().data(index) == linked_video_filename | ||
|
||
# Test if a comcorder change is reflected |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Another typographical error: "comcorder" should be corrected to "camcorder".
- # Test if a comcorder change is reflected
+ # Test if a camcorder change is reflected
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.
# Test if a comcorder change is reflected | |
# Test if a camcorder change is reflected |
assert table.model().data(index) == linked_video_filename | ||
|
||
# Test if a comcorder change is reflected | ||
idxs_to_remove = [1, 2, 7] |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The use of magic numbers in idxs_to_remove = [1, 2, 7]
should be documented or derived programmatically to improve readability and maintainability.
Consider documenting the rationale behind the choice of indices in idxs_to_remove
or deriving them programmatically to avoid magic numbers.
Description
The overall goal is to add
Camcorder
/Video
assignment table to theSessionsDock
that is linked to theSessionsTable
created above. ThisCamerasTable
will have two columns. One column will display theCamcorder.name
s of allCamcorder
s in theRecordingSession.cameras: List[Camcorder]
list. The second column will show whichVideo
s are assigned to the adjacentCamcorder.name
by displaying the returned value ofRecordingSession.get_video(Camcorder)
(or""
ifNone
).GenericTableModel
to create a new model calledCamerasTableModel
withproperties = ("camera", "video")
CamerasTableModel.object_to_items(self, obj: RecordingSession)
which returns the list ofCamcorder
s inobj.cameras: List[Camcorder]
CamerasTableModel.item_to_data(self, obj: RecordingSession, item: Camcorder)
which returns the following dictionary:{camera: item.name, video: obj.get_video(item)}
cameras_model_type = CamerasTableModel
attribute toSessionsDock
(similar toSkeletonDock.nodes_model_type
) and pass into themodel_type
list forsuper().__init__
SessionsDock.create_models
method (or append to it if it already exists) that sets aSessionsDock.cameras_model = SessionsDock.sessions_model_type(items=main_window.state["selected_session"], context=main_window.commands)
and returns a list of all models created (seeSkeletonDock.create_models
)SessionsDock.create_tables
method (or append to it if it already exists) that sets aSessionsDock.cameras_table = GenericTableView(state=..., row_name="camera", ...)
and returns a list of all tables created (seeSkeletonDock.create_tables
)CommandContext.unlink_video_from_camera
method to un-link aVideo
from aCamcorder
CommandContext.unlink_video_from_camera
methodCamcorder
for theCamerasTable
SessionsDock.lay_everything_out
method, add theSessionsDock.cameras_table
to theSessionsDock.wgt_layout: QVBoxLayout
(see theInstancesDock.lay_everything_out
).sessions
option toUpdateTopic: Enum
if not already thereMainWindow.on_data_update
add (or append to) theif _has_topic(UpdateTopic.sessions)
that updates the items to display in theCamerasTable
(seevideo_dock.table
update)GuiState
for the"session"
key to the firstRecordingSession
inlabels.sessions: List[RecordingSession]
when loading aLabels
project (similar to how we set the state for"video"
)Test the
CamerasTable
Tests for the
CamerasTable
will be added totests/gui/test_dataviews.py
. These tests will only test the functionality of the table as it's own entity.Camcorder
is returned when we select a specific row of the table (see existing test for inspiration)Camcorder
inlabels.sessions
, theCamcorder
in the table is updated as well (i.e. if we unlink aVideo
from aCamcorder
, the data in theVideo
cell should be empty)Test the
CamerasTable
as part of theSessionsDock
Tests for the
SessionsDock
will be added totests/gui/widgets/test_docks.py
.GuiState
(see existing test)Fig 1: Depiction of Cameras Table layout.
Types of changes
Does this address any currently open issues?
[list open issues here]
Outside contributors checklist
Thank you for contributing to SLEAP!
❤️
Summary by CodeRabbit
CamerasTableModel
for managing camera and video pairs in recording sessions.SessionsDock
with new methods and attributes to support camera management and video unlinking.SessionsDock
functionality, including camera table presentation and video unlinking.