Skip to content

Commit 31bd6ea

Browse files
committed
minor improvements 1
1 parent 92fc634 commit 31bd6ea

5 files changed

Lines changed: 109 additions & 44 deletions

File tree

village/custom_classes/online_plot_base.py

Lines changed: 9 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44

55

66
class OnlinePlotBase:
7-
"""Class to handle creation and management of Matplotlib figures to monitor behavioral data in real-time.
7+
"""Class to handle creation and management of Matplotlib figures to monitor
8+
behavioral data in real-time.
89
910
Use this with the variables you are registering in your task,
1011
that are part of Task.session_df.
@@ -19,15 +20,10 @@ def __init__(self) -> None:
1920
self.fig: Figure | None = None
2021
self.active = False
2122

22-
def ensure_figure(self, width: int = 10, height: int = 8) -> None:
23-
"""Ensures that the matplotlib figure exists, creating it if necessary.
24-
25-
Args:
26-
width (int, optional): Width of the figure in inches. Defaults to 10.
27-
height (int, optional): Height of the figure in inches. Defaults to 8.
28-
"""
23+
def ensure_figure(self) -> None:
24+
"""Ensures that the matplotlib figure exists, creating it if necessary."""
2925
if self.fig is None or getattr(self.fig, "canvas", None) is None:
30-
self.create_figure_and_axes(width, height)
26+
self.create_figure_and_axes()
3127

3228
def close(self) -> None:
3329
"""Closes the matplotlib figure and resets the state."""
@@ -52,13 +48,10 @@ def update_canvas(self, df: pd.DataFrame) -> None:
5248
except Exception:
5349
pass
5450

55-
def create_figure_and_axes(self, width: int = 10, height: int = 8) -> None:
56-
"""Creates the figure and axes. Should be overridden by subclasses.
57-
58-
Args:
59-
width (int, optional): Width of the figure. Defaults to 10.
60-
height (int, optional): Height of the figure. Defaults to 8.
61-
"""
51+
def create_figure_and_axes(self) -> None:
52+
"""Creates the figure and axes. Should be overridden by subclasses."""
53+
width = 10
54+
height = 8
6255
self.fig, self.ax = plt.subplots(figsize=(width, height))
6356

6457
# self.fig = plt.figure(figsize=(width, height))

village/gui/data_layout.py

Lines changed: 56 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,8 @@ def __init__(self, model: "Table | None" = None) -> None:
6363
"""Initializes the TableView.
6464
6565
Args:
66-
model (Table | None, optional): The data model for the table. Defaults to None.
66+
model (Table | None, optional): The data model for the table.
67+
Defaults to None.
6768
"""
6869
super().__init__()
6970
if model is not None:
@@ -226,7 +227,8 @@ def __init__(self, parent=None, current_value=None) -> None:
226227
227228
Args:
228229
parent (QWidget, optional): Parent widget. Defaults to None.
229-
current_value (str, optional): Current value ("ON", "OFF", or "Mon-Tue..."). Defaults to None.
230+
current_value (str, optional): Current value ("ON", "OFF", or "Mon-Tue...").
231+
Defaults to None.
230232
"""
231233
super().__init__(parent)
232234
self.setWindowTitle("Select Days or On/Off")
@@ -344,7 +346,8 @@ def getSelection(self) -> str | None:
344346
"""Constructs the result string based on selected days.
345347
346348
Returns:
347-
str: A string representing the selected days (e.g., "Mon-Wed-Fri") or "ON"/"OFF".
349+
str: A string representing the selected days
350+
(e.g., "Mon-Wed-Fri") or "ON"/"OFF".
348351
"""
349352
if self.on_checkbox.isChecked():
350353
return "ON"
@@ -413,11 +416,13 @@ def columnCount(self, parent: QModelIndex = QModelIndex()) -> int:
413416
return self.df.shape[1]
414417

415418
def data(self, index: QModelIndex, role: int = Qt.DisplayRole) -> Any:
416-
"""Returns the data stored under the given role for the item referred to by the index.
419+
"""Returns the data stored under the given role for the item
420+
referred to by the index.
417421
418422
Args:
419423
index (QModelIndex): The index of the item.
420-
role (int, optional): The role for which data is requested. Defaults to Qt.DisplayRole.
424+
role (int, optional): The role for which data is requested.
425+
Defaults to Qt.DisplayRole.
421426
422427
Returns:
423428
Any: The data for the given role.
@@ -454,12 +459,15 @@ def data(self, index: QModelIndex, role: int = Qt.DisplayRole) -> Any:
454459
def headerData(
455460
self, section: int, orientation: Qt.Orientation, role: int = Qt.DisplayRole
456461
) -> Any:
457-
"""Returns the data for the given role and section in the header with the specified orientation.
462+
"""Returns the data for the given role and section in the header
463+
with the specified orientation.
458464
459465
Args:
460466
section (int): The section number (row or column index).
461-
orientation (Qt.Orientation): The orientation of the header (Horizontal or Vertical).
462-
role (int, optional): The role for which data is requested. Defaults to Qt.DisplayRole.
467+
orientation (Qt.Orientation): The orientation of the header
468+
(Horizontal or Vertical).
469+
role (int, optional): The role for which data is requested.
470+
Defaults to Qt.DisplayRole.
463471
464472
Returns:
465473
Any: The header data.
@@ -738,7 +746,8 @@ def change_to_df(self) -> None:
738746
self.central_layout.setCurrentWidget(self.page1)
739747

740748
def update_data(self) -> None:
741-
"""Updates the data displayed in the current layout based on the selected table."""
749+
"""Updates the data displayed in the current layout based on
750+
the selected table."""
742751
if self.central_layout.currentIndex() == 0:
743752
self.page1Layout.update_data()
744753
self.page1Layout.create_table()
@@ -1168,7 +1177,8 @@ def connect_button_to_add(self, button: QPushButton) -> None:
11681177
button.show()
11691178

11701179
def update_buttons(self) -> None:
1171-
"""Updates the state and visibility of action buttons based on selection and table type."""
1180+
"""Updates the state and visibility of action buttons based on selection
1181+
and table type."""
11721182
sel_model = self.table_view.selectionModel()
11731183
selected_indexes = sel_model.selectedRows() if sel_model else []
11741184
match manager.table:
@@ -1315,7 +1325,8 @@ def get_selected_row_series(self) -> pd.Series | None:
13151325
return self.model.df.iloc[index.row()]
13161326

13171327
def get_seconds_from_session_row(self) -> int:
1318-
"""Calculates the time elapsed in seconds from the session start for the selected row.
1328+
"""Calculates the time elapsed in seconds from the session start
1329+
for the selected row.
13191330
13201331
Returns:
13211332
int: The elapsed time in seconds.
@@ -1405,7 +1416,8 @@ def get_paths_from_sessions_summary_row(self, row: pd.Series) -> list[str]:
14051416
row (pd.Series): The session summary row.
14061417
14071418
Returns:
1408-
list[str]: A list containing session paths (csv, raw, json, video, video_data).
1419+
list[str]: A list containing session paths
1420+
(csv, raw, json, video, video_data).
14091421
"""
14101422
date_str = row["date"]
14111423
task = row["task"]
@@ -1820,8 +1832,7 @@ def __init__(self, window: GuiWindow, rows: int, columns: int) -> None:
18201832
columns (int): Number of columns.
18211833
"""
18221834
super().__init__(window, rows=rows, columns=columns)
1823-
self.deltas: list[float] = []
1824-
self.now = time_utils.get_time_monotonic()
1835+
self.video_path = ""
18251836
self.draw()
18261837

18271838
def draw(self) -> None:
@@ -1913,6 +1924,36 @@ def draw(self) -> None:
19131924
self.backward_five_minutes,
19141925
"Skip backward 5 minutes",
19151926
)
1927+
self.create_and_add_button(
1928+
"Previous video",
1929+
26,
1930+
155,
1931+
15,
1932+
2,
1933+
self.previous_video,
1934+
"Play the previous video",
1935+
)
1936+
self.create_and_add_button(
1937+
"Next video",
1938+
26,
1939+
170,
1940+
15,
1941+
2,
1942+
self.next_video,
1943+
"Play the next video",
1944+
)
1945+
1946+
def next_video(self) -> None:
1947+
path = time_utils.next_video_path(self.video_path)
1948+
if path is not None:
1949+
self.stop_button_clicked()
1950+
self.start_video(path, 0)
1951+
1952+
def previous_video(self) -> None:
1953+
path = time_utils.previous_video_path(self.video_path)
1954+
if path is not None:
1955+
self.stop_button_clicked()
1956+
self.start_video(path, 0)
19161957

19171958
def start_video(self, path: str, seconds: int) -> None:
19181959
"""Starts video playback from a specific time.
@@ -1921,6 +1962,7 @@ def start_video(self, path: str, seconds: int) -> None:
19211962
path (str): The path to the video file.
19221963
seconds (int): The number of seconds to skip.
19231964
"""
1965+
self.video_path = path
19241966
try:
19251967
self.cap = cv2.VideoCapture(path)
19261968
self.fps = int(self.cap.get(cv2.CAP_PROP_FPS))
@@ -2059,16 +2101,9 @@ def close(self) -> None:
20592101
"""Closes the video layout and saves playback data."""
20602102
self.stop_button_clicked()
20612103
self.data_from_video_change_requested.emit("")
2062-
with open("deltas.txt", "w") as f:
2063-
for delta in self.deltas:
2064-
f.write(f"{delta}\n")
20652104

20662105
def next_frame_slot(self) -> None:
20672106
"""Handles the next frame timer event to update the video display."""
2068-
last = self.now
2069-
self.now = time_utils.get_time_monotonic()
2070-
delta = int((self.now - last) * 1000)
2071-
self.deltas.append(delta)
20722107

20732108
ret, frame = self.cap.read()
20742109
if ret:

village/gui/layout.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,8 @@ def __init__(
212212
213213
Args:
214214
window (GuiWindow): The parent window.
215-
stacked (bool, optional): Whether this layout is part of a stacked widget. Defaults to False.
215+
stacked (bool, optional): Whether this layout is part of a stacked widget.
216+
Defaults to False.
216217
rows (int, optional): Number of rows in the grid. Defaults to 51.
217218
columns (int, optional): Number of columns in the grid. Defaults to 200.
218219
"""
@@ -793,7 +794,8 @@ def create_and_add_toggle_button(
793794
index (int): Initial index in possible_values.
794795
action (Callable): The function to call on toggle.
795796
description (str): Tooltip text.
796-
complete_name (bool, optional): Whether to show key + value. Defaults to False.
797+
complete_name (bool, optional): Whether to show key + value.
798+
Defaults to False.
797799
color (str, optional): Background color. Defaults to "lightgray".
798800
799801
Returns:

village/gui/sound_calibration_layout.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -348,11 +348,12 @@ def draw(self) -> None:
348348
self.addLayout(self.plot_layout, 5, 121, 38, 79)
349349

350350
def change_layout(self, auto: bool = False) -> bool:
351-
if manager.state in [State.RUN_MANUAL, State.SAVE_MANUAL]:
352-
if not auto:
353-
QMessageBox.information(
354-
self.window, "WARNING", "Wait until the task finishes."
355-
)
351+
if auto:
352+
return False
353+
elif manager.state in [State.RUN_MANUAL, State.SAVE_MANUAL]:
354+
QMessageBox.information(
355+
self.window, "WARNING", "Wait until the task finishes."
356+
)
356357
return False
357358
elif self.save_button.isEnabled():
358359
reply = QMessageBox.question(

village/scripts/time_utils.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -347,6 +347,40 @@ def format_duration(self, milliseconds: int) -> str:
347347
millis = milliseconds % 1_000
348348
return f"{hours:02}:{minutes:02}:{seconds:02}.{millis:03}"
349349

350+
def get_sorted_videos(self, directory: str) -> list[str]:
351+
files = [f for f in os.listdir(directory) if f.endswith(".mp4")]
352+
files_with_dates = []
353+
for f in files:
354+
try:
355+
date = self.date_from_path(f)
356+
files_with_dates.append((f, date))
357+
except (ValueError, IndexError):
358+
pass
359+
files_with_dates.sort(key=lambda x: x[1])
360+
return [f for f, _ in files_with_dates]
361+
362+
def next_video_path(self, video_path: str) -> str | None:
363+
directory = os.path.dirname(video_path)
364+
filename = os.path.basename(video_path)
365+
sorted_videos = self.get_sorted_videos(directory)
366+
if filename not in sorted_videos:
367+
return None
368+
idx = sorted_videos.index(filename)
369+
if idx + 1 < len(sorted_videos):
370+
return os.path.join(directory, sorted_videos[idx + 1])
371+
return None
372+
373+
def previous_video_path(self, video_path: str) -> str | None:
374+
directory = os.path.dirname(video_path)
375+
filename = os.path.basename(video_path)
376+
sorted_videos = self.get_sorted_videos(directory)
377+
if filename not in sorted_videos:
378+
return None
379+
idx = sorted_videos.index(filename)
380+
if idx - 1 >= 0:
381+
return os.path.join(directory, sorted_videos[idx - 1])
382+
return None
383+
350384
class Chrono:
351385
"""Simple specific chronometer for measuring elapsed time."""
352386

0 commit comments

Comments
 (0)