Skip to content

Commit 59f4353

Browse files
authored
Merge pull request #143 from KumarLabJax/select-all
add select all feature
2 parents cec0ea0 + 543e18c commit 59f4353

File tree

7 files changed

+78
-15
lines changed

7 files changed

+78
-15
lines changed

src/jabs/behavior_search/behavior_search_util.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,7 +193,6 @@ def _gen_contig_true_intervals(
193193

194194
match pred_query.search_kind:
195195
case PredictionSearchKind.POSITIVE_PREDICTION:
196-
print("Searching for positive predictions...")
197196
crit_mask = animal_predictions == TrackLabels.Label.BEHAVIOR.value
198197
case PredictionSearchKind.NEGATIVE_PREDICTION:
199198
crit_mask = animal_predictions == TrackLabels.Label.NOT_BEHAVIOR.value

src/jabs/resources/docs/user_guide/user_guide.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -360,6 +360,10 @@ While not in select mode:
360360

361361
- z, x, c: enter select mode
362362

363+
In all modes:
364+
365+
- ctrl+a (windows) / cmd+a (Mac): select all frames for the current timeline
366+
363367
### Other
364368

365369
Switching subject:

src/jabs/ui/central_widget.py

Lines changed: 29 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -423,26 +423,52 @@ def _start_selection(self, pressed):
423423
if pressed:
424424
self._controls.enable_label_buttons()
425425
self._selection_start = self._player_widget.current_frame()
426+
self._selection_end = None
426427
self._stacked_timeline.start_selection(self._selection_start)
427428
else:
428429
self._controls.disable_label_buttons()
429430
self._stacked_timeline.clear_selection()
430431

432+
def select_all(self):
433+
"""Select all frames in the current video for the current identity and behavior."""
434+
if not self._controls.select_button_is_checked and self._controls.select_button_enabled:
435+
self._controls.toggle_select_button()
436+
437+
if self._controls.select_button_enabled:
438+
num_frames = self._player_widget.num_frames()
439+
if num_frames > 0:
440+
self._controls.enable_label_buttons()
441+
self._selection_start = 0
442+
self._selection_end = num_frames - 1
443+
self._stacked_timeline.start_selection(self._selection_start, self._selection_end)
444+
445+
@property
446+
def _curr_selection_end(self):
447+
"""Get the end of the current selection.
448+
449+
If no selection end is set, return the current frame index.
450+
"""
451+
return (
452+
self._selection_end
453+
if self._selection_end is not None
454+
else self._player_widget.current_frame()
455+
)
456+
431457
def _label_behavior(self):
432458
"""Apply behavior label to currently selected range of frames"""
433-
start, end = sorted([self._selection_start, self._player_widget.current_frame()])
459+
start, end = sorted([self._selection_start, self._curr_selection_end])
434460
self._get_label_track().label_behavior(start, end)
435461
self._label_button_common()
436462

437463
def _label_not_behavior(self):
438464
"""apply _not_ behavior label to currently selected range of frames"""
439-
start, end = sorted([self._selection_start, self._player_widget.current_frame()])
465+
start, end = sorted([self._selection_start, self._curr_selection_end])
440466
self._get_label_track().label_not_behavior(start, end)
441467
self._label_button_common()
442468

443469
def _clear_behavior_label(self):
444470
"""clear all behavior/not behavior labels from current selection"""
445-
label_range = sorted([self._selection_start, self._player_widget.current_frame()])
471+
label_range = sorted([self._selection_start, self._curr_selection_end])
446472
self._get_label_track().clear_labels(*label_range)
447473
self._label_button_common()
448474

src/jabs/ui/main_window.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -229,7 +229,7 @@ def __init__(self, app_name: str, app_name_long: str, *args, **kwargs):
229229

230230
# add behavior search
231231
self.behavior_search = QtGui.QAction("Search Behaviors", self)
232-
self.behavior_search.setShortcut(QtGui.QKeySequence.Find)
232+
self.behavior_search.setShortcut(QtGui.QKeySequence.StandardKey.Find)
233233
self.behavior_search.setStatusTip("Search for behaviors")
234234
self.behavior_search.setEnabled(False)
235235
self.behavior_search.triggered.connect(self._search_behaviors)
@@ -275,6 +275,12 @@ def __init__(self, app_name: str, app_name_long: str, *args, **kwargs):
275275
self.enable_segmentation_features.triggered.connect(self._toggle_segmentation_features)
276276
feature_menu.addAction(self.enable_segmentation_features)
277277

278+
# select all action
279+
select_all_action = QtGui.QAction(self)
280+
select_all_action.setShortcut(QtGui.QKeySequence.StandardKey.SelectAll)
281+
select_all_action.triggered.connect(self._handle_select_all)
282+
self.addAction(select_all_action)
283+
278284
# playlist widget added to dock on left side of main window
279285
self.video_list = VideoListDockWidget(self)
280286
self.addDockWidget(Qt.DockWidgetArea.LeftDockWidgetArea, self.video_list)
@@ -684,3 +690,7 @@ def _on_label_overlay_mode_changed(self):
684690
self._central_widget.label_overlay_mode = PlayerWidget.LabelOverlay.LABEL
685691
elif self._label_overlay_preds.isChecked():
686692
self._central_widget.label_overlay_mode = PlayerWidget.LabelOverlay.PREDICTION
693+
694+
def _handle_select_all(self):
695+
"""Handle the Select All event"""
696+
self._central_widget.select_all()

src/jabs/ui/stacked_timeline_widget/label_overview_widget/label_overview_widget.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,13 +146,15 @@ def reset(self) -> None:
146146
self._label_widget.reset()
147147
self._num_frames = 0
148148

149-
def start_selection(self, starting_frame: int) -> None:
149+
def start_selection(self, starting_frame: int, ending_frame: int | None = None) -> None:
150150
"""Start a selection from the given frame to the current frame.
151151
152152
Args:
153153
starting_frame: The frame index where the selection begins.
154+
ending_frame: Optional; the frame index where the selection ends. If None,
155+
selection continues to current frame.
154156
"""
155-
self._label_widget.start_selection(starting_frame)
157+
self._label_widget.start_selection(starting_frame, ending_frame)
156158

157159
def clear_selection(self) -> None:
158160
"""Clear the current selection.

src/jabs/ui/stacked_timeline_widget/label_overview_widget/manual_label_widget.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ def __init__(self, *args, **kwargs) -> None:
7070
# current position
7171
self._current_frame = 0
7272
self._selection_start = None
73+
self._selection_end = None
7374

7475
# information about the video needed to properly render widget
7576
self._num_frames = 0
@@ -230,7 +231,13 @@ def _draw_selection_overlay(self, painter: QPainter) -> None:
230231
painter.setBrush(self._selection_brush)
231232

232233
# figure out the start and width of the selection rectangle
233-
if self._selection_start < self._current_frame:
234+
if self._selection_end is not None:
235+
# we have an explicit end frame for the selection
236+
selection_start = max(self._selection_start - start, 0)
237+
selection_width = (
238+
min(end, self._selection_end) - max(start, self._selection_start) + 1
239+
) * self._frame_width
240+
elif self._selection_start < self._current_frame:
234241
# normal selection, start is lower than current frame
235242
selection_start = max(self._selection_start - start, 0)
236243
selection_width = (
@@ -328,22 +335,27 @@ def set_framerate(self, fps: int) -> None:
328335
"""
329336
self._framerate = fps
330337

331-
def start_selection(self, start_frame: int) -> None:
338+
def start_selection(self, start_frame: int, end_frame: int | None = None) -> None:
332339
"""Begin highlighting a selection range on the label bar.
333340
334-
Sets the starting frame for the selection overlay, which will extend to the current frame.
341+
Sets the starting frame for the selection overlay, which will extend to the current frame
342+
unless an end frame is specified.
335343
336344
Args:
337345
start_frame: The frame index where the selection begins.
346+
end_frame: Optional; the frame index where the selection ends. If not provided,
347+
the selection will extend to the current frame.
338348
"""
339349
self._selection_start = start_frame
350+
self._selection_end = end_frame
340351

341352
def clear_selection(self) -> None:
342353
"""Stop highlighting the selection range.
343354
344355
Clears the selection overlay from the label bar.
345356
"""
346357
self._selection_start = None
358+
self._selection_end = None
347359

348360
def reset(self) -> None:
349361
"""Reset the widget to its initial state.
@@ -353,5 +365,6 @@ def reset(self) -> None:
353365
self._labels = None
354366
self._identity_mask = None
355367
self._selection_start = None
368+
self._selection_end = None
356369
self._num_frames = 0
357370
self.update()

src/jabs/ui/stacked_timeline_widget/stacked_timeline_widget.py

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,13 @@ def __init__(self, *args, **kwargs) -> None:
4848

4949
self._active_identity_index = None
5050
self._selection_starting_frame = None
51+
self._selection_ending_frame = None
5152
self._view_mode = self.ViewMode.LABELS_AND_PREDICTIONS
5253
self._identity_mode = self.IdentityMode.ACTIVE
5354
self._num_identities = 0
5455
self._num_frames = 0
5556
self._framerate = 0
56-
self._label_overview_widgets = []
57+
self._label_overview_widgets: list[LabelOverviewWidget] = []
5758
self._prediction_overview_widgets = []
5859
self._identity_frames = []
5960
self._frame_labels = FrameLabelsWidget(self)
@@ -230,7 +231,8 @@ def active_identity_index(self, value: int) -> None:
230231
# Transfer selection to new active widget if selection is active
231232
if selection_frame is not None:
232233
self._label_overview_widgets[self._active_identity_index].start_selection(
233-
selection_frame
234+
selection_frame,
235+
self._selection_ending_frame,
234236
)
235237

236238
# Set active state or frame border depending on display mode
@@ -370,18 +372,24 @@ def set_predictions(
370372
widget.framerate = self.framerate
371373
widget.set_labels(predictions_list[i], probabilities_list[i])
372374

373-
def start_selection(self, starting_frame: int) -> None:
374-
"""Start a selection from the given frame on the active identity's widget.
375+
def start_selection(self, starting_frame: int, ending_frame: int | None = None) -> None:
376+
"""Start a selection from the given frame(s) on the active identity's widget.
375377
376378
Records the starting frame and initiates selection mode on the currently active identity.
379+
If `ending_frame` is provided, it sets the selection range to include that frame as well.
380+
If `ending_frame` is None, the selection continues to the current frame.
377381
378382
Args:
379383
starting_frame: The frame index where the selection begins.
384+
ending_frame: Optional; the frame index where the selection ends. If None,
385+
selection continues to current frame.
380386
"""
381387
if self._active_identity_index is not None:
382388
self._selection_starting_frame = starting_frame
389+
self._selection_ending_frame = ending_frame
383390
self._label_overview_widgets[self._active_identity_index].start_selection(
384-
starting_frame
391+
starting_frame,
392+
ending_frame,
385393
)
386394
self._label_overview_widgets[self._active_identity_index].update()
387395

@@ -393,6 +401,7 @@ def clear_selection(self) -> None:
393401
if self._active_identity_index is not None:
394402
self._label_overview_widgets[self._active_identity_index].clear_selection()
395403
self._selection_starting_frame = None
404+
self._selection_ending_frame = None
396405
self._label_overview_widgets[self._active_identity_index].update_labels()
397406

398407
def reset(self) -> None:

0 commit comments

Comments
 (0)