From 5cf4a863465cf5f9f9ac68ee819f0f0c6bba353e Mon Sep 17 00:00:00 2001 From: Glen Beane <356266+gbeane@users.noreply.github.com> Date: Sat, 5 Apr 2025 00:19:18 -0400 Subject: [PATCH 1/8] add file menu action to clear project cache --- src/jabs/project/project.py | 11 +++++++ src/jabs/ui/main_window.py | 54 +++++++++++++++++++++++++++----- src/jabs/ui/video_list_widget.py | 7 +++++ 3 files changed, 65 insertions(+), 7 deletions(-) diff --git a/src/jabs/project/project.py b/src/jabs/project/project.py index c14eb214..e3d113ee 100644 --- a/src/jabs/project/project.py +++ b/src/jabs/project/project.py @@ -777,6 +777,17 @@ def get_labeled_features(self, behavior=None, progress_callable=None): 'groups': np.concatenate(all_groups), }, group_mapping + def clear_cache(self): + if self._cache_dir is not None: + for f in self._cache_dir.glob('*'): + try: + if f.is_dir(): + shutil.rmtree(f) + else: + f.unlink() + except OSError: + pass + def __update_version(self): """ update the version number saved in project metadata """ # only update if the version in the metadata is different from current diff --git a/src/jabs/ui/main_window.py b/src/jabs/ui/main_window.py index 9ba12841..b62de23f 100644 --- a/src/jabs/ui/main_window.py +++ b/src/jabs/ui/main_window.py @@ -77,12 +77,12 @@ def __init__(self, app_name: str, app_name_long: str, *args, **kwargs): app_menu.addAction(exit_action) # export training data action - self._export_training = QtGui.QAction('Export Training Data', self) - self._export_training.setShortcut(QtGui.QKeySequence(Qt.CTRL | Qt.Key_T)) - self._export_training.setStatusTip('Export training data for this classifier') - self._export_training.setEnabled(False) - self._export_training.triggered.connect(self._export_training_data) - file_menu.addAction(self._export_training) + export_training = QtGui.QAction('Export Training Data', self) + export_training.setShortcut(QtGui.QKeySequence(Qt.CTRL | Qt.Key_T)) + export_training.setStatusTip('Export training data for this classifier') + export_training.setEnabled(False) + export_training.triggered.connect(export_training_data) + file_menu.addAction(export_training) # archive behavior action self._archive_behavior = QtGui.QAction('Archive Behavior', self) @@ -91,6 +91,13 @@ def __init__(self, app_name: str, app_name_long: str, *args, **kwargs): self._archive_behavior.triggered.connect(self._open_archive_behavior_dialog) file_menu.addAction(self._archive_behavior) + # clear cache action + self._clear_cache = QtGui.QAction('Clear Project Cache', self) + self._clear_cache.setStatusTip('Clear Project Cache') + self._clear_cache.setEnabled(False) + self._clear_cache.triggered.connect(self._clear_cache_action) + file_menu.addAction(self._clear_cache) + # video playlist menu item self.view_playlist = QtGui.QAction('View Playlist', self) self.view_playlist.setCheckable(True) @@ -176,7 +183,7 @@ def __init__(self, app_name: str, app_name_long: str, *args, **kwargs): # handle event to set status of File-Export Training Data action self._central_widget.export_training_status_change.connect( - self._export_training.setEnabled) + export_training.setEnabled) def keyPressEvent(self, event: QKeyEvent): """ @@ -401,6 +408,7 @@ def _project_loaded_callback(self): # Update which controls should be available self._archive_behavior.setEnabled(True) + self._clear_cache.setEnabled(True) self.enable_cm_units.setEnabled(self._project.is_cm_unit) self.enable_social_features.setEnabled(self._project.can_use_social_features) self.enable_segmentation_features.setEnabled(self._project.can_use_segmentation) @@ -421,6 +429,38 @@ def _project_load_error_callback(self, error: Exception): QtWidgets.QMessageBox.critical( self, "Error loading project", str(error)) + def _clear_cache_action(self): + """ + Clear the cache for the current project. Opens a dialog to get user confirmation first. + """ + + app = QtWidgets.QApplication.instance() + dont_use_native_dialogs = QtWidgets.QApplication.instance().testAttribute( + Qt.ApplicationAttribute.AA_DontUseNativeDialogs) + + if dont_use_native_dialogs is False: + # QMessageBox style is not ideal (on macOS it shows a large folder icon in the message box), so we set the + # attribute to use Qt style + app.setAttribute(Qt.ApplicationAttribute.AA_DontUseNativeDialogs, True) + + result = QtWidgets.QMessageBox.warning( + self, + "Clear Cache", + "Are you sure you want to clear the project cache?", + QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, + QtWidgets.QMessageBox.No + ) + + if dont_use_native_dialogs is False: + # reset the attribute to use native dialogs + app.setAttribute(Qt.ApplicationAttribute.AA_DontUseNativeDialogs, False) + + if result == QtWidgets.QMessageBox.Yes: + self._project.clear_cache() + # need to reload the current video to force the pose file to reload + self._central_widget.load_video(self._project.video_path(self.video_list.selected_video)) + self.display_status_message("Cache cleared", 3000) + def show_license_dialog(self): dialog = LicenseAgreementDialog(self) result = dialog.exec_() diff --git a/src/jabs/ui/video_list_widget.py b/src/jabs/ui/video_list_widget.py index 98053334..5d2b8e83 100644 --- a/src/jabs/ui/video_list_widget.py +++ b/src/jabs/ui/video_list_widget.py @@ -46,6 +46,7 @@ def __init__(self): self.setWidget(self.file_list) self._project = None + self._selected_video = None # connect to the model selectionChanged signal self.file_list.currentItemChanged.connect(self._selection_changed) @@ -54,6 +55,12 @@ def _selection_changed(self, current, previous): """ signal main window that use changed selected video """ if current: self.selectionChanged.emit(current.text()) + self._selected_video = current.text() + + @property + def selected_video(self): + """ return the currently selected video """ + return self._selected_video def set_project(self, project): """ From 0486f286d9dc43a343fc0f2ef4540f52f2f7c6b8 Mon Sep 17 00:00:00 2001 From: Glen Beane <356266+gbeane@users.noreply.github.com> Date: Sat, 5 Apr 2025 00:27:24 -0400 Subject: [PATCH 2/8] update comments --- src/jabs/ui/main_window.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/jabs/ui/main_window.py b/src/jabs/ui/main_window.py index b62de23f..270bc200 100644 --- a/src/jabs/ui/main_window.py +++ b/src/jabs/ui/main_window.py @@ -438,9 +438,9 @@ def _clear_cache_action(self): dont_use_native_dialogs = QtWidgets.QApplication.instance().testAttribute( Qt.ApplicationAttribute.AA_DontUseNativeDialogs) + # if app is currently set to use native dialogs, we will temporarily set it to use Qt dialogs + # the native style, at least on macOS, is not ideal so we'll force the Qt dialog instead if dont_use_native_dialogs is False: - # QMessageBox style is not ideal (on macOS it shows a large folder icon in the message box), so we set the - # attribute to use Qt style app.setAttribute(Qt.ApplicationAttribute.AA_DontUseNativeDialogs, True) result = QtWidgets.QMessageBox.warning( @@ -451,9 +451,8 @@ def _clear_cache_action(self): QtWidgets.QMessageBox.No ) - if dont_use_native_dialogs is False: - # reset the attribute to use native dialogs - app.setAttribute(Qt.ApplicationAttribute.AA_DontUseNativeDialogs, False) + # restore the original setting + app.setAttribute(Qt.ApplicationAttribute.AA_DontUseNativeDialogs, dont_use_native_dialogs) if result == QtWidgets.QMessageBox.Yes: self._project.clear_cache() From 9f7ad4a0a84c127fd99e070bbfcff11ea9883930 Mon Sep 17 00:00:00 2001 From: Glen Beane <356266+gbeane@users.noreply.github.com> Date: Wed, 20 Aug 2025 14:18:39 -0400 Subject: [PATCH 3/8] fix some issues from merge --- src/jabs/ui/main_window.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/jabs/ui/main_window.py b/src/jabs/ui/main_window.py index c6057e31..b5a4eacf 100644 --- a/src/jabs/ui/main_window.py +++ b/src/jabs/ui/main_window.py @@ -704,12 +704,8 @@ def _project_loaded_callback(self) -> None: self.enable_segmentation_features.setEnabled( self._project.feature_manager.can_use_segmentation_features ) - available_objects = self._project.feature_manager.static_objects self._clear_cache.setEnabled(True) - self.enable_cm_units.setEnabled(self._project.is_cm_unit) - self.enable_social_features.setEnabled(self._project.can_use_social_features) - self.enable_segmentation_features.setEnabled(self._project.can_use_segmentation) - available_objects = self._project.static_objects + available_objects = self._project.feature_manager.static_objects for static_object, menu_item in self.enable_landmark_features.items(): if static_object in available_objects: menu_item.setEnabled(True) From d034ea76e20d9ca6169da33c93f6dcc256d4ee10 Mon Sep 17 00:00:00 2001 From: Glen Beane <356266+gbeane@users.noreply.github.com> Date: Wed, 20 Aug 2025 14:26:19 -0400 Subject: [PATCH 4/8] fix some issues from merge --- src/jabs/ui/central_widget.py | 5 +++++ src/jabs/ui/main_window.py | 4 +--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/jabs/ui/central_widget.py b/src/jabs/ui/central_widget.py index 726a8980..2c7cba67 100644 --- a/src/jabs/ui/central_widget.py +++ b/src/jabs/ui/central_widget.py @@ -142,6 +142,11 @@ def update_behavior_search_query(self, search_query) -> None: """Update the search query for the search bar widget""" self._search_bar_widget.update_search(search_query) + @property + def loaded_video(self) -> Path | None: + """get the currently loaded video path""" + return self._loaded_video + @property def overlay_annotations_enabled(self) -> bool: """get the annotation overlay enabled status from player widget.""" diff --git a/src/jabs/ui/main_window.py b/src/jabs/ui/main_window.py index b5a4eacf..ed769a27 100644 --- a/src/jabs/ui/main_window.py +++ b/src/jabs/ui/main_window.py @@ -774,9 +774,7 @@ def _clear_cache_action(self): if result == QtWidgets.QMessageBox.Yes: self._project.clear_cache() # need to reload the current video to force the pose file to reload - self._central_widget.load_video( - self._project.video_path(self.video_list.selected_video) - ) + self._central_widget.load_video(self._central_widget.loaded_video) self.display_status_message("Cache cleared", 3000) def _update_recent_projects(self) -> None: From 741f2633a4b369749fd55729f52447e15e919e87 Mon Sep 17 00:00:00 2001 From: Glen Beane <356266+gbeane@users.noreply.github.com> Date: Wed, 20 Aug 2025 14:29:17 -0400 Subject: [PATCH 5/8] a little code cleanup --- src/jabs/ui/main_window.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/jabs/ui/main_window.py b/src/jabs/ui/main_window.py index ed769a27..b5824ec8 100644 --- a/src/jabs/ui/main_window.py +++ b/src/jabs/ui/main_window.py @@ -757,21 +757,21 @@ def _clear_cache_action(self): # if app is currently set to use native dialogs, we will temporarily set it to use Qt dialogs # the native style, at least on macOS, is not ideal so we'll force the Qt dialog instead - if dont_use_native_dialogs is False: + if not dont_use_native_dialogs: app.setAttribute(Qt.ApplicationAttribute.AA_DontUseNativeDialogs, True) result = QtWidgets.QMessageBox.warning( self, "Clear Cache", "Are you sure you want to clear the project cache?", - QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No, - QtWidgets.QMessageBox.No, + QtWidgets.QMessageBox.StandardButton.Yes | QtWidgets.QMessageBox.StandardButton.No, + QtWidgets.QMessageBox.StandardButton.No, ) # restore the original setting app.setAttribute(Qt.ApplicationAttribute.AA_DontUseNativeDialogs, dont_use_native_dialogs) - if result == QtWidgets.QMessageBox.Yes: + if result == QtWidgets.QMessageBox.StandardButton.Yes: self._project.clear_cache() # need to reload the current video to force the pose file to reload self._central_widget.load_video(self._central_widget.loaded_video) From cf3f22b714000a679b83e878a0e4ade00b05331c Mon Sep 17 00:00:00 2001 From: Glen Beane <356266+gbeane@users.noreply.github.com> Date: Mon, 29 Sep 2025 11:36:25 -0400 Subject: [PATCH 6/8] improve pose cache file versioning --- src/jabs/pose_estimation/pose_est.py | 37 +++++++++++++++++++++++++ src/jabs/pose_estimation/pose_est_v3.py | 16 ++++------- src/jabs/pose_estimation/pose_est_v4.py | 15 +++------- src/jabs/pose_estimation/pose_est_v6.py | 2 +- src/jabs/pose_estimation/pose_est_v8.py | 4 +++ src/jabs/ui/main_window.py | 3 +- 6 files changed, 54 insertions(+), 23 deletions(-) diff --git a/src/jabs/pose_estimation/pose_est.py b/src/jabs/pose_estimation/pose_est.py index 47ca9d89..e3560155 100644 --- a/src/jabs/pose_estimation/pose_est.py +++ b/src/jabs/pose_estimation/pose_est.py @@ -1,4 +1,5 @@ import enum +import logging from abc import ABC, abstractmethod from pathlib import Path @@ -112,6 +113,8 @@ class KeypointIndex(enum.IntEnum): ), ) + _CACHE_FILE_VERSION = 1 + def __init__(self, file_path: Path, cache_dir: Path | None = None, fps: int = 30): """initialize new object from h5 file @@ -137,6 +140,16 @@ def __init__(self, file_path: Path, cache_dir: Path | None = None, fps: int = 30 self._static_objects = {} + # check cache version, if it doesn't match, clear the cache file for this pose file + if self._cache_dir is not None and not self.check_cache_version(): + cache_file = self._cache_file_path() + if cache_file and cache_file.exists(): + try: + cache_file.unlink() + except Exception: + logging.warning("Unable to delete old cache file %s", cache_file) + pass + @property def num_frames(self) -> int: """return the number of frames in the pose_est file""" @@ -377,3 +390,27 @@ def identity_index_to_display(self, identity_index: int) -> str: if self.external_identities and 0 <= identity_index < len(self.external_identities): return self.external_identities[identity_index] return str(identity_index) + + def check_cache_version(self) -> bool: + """Check if the cache version matches the expected version. + + Returns: + bool: True if the cache version matches, False otherwise. + """ + try: + with h5py.File(self._cache_file_path(), "r") as cache_h5: + cache_version = cache_h5.attrs.get("cache_file_version", None) + return cache_version == self._CACHE_FILE_VERSION + except Exception: + return False + + def _cache_file_path(self) -> Path | None: + """Get the path to the cache file for this pose file. + + Returns: + Path | None: The path to the cache file, or None if no cache directory is set. + """ + if self._cache_dir is None: + return None + filename = self._path.name.replace(".h5", "_cache.h5") + return self._cache_dir / filename diff --git a/src/jabs/pose_estimation/pose_est_v3.py b/src/jabs/pose_estimation/pose_est_v3.py index 1155c2da..5bbcf106 100644 --- a/src/jabs/pose_estimation/pose_est_v3.py +++ b/src/jabs/pose_estimation/pose_est_v3.py @@ -34,7 +34,10 @@ class PoseEstimationV3(PoseEstimation): get_identity_point_mask(identity): Get the point mask array for a given identity. """ - __CACHE_FILE_VERSION = 2 + # super class handles validating cache file version and will delete + # if it doesn't match expected version so it will get regenerated + # bump this version to force v3 pose file cache regeneration + _CACHE_FILE_VERSION = 2 def __init__(self, file_path: Path, cache_dir: Path | None = None, fps: int = 30): super().__init__(file_path, cache_dir, fps) @@ -47,18 +50,11 @@ def __init__(self, file_path: Path, cache_dir: Path | None = None, fps: int = 30 # to speedup reopening the pose file later, we'll cache the transformed # pose file in the project dir if cache_dir is not None: - filename = self._path.name.replace(".h5", "_cache.h5") - cache_file_path = self._cache_dir / filename + cache_file_path = self._cache_file_path() use_cache = True try: with h5py.File(cache_file_path, "r") as cache_h5: - if cache_h5.attrs["version"] != self.__CACHE_FILE_VERSION: - # cache file version is not what we expect, raise - # exception so we will revert to reading source pose - # file - raise _CacheFileVersion - if cache_h5.attrs["source_pose_hash"] != self._hash: raise PoseHashException @@ -149,7 +145,7 @@ def __init__(self, file_path: Path, cache_dir: Path | None = None, fps: int = 30 if self._cache_dir is not None: with h5py.File(cache_file_path, "w") as cache_h5: - cache_h5.attrs["version"] = self.__CACHE_FILE_VERSION + cache_h5.attrs["cache_file_version"] = self._CACHE_FILE_VERSION cache_h5.attrs["source_pose_hash"] = self.hash group = cache_h5.create_group("poseest") if self._cm_per_pixel is not None: diff --git a/src/jabs/pose_estimation/pose_est_v4.py b/src/jabs/pose_estimation/pose_est_v4.py index e5eca252..ffb9e16a 100644 --- a/src/jabs/pose_estimation/pose_est_v4.py +++ b/src/jabs/pose_estimation/pose_est_v4.py @@ -37,7 +37,7 @@ class PoseEstimationV4(PoseEstimation): get_identity_point_mask(identity): Get the point mask array for a given identity. """ - __CACHE_FILE_VERSION = 4 + _CACHE_FILE_VERSION = 4 def __init__(self, file_path: Path, cache_dir: Path | None = None, fps: int = 30): super().__init__(file_path, cache_dir, fps) @@ -51,7 +51,7 @@ def __init__(self, file_path: Path, cache_dir: Path | None = None, fps: int = 30 try: self._load_from_cache() use_cache = True - except (OSError, KeyError, _CacheFileVersion, PoseHashException): + except (OSError, KeyError, PoseHashException): # if load_from_cache() raises an exception, we'll read from # the source pose file below because use_cache will still be # set to false, just ignore the exceptions here @@ -250,16 +250,9 @@ def _load_from_cache(self): Returns: None """ - filename = self._path.name.replace(".h5", "_cache.h5") - cache_file_path = self._cache_dir / filename + cache_file_path = self._cache_file_path() with h5py.File(cache_file_path, "r") as cache_h5: - if cache_h5.attrs["version"] != self.__CACHE_FILE_VERSION: - # cache file version is not what we expect, raise - # exception so we will revert to reading source pose - # file - raise _CacheFileVersion - if cache_h5.attrs["source_pose_hash"] != self._hash: raise PoseHashException @@ -297,7 +290,7 @@ def _cache_poses(self): cache_file_path = self._cache_dir / filename with h5py.File(cache_file_path, "w") as cache_h5: - cache_h5.attrs["version"] = self.__CACHE_FILE_VERSION + cache_h5.attrs["cache_file_version"] = self._CACHE_FILE_VERSION cache_h5.attrs["source_pose_hash"] = self.hash cache_h5.attrs["num_identities"] = self._num_identities cache_h5.attrs["num_frames"] = self._num_frames diff --git a/src/jabs/pose_estimation/pose_est_v6.py b/src/jabs/pose_estimation/pose_est_v6.py index e1dbbc14..61f4e780 100644 --- a/src/jabs/pose_estimation/pose_est_v6.py +++ b/src/jabs/pose_estimation/pose_est_v6.py @@ -40,7 +40,7 @@ def __init__(self, file_path: Path, cache_dir: Path | None = None, fps: int = 30 "seg_data": None, } - # open the hdf5 pose file and extract segmentation data. + # open the hdf5 pose file and extract segmentation data, this is not cached with h5py.File(self._path, "r") as pose_h5: for seg_key in set(pose_h5["poseest"].keys()) & set(self._segmentation_dict.keys()): self._segmentation_dict[seg_key] = pose_h5[f"poseest/{seg_key}"][:] diff --git a/src/jabs/pose_estimation/pose_est_v8.py b/src/jabs/pose_estimation/pose_est_v8.py index 5b8d138c..5dcdc4ee 100644 --- a/src/jabs/pose_estimation/pose_est_v8.py +++ b/src/jabs/pose_estimation/pose_est_v8.py @@ -14,6 +14,9 @@ class PoseEstimationV8(PoseEstimationV7): Adds bounding box support. """ + # force a bump in cache file version if either parent class or this class changes + _CACHE_FILE_VERSION = PoseEstimationV7._CACHE_FILE_VERSION + 1 + def __init__(self, file_path: Path, cache_dir: Path | None = None, fps: int = 30) -> None: super().__init__(file_path, cache_dir, fps) self._has_bounding_boxes = False @@ -112,6 +115,7 @@ def _load_from_h5(self, cache_dir: Path | None) -> None: filename = self._path.name.replace(".h5", "_cache.h5") cache_file_path = self._cache_dir / filename with h5py.File(cache_file_path, "a") as cache_h5: + cache_h5.attrs["cache_file_version"] = self._CACHE_FILE_VERSION grp = cache_h5.require_group("poseest") if "bboxes" in grp: del grp["bboxes"] diff --git a/src/jabs/ui/main_window.py b/src/jabs/ui/main_window.py index e0168df8..a731a6fc 100644 --- a/src/jabs/ui/main_window.py +++ b/src/jabs/ui/main_window.py @@ -787,7 +787,8 @@ def _clear_cache_action(self): if result == QtWidgets.QMessageBox.StandardButton.Yes: self._project.clear_cache() # need to reload the current video to force the pose file to reload - self._central_widget.load_video(self._central_widget.loaded_video) + if self._central_widget.loaded_video: + self._central_widget.load_video(self._central_widget.loaded_video) self.display_status_message("Cache cleared", 3000) def _update_recent_projects(self) -> None: From bdfdd0965ec5699c6324d38b7200df2b09444d64 Mon Sep 17 00:00:00 2001 From: Glen Beane <356266+gbeane@users.noreply.github.com> Date: Mon, 29 Sep 2025 11:39:52 -0400 Subject: [PATCH 7/8] edit comments --- src/jabs/pose_estimation/pose_est_v3.py | 2 +- src/jabs/pose_estimation/pose_est_v4.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/jabs/pose_estimation/pose_est_v3.py b/src/jabs/pose_estimation/pose_est_v3.py index 5bbcf106..2fecd8cd 100644 --- a/src/jabs/pose_estimation/pose_est_v3.py +++ b/src/jabs/pose_estimation/pose_est_v3.py @@ -36,7 +36,7 @@ class PoseEstimationV3(PoseEstimation): # super class handles validating cache file version and will delete # if it doesn't match expected version so it will get regenerated - # bump this version to force v3 pose file cache regeneration + # bumping this version to force v3 pose file cache regeneration only _CACHE_FILE_VERSION = 2 def __init__(self, file_path: Path, cache_dir: Path | None = None, fps: int = 30): diff --git a/src/jabs/pose_estimation/pose_est_v4.py b/src/jabs/pose_estimation/pose_est_v4.py index ffb9e16a..9a3e91ec 100644 --- a/src/jabs/pose_estimation/pose_est_v4.py +++ b/src/jabs/pose_estimation/pose_est_v4.py @@ -37,6 +37,7 @@ class PoseEstimationV4(PoseEstimation): get_identity_point_mask(identity): Get the point mask array for a given identity. """ + # bump to force regeneration of pose cache files for v4 or any subclass _CACHE_FILE_VERSION = 4 def __init__(self, file_path: Path, cache_dir: Path | None = None, fps: int = 30): From 249d226ee4fc9e6aff1434b8362e461f72635ea4 Mon Sep 17 00:00:00 2001 From: Glen Beane <356266+gbeane@users.noreply.github.com> Date: Tue, 30 Sep 2025 10:48:23 -0400 Subject: [PATCH 8/8] move clear cache menu action to app menu (from file menu) --- src/jabs/ui/main_window.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/jabs/ui/main_window.py b/src/jabs/ui/main_window.py index a731a6fc..c081a581 100644 --- a/src/jabs/ui/main_window.py +++ b/src/jabs/ui/main_window.py @@ -106,6 +106,13 @@ def __init__(self, app_name: str, app_name_long: str, *args, **kwargs) -> None: ) app_menu.addAction(session_tracking_action) + # clear cache action + self._clear_cache = QtGui.QAction("Clear Project Cache", self) + self._clear_cache.setStatusTip("Clear Project Cache") + self._clear_cache.setEnabled(False) + self._clear_cache.triggered.connect(self._clear_cache_action) + app_menu.addAction(self._clear_cache) + # exit action exit_action = QtGui.QAction(f" &Quit {self._app_name}", self) exit_action.setShortcut(QtGui.QKeySequence("Ctrl+Q")) @@ -149,12 +156,6 @@ def __init__(self, app_name: str, app_name_long: str, *args, **kwargs) -> None: file_menu.addAction(self._prune_action) # Setup View Menu - # clear cache action - self._clear_cache = QtGui.QAction("Clear Project Cache", self) - self._clear_cache.setStatusTip("Clear Project Cache") - self._clear_cache.setEnabled(False) - self._clear_cache.triggered.connect(self._clear_cache_action) - file_menu.addAction(self._clear_cache) # video playlist menu item self.view_playlist = QtGui.QAction("View Playlist", self)