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

Fix Frame Selection and Frame Count in GUI (Shift + Drag and Shift + Double Click) #2078

Open
wants to merge 24 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
19e432a
take away + 1 to frame count display
ericleonardis Dec 20, 2024
2ca0762
clamp frame selection slider to min and max frames and add doc strings
ericleonardis Dec 20, 2024
85cdb5d
slider width plus 6 fixes the error
ericleonardis Dec 20, 2024
8adfc39
extensive test for different slider ranges and widths
ericleonardis Dec 20, 2024
fce0522
do not subtract the handle width
ericleonardis Dec 20, 2024
7c4be6d
simplify correction
ericleonardis Dec 20, 2024
0ae9a45
do not subtract handle width from slider_width
ericleonardis Dec 20, 2024
de3a73c
test the toVal ranges are exhibiting expected behavior
ericleonardis Dec 20, 2024
a06c954
add test to ensure slider width is not using handle width and is retu…
ericleonardis Dec 21, 2024
fb35ff2
black format
ericleonardis Dec 21, 2024
ad57162
set negatives to 1
ericleonardis Jan 2, 2025
fb403d9
improved error handling for invalid inputs
ericleonardis Jan 2, 2025
7c8130f
black formatting
ericleonardis Jan 2, 2025
48c8b0d
black formatting and comprehensive test for toval
ericleonardis Jan 2, 2025
f296aa9
Merge branch 'eric/shift-drag-fix' of https://github.com/talmolab/sle…
ericleonardis Jan 2, 2025
7a7f933
clean up old tests
ericleonardis Jan 2, 2025
3fb79f0
get rid of setting 0 to 1
ericleonardis Jan 2, 2025
2c9544e
get rid of center logic
ericleonardis Jan 2, 2025
3e7700d
getting rid of center=true in toVal because it is never used (despite…
ericleonardis Jan 2, 2025
9cef9f3
black reformat
ericleonardis Jan 2, 2025
1dda08c
expect slider to allow for 0 again
ericleonardis Jan 2, 2025
a50ea50
eliminate reference to center
ericleonardis Jan 2, 2025
be4c828
fix comment
ericleonardis Jan 2, 2025
d0e3558
remove unnecessary loop
ericleonardis Jan 2, 2025
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
2 changes: 1 addition & 1 deletion sleap/gui/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -1304,7 +1304,7 @@ def updateStatusMessage(self, message: Optional[str] = None):
if self.player.seekbar.hasSelection():
start, end = self.state["frame_range"]
message += spacer
message += f"Selection: {start+1:,}-{end:,} ({end-start+1:,} frames)"
message += f"Selection: {start+1:,}-{end:,} ({end-start:,} frames)"

message += f"{spacer}Labeled Frames: "
if current_video is not None:
Expand Down
34 changes: 27 additions & 7 deletions sleap/gui/widgets/slider.py
Original file line number Diff line number Diff line change
Expand Up @@ -368,8 +368,27 @@ def _toPos(self, val: float, center=False) -> float:
x += self.handle.rect().width() / 2.0
return x

def _toVal(self, x: float, center=False) -> float:
"""Converts x position to slider value."""
def _toVal(self, x: float) -> float:
"""
Converts x position to slider value.

Args:
x: x position on the slider.
center: Whether to offset by half the width of the handle.

Returns:
Slider value (frame index).
"""
if x is None:
raise ValueError("x position cannot be None")

# Force conversion to float here.
try:
val = float(x)
except (TypeError, ValueError):
raise ValueError(f"x position must be a number, got {x}")

# Proceed with arithmetic only after we have a valid float.
val = x
val /= self._slider_width
val *= max(1, self._val_max - self._val_min)
Expand All @@ -380,7 +399,8 @@ def _toVal(self, x: float, center=False) -> float:
@property
def _slider_width(self) -> float:
"""Returns visual width of slider."""
return self.box_rect.width() - self.handle.rect().width()
width = self.box_rect.width()
return width

@property
def slider_visible_value_range(self) -> float:
Expand Down Expand Up @@ -520,7 +540,7 @@ def moveSelectionAnchor(self, x: float, y: float):
"""
x = max(x, 0)
x = min(x, self.box_rect.width())
anchor_val = self._toVal(x, center=True)
anchor_val = self._toVal(x)

if len(self._selection) % 2 == 0:
self.startSelection(anchor_val)
Expand All @@ -545,9 +565,9 @@ def releaseSelectionAnchor(self, x, y):

def moveZoomDrag(self, x: float, y: float):
if getattr(self, "_zoom_start_val", None) is None:
self._zoom_start_val = self._toVal(x, center=True)
self._zoom_start_val = self._toVal(x)

current_val = self._toVal(x, center=True)
current_val = self._toVal(x)

self._draw_zoom_box(current_val, self._zoom_start_val)

Expand All @@ -556,7 +576,7 @@ def releaseZoomDrag(self, x, y):
self.zoom_box.hide()

val_a = self._zoom_start_val
val_b = self._toVal(x, center=True)
val_b = self._toVal(x)

val_start = min(val_a, val_b)
val_end = max(val_a, val_b)
Expand Down
123 changes: 123 additions & 0 deletions tests/gui/test_slider.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from sleap.gui.widgets.slider import VideoSlider, set_slider_marks_from_labels
import pytest


def test_slider(qtbot, centered_pair_predictions):
Expand Down Expand Up @@ -39,3 +40,125 @@ def test_slider(qtbot, centered_pair_predictions):

slider.setEnabled(True)
assert slider.enabled()


@pytest.mark.parametrize(
"slider_width, x_value, handle_width, min_value, max_value",
[
# ---- Cases from test_toVal ----
(1000, 500, 0, 0, 1000), # Midpoint w/o centering
(800, 400, 0, 0, 800),
(1500, 750, 0, 100, 1200),
(2000, 1000, 0, 50, 1950),
(1000, -100, 0, 0, 1000), # Below minimum
(1000, 1200, 0, 0, 1000), # Above maximum
],
)
def test_toVal(qtbot, slider_width, x_value, handle_width, min_value, max_value):
"""
Merged test that checks the slider value transformation for both:
1) center=True (handle offset subtracted).
2) center=False (no offset).

Args:
qtbot: The pytest-qt bot fixture.
slider_width: The width of the slider box_rect in pixels.
x_value: The x-coordinate on the slider to convert to a value.
handle_width: The desired width of the slider handle rect (0 if center=False).
min_value: The slider's minimum value.
max_value: The slider's maximum value.
center: Whether _toVal() is called with center=True or not.

Returns:
None
"""
slider = VideoSlider(min=min_value, max=max_value, val=(min_value + max_value) // 2)
slider.box_rect.setWidth(slider_width)

# If we have a non-zero handle_width, set the handle's bounding rect.
if handle_width > 0:
current_handle_rect = slider.handle.rect()
slider.handle.setRect(
current_handle_rect.x(),
current_handle_rect.y(),
handle_width,
current_handle_rect.height(),
)

# Read back the actual handle width (will be 0 if center=False).
actual_handle_width = slider.handle.rect().width()

# Manually compute the expected slider value.
# If center=True, subtract half the handle width.
val = float(x_value)
effective_width = max(1.0, slider_width) # Prevent zero-division
val /= effective_width
val *= max(1, max_value - min_value)
val += min_value
expected_val = round(val)

# Now call the actual function with the specified center setting.
actual_val = slider._toVal(x_value)

assert (
actual_val == expected_val
), f"For x={x_value}, handle_width={actual_handle_width}, slider_width={slider_width}, "


def test_slider_width_property(qtbot):
"""
Test the _slider_width property to ensure it accurately reflects
the visual width of the slider's box_rect.
"""
slider = VideoSlider(min=0, max=1000, val=15, marks=(10, 15)) # Initialize slider

# Test various box_rect widths
for width in [800, 1000, 1200, 1500]:
slider.box_rect.setWidth(width) # Simulate setting the visual width
assert (
slider._slider_width == width
), f"Expected _slider_width to be {width}, but got {slider._slider_width}"

# Test edge cases with very small and large widths
slider.box_rect.setWidth(0)
assert (
slider._slider_width == 0
), "Expected _slider_width to be 0 when box_rect width is 0"

slider.box_rect.setWidth(10000)
assert (
slider._slider_width == 10000
), "Expected _slider_width to handle large values correctly"


@pytest.mark.parametrize(
"invalid_value, expected_error_msg",
[
(None, "x position cannot be None"),
("invalid", "x position must be a number, got invalid"),
([], "x position must be a number, got []"),
({}, "x position must be a number, got {}"),
],
)
def test_toVal_invalid_input(qtbot, invalid_value, expected_error_msg):
"""
Tests _toVal for invalid inputs to ensure ValueError is raised
with the correct message.

Args:
qtbot: Pytest fixture for Qt applications.
invalid_value: The invalid value for x.
expected_error_msg: The exact error message expected.

Returns:
None
"""
# We use tqdm to track progress across multiple invalid inputs (optional).

slider = VideoSlider(min=0, max=1000, val=15)

with pytest.raises(ValueError) as excinfo:
slider._toVal(invalid_value)

# Verify the exact error message
assert str(excinfo.value) == expected_error_msg
Loading