From dbd1fcb98912616d03c456718baf9b3f2e65a03c Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 3 Nov 2023 17:03:54 +0100 Subject: [PATCH 1/9] make sure all QThread objects are always removed from python memory --- .../ayon_utils/widgets/folders_widget.py | 3 +- .../ayon_utils/widgets/projects_widget.py | 13 +++-- .../tools/ayon_utils/widgets/tasks_widget.py | 49 ++++++++++--------- 3 files changed, 35 insertions(+), 30 deletions(-) diff --git a/openpype/tools/ayon_utils/widgets/folders_widget.py b/openpype/tools/ayon_utils/widgets/folders_widget.py index 322553c51c..b72a992858 100644 --- a/openpype/tools/ayon_utils/widgets/folders_widget.py +++ b/openpype/tools/ayon_utils/widgets/folders_widget.py @@ -104,8 +104,8 @@ class FoldersModel(QtGui.QStandardItemModel): if not project_name: self._last_project_name = project_name - self._current_refresh_thread = None self._fill_items({}) + self._current_refresh_thread = None return self._is_refreshing = True @@ -152,6 +152,7 @@ class FoldersModel(QtGui.QStandardItemModel): return self._fill_items(thread.get_result()) + self._current_refresh_thread = None def _fill_item_data(self, item, folder_item): """ diff --git a/openpype/tools/ayon_utils/widgets/projects_widget.py b/openpype/tools/ayon_utils/widgets/projects_widget.py index be18cfe3ed..05347faca4 100644 --- a/openpype/tools/ayon_utils/widgets/projects_widget.py +++ b/openpype/tools/ayon_utils/widgets/projects_widget.py @@ -35,12 +35,11 @@ class ProjectsModel(QtGui.QStandardItemModel): self._selected_project = None - self._is_refreshing = False self._refresh_thread = None @property def is_refreshing(self): - return self._is_refreshing + return self._refresh_thread is not None def refresh(self): self._refresh() @@ -169,15 +168,16 @@ class ProjectsModel(QtGui.QStandardItemModel): return self._select_item def _refresh(self): - if self._is_refreshing: + if self._refresh_thread is not None: return - self._is_refreshing = True + refresh_thread = RefreshThread( "projects", self._query_project_items ) refresh_thread.refresh_finished.connect(self._refresh_finished) - refresh_thread.start() + self._refresh_thread = refresh_thread + refresh_thread.start() def _query_project_items(self): return self._controller.get_project_items() @@ -185,11 +185,10 @@ class ProjectsModel(QtGui.QStandardItemModel): def _refresh_finished(self): # TODO check if failed result = self._refresh_thread.get_result() - self._refresh_thread = None self._fill_items(result) - self._is_refreshing = False + self._refresh_thread = None self.refreshed.emit() def _fill_items(self, project_items): diff --git a/openpype/tools/ayon_utils/widgets/tasks_widget.py b/openpype/tools/ayon_utils/widgets/tasks_widget.py index d01b3a7917..a6375c6ae6 100644 --- a/openpype/tools/ayon_utils/widgets/tasks_widget.py +++ b/openpype/tools/ayon_utils/widgets/tasks_widget.py @@ -185,28 +185,7 @@ class TasksModel(QtGui.QStandardItemModel): thread.refresh_finished.connect(self._on_refresh_thread) thread.start() - def _on_refresh_thread(self, thread_id): - """Callback when refresh thread is finished. - - Technically can be running multiple refresh threads at the same time, - to avoid using values from wrong thread, we check if thread id is - current refresh thread id. - - Tasks are stored by name, so if a folder has same task name as - previously selected folder it keeps the selection. - - Args: - thread_id (str): Thread id. - """ - - # Make sure to remove thread from '_refresh_threads' dict - thread = self._refresh_threads.pop(thread_id) - if ( - self._current_refresh_thread is None - or thread_id != self._current_refresh_thread.id - ): - return - + def _fill_data_from_thread(self, thread): task_items = thread.get_result() # Task items are refreshed if task_items is None: @@ -247,7 +226,33 @@ class TasksModel(QtGui.QStandardItemModel): if new_items: root_item.appendRows(new_items) + def _on_refresh_thread(self, thread_id): + """Callback when refresh thread is finished. + + Technically can be running multiple refresh threads at the same time, + to avoid using values from wrong thread, we check if thread id is + current refresh thread id. + + Tasks are stored by name, so if a folder has same task name as + previously selected folder it keeps the selection. + + Args: + thread_id (str): Thread id. + """ + + # Make sure to remove thread from '_refresh_threads' dict + thread = self._refresh_threads.pop(thread_id) + if ( + self._current_refresh_thread is None + or thread_id != self._current_refresh_thread.id + ): + return + + self._fill_data_from_thread(thread) + + root_item = self.invisibleRootItem() self._has_content = root_item.rowCount() > 0 + self._current_refresh_thread = None self._is_refreshing = False self.refreshed.emit() From 2a19d5cc548e55bd526f24a70494717fbe5f23c7 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 3 Nov 2023 18:57:15 +0100 Subject: [PATCH 2/9] fix default type of projects model cache --- openpype/tools/ayon_utils/models/projects.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/openpype/tools/ayon_utils/models/projects.py b/openpype/tools/ayon_utils/models/projects.py index 4ad53fbbfa..383f676c64 100644 --- a/openpype/tools/ayon_utils/models/projects.py +++ b/openpype/tools/ayon_utils/models/projects.py @@ -87,7 +87,7 @@ def _get_project_items_from_entitiy(projects): class ProjectsModel(object): def __init__(self, controller): - self._projects_cache = CacheItem(default_factory=dict) + self._projects_cache = CacheItem(default_factory=list) self._project_items_by_name = {} self._projects_by_name = {} From 3bffe3b31b3b367e4a24f0446d2b217e3623ecbe Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 3 Nov 2023 18:58:29 +0100 Subject: [PATCH 3/9] renamed 'ProjectsModel' to 'ProjectsQtModel' --- openpype/tools/ayon_launcher/ui/projects_widget.py | 4 ++-- openpype/tools/ayon_utils/widgets/__init__.py | 4 ++-- openpype/tools/ayon_utils/widgets/projects_widget.py | 6 +++--- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/openpype/tools/ayon_launcher/ui/projects_widget.py b/openpype/tools/ayon_launcher/ui/projects_widget.py index 7dbaec5147..31c36719a6 100644 --- a/openpype/tools/ayon_launcher/ui/projects_widget.py +++ b/openpype/tools/ayon_launcher/ui/projects_widget.py @@ -3,7 +3,7 @@ from qtpy import QtWidgets, QtCore from openpype.tools.flickcharm import FlickCharm from openpype.tools.utils import PlaceholderLineEdit, RefreshButton from openpype.tools.ayon_utils.widgets import ( - ProjectsModel, + ProjectsQtModel, ProjectSortFilterProxy, ) from openpype.tools.ayon_utils.models import PROJECTS_MODEL_SENDER @@ -95,7 +95,7 @@ class ProjectsWidget(QtWidgets.QWidget): projects_view.setSelectionMode(QtWidgets.QListView.NoSelection) flick = FlickCharm(parent=self) flick.activateOn(projects_view) - projects_model = ProjectsModel(controller) + projects_model = ProjectsQtModel(controller) projects_proxy_model = ProjectSortFilterProxy() projects_proxy_model.setSourceModel(projects_model) diff --git a/openpype/tools/ayon_utils/widgets/__init__.py b/openpype/tools/ayon_utils/widgets/__init__.py index 432a249a73..1ef7dfe482 100644 --- a/openpype/tools/ayon_utils/widgets/__init__.py +++ b/openpype/tools/ayon_utils/widgets/__init__.py @@ -1,7 +1,7 @@ from .projects_widget import ( # ProjectsWidget, ProjectsCombobox, - ProjectsModel, + ProjectsQtModel, ProjectSortFilterProxy, ) @@ -25,7 +25,7 @@ from .utils import ( __all__ = ( # "ProjectsWidget", "ProjectsCombobox", - "ProjectsModel", + "ProjectsQtModel", "ProjectSortFilterProxy", "FoldersWidget", diff --git a/openpype/tools/ayon_utils/widgets/projects_widget.py b/openpype/tools/ayon_utils/widgets/projects_widget.py index 05347faca4..9f0f839281 100644 --- a/openpype/tools/ayon_utils/widgets/projects_widget.py +++ b/openpype/tools/ayon_utils/widgets/projects_widget.py @@ -10,11 +10,11 @@ PROJECT_IS_CURRENT_ROLE = QtCore.Qt.UserRole + 4 LIBRARY_PROJECT_SEPARATOR_ROLE = QtCore.Qt.UserRole + 5 -class ProjectsModel(QtGui.QStandardItemModel): +class ProjectsQtModel(QtGui.QStandardItemModel): refreshed = QtCore.Signal() def __init__(self, controller): - super(ProjectsModel, self).__init__() + super(ProjectsQtModel, self).__init__() self._controller = controller self._project_items = {} @@ -402,7 +402,7 @@ class ProjectsCombobox(QtWidgets.QWidget): projects_combobox = QtWidgets.QComboBox(self) combobox_delegate = QtWidgets.QStyledItemDelegate(projects_combobox) projects_combobox.setItemDelegate(combobox_delegate) - projects_model = ProjectsModel(controller) + projects_model = ProjectsQtModel(controller) projects_proxy_model = ProjectSortFilterProxy() projects_proxy_model.setSourceModel(projects_model) projects_combobox.setModel(projects_proxy_model) From 264e3cac79b863c5eb45841860da7789a13e714f Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 3 Nov 2023 18:59:02 +0100 Subject: [PATCH 4/9] renamed other qt models to contain 'Qt' --- openpype/tools/ayon_loader/ui/folders_widget.py | 4 ++-- openpype/tools/ayon_utils/widgets/__init__.py | 8 ++++---- openpype/tools/ayon_utils/widgets/folders_widget.py | 6 +++--- openpype/tools/ayon_utils/widgets/tasks_widget.py | 8 ++++---- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/openpype/tools/ayon_loader/ui/folders_widget.py b/openpype/tools/ayon_loader/ui/folders_widget.py index 53351f76d9..eaaf7ca617 100644 --- a/openpype/tools/ayon_loader/ui/folders_widget.py +++ b/openpype/tools/ayon_loader/ui/folders_widget.py @@ -8,7 +8,7 @@ from openpype.tools.utils import ( from openpype.style import get_objected_colors from openpype.tools.ayon_utils.widgets import ( - FoldersModel, + FoldersQtModel, FOLDERS_MODEL_SENDER_NAME, ) from openpype.tools.ayon_utils.widgets.folders_widget import FOLDER_ID_ROLE @@ -182,7 +182,7 @@ class UnderlinesFolderDelegate(QtWidgets.QItemDelegate): painter.restore() -class LoaderFoldersModel(FoldersModel): +class LoaderFoldersModel(FoldersQtModel): def __init__(self, *args, **kwargs): super(LoaderFoldersModel, self).__init__(*args, **kwargs) diff --git a/openpype/tools/ayon_utils/widgets/__init__.py b/openpype/tools/ayon_utils/widgets/__init__.py index 1ef7dfe482..f58de17c4a 100644 --- a/openpype/tools/ayon_utils/widgets/__init__.py +++ b/openpype/tools/ayon_utils/widgets/__init__.py @@ -7,13 +7,13 @@ from .projects_widget import ( from .folders_widget import ( FoldersWidget, - FoldersModel, + FoldersQtModel, FOLDERS_MODEL_SENDER_NAME, ) from .tasks_widget import ( TasksWidget, - TasksModel, + TasksQtModel, TASKS_MODEL_SENDER_NAME, ) from .utils import ( @@ -29,11 +29,11 @@ __all__ = ( "ProjectSortFilterProxy", "FoldersWidget", - "FoldersModel", + "FoldersQtModel", "FOLDERS_MODEL_SENDER_NAME", "TasksWidget", - "TasksModel", + "TasksQtModel", "TASKS_MODEL_SENDER_NAME", "get_qt_icon", diff --git a/openpype/tools/ayon_utils/widgets/folders_widget.py b/openpype/tools/ayon_utils/widgets/folders_widget.py index b72a992858..44323a192c 100644 --- a/openpype/tools/ayon_utils/widgets/folders_widget.py +++ b/openpype/tools/ayon_utils/widgets/folders_widget.py @@ -16,7 +16,7 @@ FOLDER_PATH_ROLE = QtCore.Qt.UserRole + 3 FOLDER_TYPE_ROLE = QtCore.Qt.UserRole + 4 -class FoldersModel(QtGui.QStandardItemModel): +class FoldersQtModel(QtGui.QStandardItemModel): """Folders model which cares about refresh of folders. Args: @@ -26,7 +26,7 @@ class FoldersModel(QtGui.QStandardItemModel): refreshed = QtCore.Signal() def __init__(self, controller): - super(FoldersModel, self).__init__() + super(FoldersQtModel, self).__init__() self._controller = controller self._items_by_id = {} @@ -282,7 +282,7 @@ class FoldersWidget(QtWidgets.QWidget): folders_view = TreeView(self) folders_view.setHeaderHidden(True) - folders_model = FoldersModel(controller) + folders_model = FoldersQtModel(controller) folders_proxy_model = RecursiveSortFilterProxyModel() folders_proxy_model.setSourceModel(folders_model) folders_proxy_model.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive) diff --git a/openpype/tools/ayon_utils/widgets/tasks_widget.py b/openpype/tools/ayon_utils/widgets/tasks_widget.py index a6375c6ae6..f27711acdd 100644 --- a/openpype/tools/ayon_utils/widgets/tasks_widget.py +++ b/openpype/tools/ayon_utils/widgets/tasks_widget.py @@ -12,7 +12,7 @@ ITEM_NAME_ROLE = QtCore.Qt.UserRole + 3 TASK_TYPE_ROLE = QtCore.Qt.UserRole + 4 -class TasksModel(QtGui.QStandardItemModel): +class TasksQtModel(QtGui.QStandardItemModel): """Tasks model which cares about refresh of tasks by folder id. Args: @@ -22,7 +22,7 @@ class TasksModel(QtGui.QStandardItemModel): refreshed = QtCore.Signal() def __init__(self, controller): - super(TasksModel, self).__init__() + super(TasksQtModel, self).__init__() self._controller = controller @@ -285,7 +285,7 @@ class TasksModel(QtGui.QStandardItemModel): if section == 0: return "Tasks" - return super(TasksModel, self).headerData( + return super(TasksQtModel, self).headerData( section, orientation, role ) @@ -310,7 +310,7 @@ class TasksWidget(QtWidgets.QWidget): tasks_view = DeselectableTreeView(self) tasks_view.setIndentation(0) - tasks_model = TasksModel(controller) + tasks_model = TasksQtModel(controller) tasks_proxy_model = QtCore.QSortFilterProxyModel() tasks_proxy_model.setSourceModel(tasks_model) tasks_proxy_model.setSortCaseSensitivity(QtCore.Qt.CaseInsensitive) From 86e4bed1514a1506200ac3ca9c51956c95414b2b Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 3 Nov 2023 18:59:52 +0100 Subject: [PATCH 5/9] validate that item on which is clicked is enabled --- openpype/tools/ayon_launcher/ui/projects_widget.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/openpype/tools/ayon_launcher/ui/projects_widget.py b/openpype/tools/ayon_launcher/ui/projects_widget.py index 31c36719a6..38c7f62bd5 100644 --- a/openpype/tools/ayon_launcher/ui/projects_widget.py +++ b/openpype/tools/ayon_launcher/ui/projects_widget.py @@ -133,9 +133,14 @@ class ProjectsWidget(QtWidgets.QWidget): return self._projects_model.has_content() def _on_view_clicked(self, index): - if index.isValid(): - project_name = index.data(QtCore.Qt.DisplayRole) - self._controller.set_selected_project(project_name) + if not index.isValid(): + return + model = index.model() + flags = model.flags(index) + if not flags & QtCore.Qt.ItemIsEnabled: + return + project_name = index.data(QtCore.Qt.DisplayRole) + self._controller.set_selected_project(project_name) def _on_project_filter_change(self, text): self._projects_proxy_model.setFilterFixedString(text) From 05748bbb9291424ed82495a63bc581b2437fe772 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 3 Nov 2023 19:00:51 +0100 Subject: [PATCH 6/9] projects model pass sender value --- openpype/tools/ayon_utils/widgets/projects_widget.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/openpype/tools/ayon_utils/widgets/projects_widget.py b/openpype/tools/ayon_utils/widgets/projects_widget.py index 9f0f839281..804b7a05ac 100644 --- a/openpype/tools/ayon_utils/widgets/projects_widget.py +++ b/openpype/tools/ayon_utils/widgets/projects_widget.py @@ -180,7 +180,9 @@ class ProjectsQtModel(QtGui.QStandardItemModel): refresh_thread.start() def _query_project_items(self): - return self._controller.get_project_items() + return self._controller.get_project_items( + sender=PROJECTS_MODEL_SENDER + ) def _refresh_finished(self): # TODO check if failed From 42c32f81969399bc900fdfe2900d0da0e8c3edea Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 3 Nov 2023 19:01:26 +0100 Subject: [PATCH 7/9] projects model returns 'None' if is in middle of refreshing --- openpype/tools/ayon_utils/models/projects.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/openpype/tools/ayon_utils/models/projects.py b/openpype/tools/ayon_utils/models/projects.py index 383f676c64..36d53edc24 100644 --- a/openpype/tools/ayon_utils/models/projects.py +++ b/openpype/tools/ayon_utils/models/projects.py @@ -103,8 +103,18 @@ class ProjectsModel(object): self._refresh_projects_cache() def get_project_items(self, sender): + """ + + Args: + sender (str): Name of sender who asked for items. + + Returns: + Union[list[ProjectItem], None]: List of project items, or None + if model is refreshing. + """ + if not self._projects_cache.is_valid: - self._refresh_projects_cache(sender) + return self._refresh_projects_cache(sender) return self._projects_cache.get_data() def get_project_entity(self, project_name): @@ -136,11 +146,12 @@ class ProjectsModel(object): def _refresh_projects_cache(self, sender=None): if self._is_refreshing: - return + return None with self._project_refresh_event_manager(sender): project_items = self._query_projects() self._projects_cache.update_data(project_items) + return self._projects_cache.get_data() def _query_projects(self): projects = ayon_api.get_projects(fields=["name", "active", "library"]) From 5a0b2f69153f71469b60acf78d588c3583493764 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 3 Nov 2023 19:01:40 +0100 Subject: [PATCH 8/9] projects model handle cases when model is refreshing --- openpype/tools/ayon_utils/widgets/projects_widget.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/openpype/tools/ayon_utils/widgets/projects_widget.py b/openpype/tools/ayon_utils/widgets/projects_widget.py index 804b7a05ac..2beee29cb9 100644 --- a/openpype/tools/ayon_utils/widgets/projects_widget.py +++ b/openpype/tools/ayon_utils/widgets/projects_widget.py @@ -1,3 +1,5 @@ +import uuid + from qtpy import QtWidgets, QtCore, QtGui from openpype.tools.ayon_utils.models import PROJECTS_MODEL_SENDER @@ -187,11 +189,14 @@ class ProjectsQtModel(QtGui.QStandardItemModel): def _refresh_finished(self): # TODO check if failed result = self._refresh_thread.get_result() - - self._fill_items(result) + if result is not None: + self._fill_items(result) self._refresh_thread = None - self.refreshed.emit() + if result is None: + self._refresh() + else: + self.refreshed.emit() def _fill_items(self, project_items): new_project_names = { From d87506f8af15123cdc0d07176dba26bce262a434 Mon Sep 17 00:00:00 2001 From: Jakub Trllo Date: Fri, 3 Nov 2023 19:12:17 +0100 Subject: [PATCH 9/9] removed unused import --- openpype/tools/ayon_utils/widgets/projects_widget.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/openpype/tools/ayon_utils/widgets/projects_widget.py b/openpype/tools/ayon_utils/widgets/projects_widget.py index 2beee29cb9..f98bfcdf8a 100644 --- a/openpype/tools/ayon_utils/widgets/projects_widget.py +++ b/openpype/tools/ayon_utils/widgets/projects_widget.py @@ -1,5 +1,3 @@ -import uuid - from qtpy import QtWidgets, QtCore, QtGui from openpype.tools.ayon_utils.models import PROJECTS_MODEL_SENDER