Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
167 changes: 153 additions & 14 deletions deeplabcut/gui/components.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,13 +154,20 @@ def __init__(
self,
root: QtWidgets.QMainWindow,
parent: QtWidgets.QWidget,
*,
hide_videotype: bool = False,
sync_videotype_with_selection: bool = False,
strict_videotype_filter: bool = False,
):
super().__init__(parent)

self.root = root
self.parent = parent

# Optional safeties; defaults preserve current behavior
self.sync_videotype_with_selection = sync_videotype_with_selection
self.strict_videotype_filter = strict_videotype_filter

self._init_layout(hide_videotype)

def _init_layout(self, hide_videotype: bool):
Expand All @@ -170,7 +177,7 @@ def _init_layout(self, hide_videotype: bool):
self.videotype_widget = QtWidgets.QComboBox()
self.videotype_widget.setMinimumWidth(100)
self.videotype_widget.addItems(DLCParams.VIDEOTYPES)
self.videotype_widget.setCurrentText(self.root.video_type)
self.videotype_widget.setCurrentText(self._normalize_videotype(self.root.video_type))
self.root.video_type_.connect(self.videotype_widget.setCurrentText)
self.videotype_widget.currentTextChanged.connect(self.update_videotype)

Expand All @@ -181,7 +188,7 @@ def _init_layout(self, hide_videotype: bool):
self.root.video_files_.connect(self._update_video_selection)

# Number of selected videos text
self.selected_videos_text = QtWidgets.QLabel("") # updated when videos are selected
self.selected_videos_text = QtWidgets.QLabel("")

# Clear video selection
self.clear_videos = QtWidgets.QPushButton("Clear selection")
Expand All @@ -199,28 +206,146 @@ def _init_layout(self, hide_videotype: bool):
def files(self):
return self.root.video_files

def update_videotype(self, vtype):
def _normalize_videotype(self, vtype: str) -> str:
return (vtype or "").lower().lstrip(".")

@property
def selected_suffixes(self) -> set[str]:
"""Return normalized suffixes (without leading dot) of currently selected files."""
suffixes = set()
for f in self.files:
suffix = Path(f).suffix.lower().lstrip(".")
if suffix:
suffixes.add(suffix)
return suffixes

def get_effective_videotype(
self,
prefer_selected_files: bool = False,
with_dot: bool = True,
) -> str:
"""
Return the videotype to use.

By default, preserves current behavior and uses the dropdown.
If prefer_selected_files=True and the selected files all share one suffix,
that suffix is used instead.
"""
videotype = self._normalize_videotype(self.videotype_widget.currentText())

if prefer_selected_files:
suffixes = self.selected_suffixes
if len(suffixes) == 1:
videotype = next(iter(suffixes))

if with_dot and videotype:
return f".{videotype}"
return videotype

def get_files_grouped_by_suffix(self, keep_dot: bool = False) -> dict[str, list[str]]:
"""Return a dict grouping selected files by their suffixes."""
groups: dict[str, list[str]] = {}
for f in self.files:
suffix = Path(f).suffix.lower()
if not keep_dot:
suffix = suffix.lstrip(".")
groups.setdefault(suffix, []).append(f)
return groups

def _all_supported_video_patterns(self) -> list[str]:
"""Return all supported video patterns in both lower and upper case."""
return [f"*.{ext.lower()}" for ext in DLCParams.VIDEOTYPES[1:]] + [
f"*.{ext.upper()}" for ext in DLCParams.VIDEOTYPES[1:]
]

def _build_video_filter(self) -> str:
"""
Build the file dialog filter.

By default, preserve current behavior: show all supported video types.
If strict_videotype_filter is enabled, restrict to the currently selected
videotype when it is non-empty. If the current dropdown value is empty
(the "all types" option), fall back to the full supported-extension filter.
"""
all_video_types = self._all_supported_video_patterns()

if self.strict_videotype_filter:
current = self.get_effective_videotype(
prefer_selected_files=False,
with_dot=False,
)

if current:
video_types = [f"*.{current.lower()}", f"*.{current.upper()}"]
else:
# "All types" entry selected: keep the dialog usable
video_types = all_video_types
else:
video_types = all_video_types

return f"Videos ({' '.join(video_types)})"

def _set_videotype_silently(self, vtype: str):
"""
Update the dropdown/root videotype without triggering update_videotype(),
because that method clears the current selection.

Only updates state if the videotype is supported by the combo box.
Otherwise, leaves the current state unchanged.
"""
normalized = self._normalize_videotype(vtype)
current = self._normalize_videotype(self.videotype_widget.currentText())

if not normalized:
self.root.logger.warning("Attempted to set an empty videotype silently; keeping current selection.")
return

# Validate against actual combo-box items
if self.videotype_widget.findText(normalized) == -1:
self.root.logger.warning(
f"Attempted to set unsupported videotype '{normalized}' silently; "
f"keeping current videotype '{current}'."
)
return

if normalized != current:
self.videotype_widget.blockSignals(True)
self.videotype_widget.setCurrentText(normalized)
self.videotype_widget.blockSignals(False)

self.root.video_type = normalized

Comment thread
C-Achard marked this conversation as resolved.
def update_videotype(self, vtype: str):
normalized = self._normalize_videotype(vtype)
self.clear_selected_videos()
self.root.video_type = vtype
self.root.video_type = normalized

def _update_video_selection(self, videopaths):
n_videos = len(self.root.video_files)
if n_videos:
self.selected_videos_text.setText(f"{n_videos} videos selected")
suffixes = self.selected_suffixes
if len(suffixes) == 1:
suffix = next(iter(suffixes))
self.selected_videos_text.setText(f"{n_videos} videos selected (.{suffix})")
elif len(suffixes) > 1:
counts = {
suffix: len(files) for suffix, files in self.get_files_grouped_by_suffix(keep_dot=False).items()
}
summary = ", ".join(f"{count} .{suffix}" for suffix, count in sorted(counts.items()))
self.selected_videos_text.setText(
f"{n_videos} videos selected ({summary}; will run in separate batches)"
)
else:
self.selected_videos_text.setText(f"{n_videos} videos selected")

self.select_video_button.setText("Add more videos")
else:
self.selected_videos_text.setText("")
self.select_video_button.setText("Select videos")

def update_videos(self):
directory_to_open = self.root.project_folder

# Create a filter string with both lowercase and uppercase extensions

video_types = [f"*.{ext.lower()}" for ext in DLCParams.VIDEOTYPES[1:]] + [
f"*.{ext.upper()}" for ext in DLCParams.VIDEOTYPES[1:]
]
video_filter = f"Videos ({' '.join(video_types)})"
video_filter = self._build_video_filter()

filenames = QtWidgets.QFileDialog.getOpenFileNames(
parent=self,
Expand All @@ -230,8 +355,22 @@ def update_videos(self):
)

if filenames[0]:
# Qt returns a tuple (list of files, filetype)
self.root.add_video_files([os.path.abspath(vid) for vid in filenames[0]])
abs_files = [os.path.abspath(vid) for vid in filenames[0]]
self.root.add_video_files(abs_files)

# Optional safety: sync dropdown to selected file suffix
if self.sync_videotype_with_selection:
suffixes = {Path(v).suffix.lower().lstrip(".") for v in abs_files if Path(v).suffix}

if len(suffixes) == 1:
inferred = next(iter(suffixes))
self._set_videotype_silently(inferred)
self.root.logger.info(f"Inferred videotype '{inferred}' from selected file(s)")
elif len(suffixes) > 1:
self.root.logger.warning(
f"Selected videos have mixed suffixes {sorted(suffixes)}; "
"keeping current videotype dropdown unchanged."
)

def clear_selected_videos(self):
self.root.clear_video_files()
Expand Down
Loading
Loading