From 24159ff2ada7a2741921ef91e2606e06ee787749 Mon Sep 17 00:00:00 2001 From: "patrick.macdonald" Date: Wed, 7 Feb 2018 18:05:00 +0000 Subject: [PATCH 1/4] Library Tag Support added. Multi entity selection support added. --- python/tk_multi_loader/dialog.py | 201 ++++++++++++------ python/tk_multi_loader/model_latestpublish.py | 102 +++++---- 2 files changed, 197 insertions(+), 106 deletions(-) diff --git a/python/tk_multi_loader/dialog.py b/python/tk_multi_loader/dialog.py index a11ea1ab..f14aad54 100644 --- a/python/tk_multi_loader/dialog.py +++ b/python/tk_multi_loader/dialog.py @@ -67,6 +67,7 @@ def __init__(self, action_manager, parent=None): """ QtGui.QWidget.__init__(self, parent) self._action_manager = action_manager + self._bundle = sgtk.platform.current_bundle() # The loader app can be invoked from other applications with a custom # action manager as a File Open-like dialog. For these managers, we won't @@ -924,7 +925,7 @@ def _on_show_subitems_toggled(self): # tell publish UI to update itself item = self._get_selected_entity() - self._load_publishes_for_entity_item(item) + self._load_publishes_for_entity_items([item]) def _on_thumb_size_slider_change(self, value): """ @@ -1051,7 +1052,6 @@ def _get_selected_entity(self): Returns the item currently selected in the tree view, None if no selection has been made. """ - selected_item = None selection_model = self._entity_presets[self._current_entity_preset].view.selectionModel() if selection_model.hasSelection(): @@ -1070,6 +1070,30 @@ def _get_selected_entity(self): return selected_item + def _get_selected_entities(self): + """ + Returns the items currently selected in the tree view, None + if no selection has been made. + """ + selected_items = None + selection_model = self._entity_presets[self._current_entity_preset].view.selectionModel() + + if selection_model.hasSelection(): + selection = selection_model.selection().indexes() + + selected_items = [] + for current_idx in selection: + model = current_idx.model() + + if not isinstance(model, (SgHierarchyModel, SgEntityModel)): + # proxy model! + current_idx = model.mapToSource(current_idx) + + selected_item = current_idx.model().itemFromIndex(current_idx) + selected_items.append(selected_item) + + return selected_items + def _select_tab(self, tab_caption, track_in_history): """ Programmatically selects a tab based on the requested caption. @@ -1208,6 +1232,14 @@ def _load_entity_presets(self): sg_entity_type = setting_dict["entity_type"] + # Check to see if we are showing a tags view. + # Library tags are a new entity so have a temporary entity name of "CustomEntity14" + # I suggest we create a new project specific tag field as part of a default SG project called 'project_tag' + if sg_entity_type == 'CustomEntity14': + type_tag = True + else: + type_tag = False + # get optional publish_filter setting # note: actual value in the yaml settings can be None, # that's why we cannot use setting_dict.get("publish_filters", []) @@ -1242,6 +1274,14 @@ def _load_entity_presets(self): view.setHeaderHidden(True) view.setModel(proxy_model) + # Enable multiselection on tag list entities (or in this case extended selection so selections are sticky) + if type_tag: + self._log_info('view.setSelectionMode(QtGui.QAbstractItemView.ExtendedSelection)') + view.setSelectionMode(QtGui.QAbstractItemView.MultiSelection) + else: + self._log_info('view.setSelectionMode(QtGui.QAbstractItemView.SingleSelection)') + view.setSelectionMode(QtGui.QAbstractItemView.SingleSelection) + # Keep a handle to all the new Qt objects, otherwise the GC may not work. self._dynamic_widgets.extend([model, proxy_model, tab, layout, view]) @@ -1499,7 +1539,7 @@ def _hierarchy_refreshed(self): selected_item = self._get_selected_entity() # tell publish UI to update itself - self._load_publishes_for_entity_item(selected_item) + self._load_publishes_for_entity_items([selected_item]) def _node_activated(self, incremental_paths, view, proxy_model): """ @@ -1643,14 +1683,13 @@ def _switch_profile_tab(self, new_index, track_in_history): self._setup_details_panel([]) # tell the publish view to change - self._load_publishes_for_entity_item(selected_item) + self._load_publishes_for_entity_items([selected_item]) def _on_treeview_item_selected(self): """ Slot triggered when someone changes the selection in a treeview. """ - - selected_item = self._get_selected_entity() + selected_items = self._get_selected_entities() # update breadcrumbs self._populate_entity_breadcrumbs(selected_item) @@ -1659,24 +1698,31 @@ def _on_treeview_item_selected(self): # nodes are displayed in the main view, so make sure # they are loaded. model = self._entity_presets[self._current_entity_preset].model - if selected_item and model.canFetchMore(selected_item.index()): - model.fetchMore(selected_item.index()) - # notify history - self._add_history_record(self._current_entity_preset, selected_item) + multi_selection_filters = [] + for selected_item in selected_items: + selected_item_filters = model.get_filters(selected_item) + for f in selected_item_filters: + if f not in multi_selection_filters: + multi_selection_filters.append(f) + + if selected_item and model.canFetchMore(selected_item.index()): + model.fetchMore(selected_item.index()) + + # notify history + self._add_history_record(self._current_entity_preset, selected_item) # tell details panel to clear itself self._setup_details_panel([]) # tell publish UI to update itself - self._load_publishes_for_entity_item(selected_item) + self._load_publishes_for_entity_items(selected_items) - def _load_publishes_for_entity_item(self, item): + def _load_publishes_for_entity_items(self, items): """ Given an item from the treeview, or None if no item is selected, prepare the publish area UI. """ - # clear selection. If we don't clear the model at this point, # the selection model will attempt to pair up with the model is # data is being loaded in, resulting in many many events @@ -1686,67 +1732,69 @@ def _load_publishes_for_entity_item(self, item): child_folders = [] proxy_model = self._entity_presets[self._current_entity_preset].proxy_model - if item is None: - # nothing is selected, bring in all the top level - # objects in the current tab - num_children = proxy_model.rowCount() - - for x in range(num_children): - # get the (proxy model) index for the child - child_idx_proxy = proxy_model.index(x, 0) - # switch to shotgun model index - child_idx = proxy_model.mapToSource(child_idx_proxy) - # resolve the index into an actual standarditem object - i = self._entity_presets[self._current_entity_preset].model.itemFromIndex(child_idx) - child_folders.append(i) + for item in items: + if item is None: + # nothing is selected, bring in all the top level + # objects in the current tab + num_children = proxy_model.rowCount() + + for x in range(num_children): + # get the (proxy model) index for the child + child_idx_proxy = proxy_model.index(x, 0) + # switch to shotgun model index + child_idx = proxy_model.mapToSource(child_idx_proxy) + # resolve the index into an actual standarditem object + i = self._entity_presets[self._current_entity_preset].model.itemFromIndex(child_idx) + child_folders.append(i) - else: - # we got a specific item to process! - - # now get the proxy model level item instead - this way we can take search into - # account as we show the folder listings. - root_model_idx = item.index() - root_model_idx_proxy = proxy_model.mapFromSource(root_model_idx) - num_children = proxy_model.rowCount(root_model_idx_proxy) - - # get all the folder children - these need to be displayed - # by the model as folders - - for x in range(num_children): - # get the (proxy model) index for the child - child_idx_proxy = root_model_idx_proxy.child(x, 0) - # switch to shotgun model index - child_idx = proxy_model.mapToSource(child_idx_proxy) - # resolve the index into an actual standarditem object - i = self._entity_presets[self._current_entity_preset].model.itemFromIndex(child_idx) - child_folders.append(i) - - # Is the show child folders checked? - # The hierarchy model cannot handle "Show items in subfolders" mode. - show_sub_items = self.ui.show_sub_items.isChecked() and \ - not isinstance(self._entity_presets[self._current_entity_preset].model, SgHierarchyModel) - - if show_sub_items: - # indicate this with a special background color - self.ui.publish_view.setStyleSheet("#publish_view { background-color: rgba(44, 147, 226, 20%); }") - if len(child_folders) > 0: - # delegates are rendered in a special way - # if we are on a non-leaf node in the tree (e.g there are subfolders) - self._publish_thumb_delegate.set_sub_items_mode(True) - self._publish_list_delegate.set_sub_items_mode(True) else: - # we are at leaf level and the subitems check box is checked - # render the cells + # we got a specific item to process! + + # now get the proxy model level item instead - this way we can take search into + # account as we show the folder listings. + root_model_idx = item.index() + root_model_idx_proxy = proxy_model.mapFromSource(root_model_idx) + num_children = proxy_model.rowCount(root_model_idx_proxy) + + # get all the folder children - these need to be displayed + # by the model as folders + + for x in range(num_children): + # get the (proxy model) index for the child + child_idx_proxy = root_model_idx_proxy.child(x, 0) + # switch to shotgun model index + child_idx = proxy_model.mapToSource(child_idx_proxy) + # resolve the index into an actual standarditem object + i = self._entity_presets[self._current_entity_preset].model.itemFromIndex(child_idx) + child_folders.append(i) + + # Is the show child folders checked? + # The hierarchy model cannot handle "Show items in subfolders" mode. + show_sub_items = self.ui.show_sub_items.isChecked() and \ + not isinstance(self._entity_presets[self._current_entity_preset].model, SgHierarchyModel) + + if show_sub_items: + # indicate this with a special background color + self.ui.publish_view.setStyleSheet("#publish_view { background-color: rgba(44, 147, 226, 20%); }") + if len(child_folders) > 0: + # delegates are rendered in a special way + # if we are on a non-leaf node in the tree (e.g there are subfolders) + self._publish_thumb_delegate.set_sub_items_mode(True) + self._publish_list_delegate.set_sub_items_mode(True) + else: + # we are at leaf level and the subitems check box is checked + # render the cells + self._publish_thumb_delegate.set_sub_items_mode(False) + self._publish_list_delegate.set_sub_items_mode(False) + else: + self.ui.publish_view.setStyleSheet("") self._publish_thumb_delegate.set_sub_items_mode(False) self._publish_list_delegate.set_sub_items_mode(False) - else: - self.ui.publish_view.setStyleSheet("") - self._publish_thumb_delegate.set_sub_items_mode(False) - self._publish_list_delegate.set_sub_items_mode(False) - # now finally load up the data in the publish model - publish_filters = self._entity_presets[self._current_entity_preset].publish_filters - self._publish_model.load_data(item, child_folders, show_sub_items, publish_filters) + # now finally load up the data in the publish model + publish_filters = self._entity_presets[self._current_entity_preset].publish_filters + + self._publish_model.load_data(items, child_folders, show_sub_items, publish_filters) def _populate_entity_breadcrumbs(self, selected_item): """ @@ -1819,6 +1867,21 @@ def _populate_entity_breadcrumbs(self, selected_item): self.ui.entity_breadcrumbs.setText("%s" % breadcrumbs) + def _log_debug(self, msg): + """ + Convenience wrapper around debug logging + + :param msg: debug message + """ + self._bundle.log_debug("[%s] %s" % (self.__class__.__name__, msg)) + + def _log_info(self, msg): + """ + Convenience wrapper around debug logging + + :param msg: debug message + """ + self._bundle.log_info("[%s] %s" % (self.__class__.__name__, msg)) ################################################################################################ # Helper stuff diff --git a/python/tk_multi_loader/model_latestpublish.py b/python/tk_multi_loader/model_latestpublish.py index 4f55d000..4e79cb0b 100644 --- a/python/tk_multi_loader/model_latestpublish.py +++ b/python/tk_multi_loader/model_latestpublish.py @@ -39,6 +39,7 @@ def __init__(self, parent, publish_type_model, bg_task_manager): """ Model which represents the latest publishes for an entity """ + self._bundle = sgtk.platform.current_bundle() self._publish_type_model = publish_type_model self._folder_icon = QtGui.QIcon(QtGui.QPixmap(":/res/folder_512x400.png")) self._loading_icon = QtGui.QIcon(QtGui.QPixmap(":/res/loading_512x400.png")) @@ -66,7 +67,7 @@ def get_associated_tree_view_item(self, item): entity_item_hash = item.data(self.ASSOCIATED_TREE_VIEW_ITEM_ROLE) return self._associated_items.get(entity_item_hash) - def load_data(self, item, child_folders, show_sub_items, additional_sg_filters): + def load_data(self, items, child_folders, show_sub_items, additional_sg_filters): """ Clears the model and sets it up for a particular entity. Loads any cached data that exists. @@ -79,17 +80,17 @@ def load_data(self, item, child_folders, show_sub_items, additional_sg_filters): 'below' the selected item in Shotgun and hides any folders items. :param additional_sg_filters: List of shotgun filters to add to the shotgun query when retrieving publishes. """ - app = sgtk.platform.current_bundle() - if item is None: + if items is None: # nothing selected in the treeview # passing none to _load_data indicates that no query should be executed sg_filters = None - else: - # we have a selection! - + elif isinstance(items, (list, tuple)): + # we have a multi selection! + # new logic for handling multiple entity selections + # for simplicity sake i have removed the sub-folder logic if show_sub_items: # special mode -- in this case we don't show any of the # child folders and only the partial matches of all the leaf nodes @@ -120,10 +121,14 @@ def load_data(self, item, child_folders, show_sub_items, additional_sg_filters): # note that for tasks, we link via the task field # rather than the std entity link field # + + # New sg_filter for library tags (CustomEntity14). We need to pull the tag applied to the Version associated with the publish + # In the context of a media library it should be assumed that any PublishedFile WILL have a Version associated with it. + # if entity_type == "Task": sg_filters = [["task", "in", data]] - elif entity_type == "Version": - sg_filters = [["version", "in", data]] + elif entity_type == "CustomEntity14": + sg_filters = [["version.Version.sg_library_tags", "in", data ]] else: sg_filters = [["entity", "in", data]] @@ -132,54 +137,60 @@ def load_data(self, item, child_folders, show_sub_items, additional_sg_filters): # but is switching into more of a paradigm of an inverse # database. Indicate the difference by not showing any folders child_folders = [] - else: # standard mode - show folders and items for the currently selected item # for leaf nodes and for tree nodes which are connected to an entity, # show matches. - # Extract the Shotgun data and field value from the node item. - (sg_data, field_value) = model_item_data.get_item_data(item) - - if sg_data: - # leaf node! - # show the items associated. Handle tasks - # via the task field instead of the entity field - if sg_data.get("type") == "Task": - sg_filters = [["task", "is", {"type": sg_data["type"], "id": sg_data["id"]}]] - elif sg_data.get("type") == "Version": - sg_filters = [["version", "is", {"type": "Version", "id": sg_data["id"]}]] - else: - sg_filters = [["entity", "is", {"type": sg_data["type"], "id": sg_data["id"]} ]] - - else: - # intermediate node. - - if isinstance(field_value, dict) and "name" in field_value and "type" in field_value: - # this is an intermediate node like a sequence or an asset which - # can have publishes of its own associated - sg_filters = [["entity", "is", field_value ]] + # because we might have multiple entities selected, we need to create a list to hold all our filters + sg_filters = [] + + # loop through the selected items + for item in items: + # Extract the Shotgun data and field value from the node item. + (sg_data, field_value) = model_item_data.get_item_data(item) + + if sg_data: + # leaf node! + # show the items associated. Handle tasks + # via the task field instead of the entity field + # this only applies for library_tag entities of customentity14 + if sg_data.get("type") == "Task": + sg_filters.append(["task", "is", {"type": sg_data["type"], "id": sg_data["id"]} ]) + elif sg_data.get("type") == "CustomEntity14": + # as the CustomEntity 14 is the only case where multiselection is enabled, this is the only case + # where we need to append filters. The other cases simply set the filter array. + sg_filters.append(["version.Version.sg_library_tags", "in", {"type": sg_data["type"], "id": sg_data["id"]} ]) + else: + sg_filters.append(["entity", "is", {"type": sg_data["type"], "id": sg_data["id"]} ]) else: - # this is an intermediate node like status or asset type which does not - # have any publishes of its own, because the value (e.g. the status or the asset type) - # is nothing that you could link up a publish to. - sg_filters = None + # intermediate node. + + if isinstance(field_value, dict) and "name" in field_value and "type" in field_value: + # this is an intermediate node like a sequence or an asset which + # can have publishes of its own associated + sg_filters = [["entity", "is", field_value ]] + else: + # this is an intermediate node like status or asset type which does not + # have any publishes of its own, because the value (e.g. the status or the asset type) + # is nothing that you could link up a publish to. + sg_filters = None # now if sg_filters is not None (None indicates that no data should be fetched by the model), # add our external filter settings if sg_filters: # first apply any global sg filters, as specified in the config that we should append # to the main entity filters before getting publishes from shotgun. This may be stuff - # like 'only status approved' + # like 'only status appproved' pub_filters = app.get_setting("publish_filters", []) sg_filters.extend(pub_filters) - + # now, on top of that, apply any session specific filters # these typically come from the treeview and are pulled from a per-tab config setting, # allowing users to configure tabs with different publish filters, so that one - # tab can contain approved shot publishes, another can contain only items from + # tab can contain approved shot publishes, another can contain only items from # your current department, etc. sg_filters.extend(additional_sg_filters) @@ -536,3 +547,20 @@ def _before_data_processing(self, sg_data_list): self._publish_type_model.set_active_types( type_id_aggregates ) return new_sg_data + + def _log_debug(self, msg): + """ + Convenience wrapper around debug logging + + :param msg: debug message + """ + self._bundle.log_debug("[%s] %s" % (self.__class__.__name__, msg)) + + def _log_info(self, msg): + """ + Convenience wrapper around debug logging + + :param msg: debug message + """ + self._bundle.log_info("[%s] %s" % (self.__class__.__name__, msg)) + From f938cfc39384fd2560904d9e10321e53272ba23f Mon Sep 17 00:00:00 2001 From: "patrick.macdonald" Date: Thu, 8 Feb 2018 12:50:26 +0000 Subject: [PATCH 2/4] Standard Tag entity query behaviour implemented in dialog.py and model_latestPublish.py. Various dialog.py functions adapted to support Multiselection in entity tab view. _populate_entity_breadcrumbs() call moved inside multi-selection item loop. --- python/tk_multi_loader/dialog.py | 92 +++++++++++++++++-- python/tk_multi_loader/model_latestpublish.py | 19 ++-- 2 files changed, 93 insertions(+), 18 deletions(-) diff --git a/python/tk_multi_loader/dialog.py b/python/tk_multi_loader/dialog.py index f14aad54..0a14a4f8 100644 --- a/python/tk_multi_loader/dialog.py +++ b/python/tk_multi_loader/dialog.py @@ -1233,9 +1233,7 @@ def _load_entity_presets(self): sg_entity_type = setting_dict["entity_type"] # Check to see if we are showing a tags view. - # Library tags are a new entity so have a temporary entity name of "CustomEntity14" - # I suggest we create a new project specific tag field as part of a default SG project called 'project_tag' - if sg_entity_type == 'CustomEntity14': + if sg_entity_type == 'Tag': type_tag = True else: type_tag = False @@ -1276,10 +1274,8 @@ def _load_entity_presets(self): # Enable multiselection on tag list entities (or in this case extended selection so selections are sticky) if type_tag: - self._log_info('view.setSelectionMode(QtGui.QAbstractItemView.ExtendedSelection)') view.setSelectionMode(QtGui.QAbstractItemView.MultiSelection) else: - self._log_info('view.setSelectionMode(QtGui.QAbstractItemView.SingleSelection)') view.setSelectionMode(QtGui.QAbstractItemView.SingleSelection) # Keep a handle to all the new Qt objects, otherwise the GC may not work. @@ -1648,6 +1644,7 @@ def _switch_profile_tab(self, new_index, track_in_history): :param track_in_history: Hint to this method that the actions should be tracked in the history. """ + # qt returns unicode/qstring here so force to str curr_tab_name = shotgun_model.sanitize_qt(self.ui.entity_preset_tabs.tabText(new_index)) @@ -1683,7 +1680,7 @@ def _switch_profile_tab(self, new_index, track_in_history): self._setup_details_panel([]) # tell the publish view to change - self._load_publishes_for_entity_items([selected_item]) + self._load_publishes_for_entity_item(selected_item) def _on_treeview_item_selected(self): """ @@ -1691,8 +1688,7 @@ def _on_treeview_item_selected(self): """ selected_items = self._get_selected_entities() - # update breadcrumbs - self._populate_entity_breadcrumbs(selected_item) + # when an item in the treeview is selected, the child # nodes are displayed in the main view, so make sure @@ -1701,6 +1697,9 @@ def _on_treeview_item_selected(self): multi_selection_filters = [] for selected_item in selected_items: + # update breadcrumbs + self._populate_entity_breadcrumbs(selected_item) + selected_item_filters = model.get_filters(selected_item) for f in selected_item_filters: if f not in multi_selection_filters: @@ -1718,6 +1717,82 @@ def _on_treeview_item_selected(self): # tell publish UI to update itself self._load_publishes_for_entity_items(selected_items) + def _load_publishes_for_entity_item(self, item): + """ + Given an item from the treeview, or None if no item + is selected, prepare the publish area UI. + """ + # clear selection. If we don't clear the model at this point, + # the selection model will attempt to pair up with the model is + # data is being loaded in, resulting in many many events + self.ui.publish_view.selectionModel().clear() + + # Determine the child folders. + child_folders = [] + proxy_model = self._entity_presets[self._current_entity_preset].proxy_model + + if item is None: + # nothing is selected, bring in all the top level + # objects in the current tab + num_children = proxy_model.rowCount() + + for x in range(num_children): + # get the (proxy model) index for the child + child_idx_proxy = proxy_model.index(x, 0) + # switch to shotgun model index + child_idx = proxy_model.mapToSource(child_idx_proxy) + # resolve the index into an actual standarditem object + i = self._entity_presets[self._current_entity_preset].model.itemFromIndex(child_idx) + child_folders.append(i) + + else: + # we got a specific item to process! + + # now get the proxy model level item instead - this way we can take search into + # account as we show the folder listings. + root_model_idx = item.index() + root_model_idx_proxy = proxy_model.mapFromSource(root_model_idx) + num_children = proxy_model.rowCount(root_model_idx_proxy) + + # get all the folder children - these need to be displayed + # by the model as folders + + for x in range(num_children): + # get the (proxy model) index for the child + child_idx_proxy = root_model_idx_proxy.child(x, 0) + # switch to shotgun model index + child_idx = proxy_model.mapToSource(child_idx_proxy) + # resolve the index into an actual standarditem object + i = self._entity_presets[self._current_entity_preset].model.itemFromIndex(child_idx) + child_folders.append(i) + + # Is the show child folders checked? + # The hierarchy model cannot handle "Show items in subfolders" mode. + show_sub_items = self.ui.show_sub_items.isChecked() and \ + not isinstance(self._entity_presets[self._current_entity_preset].model, SgHierarchyModel) + + if show_sub_items: + # indicate this with a special background color + self.ui.publish_view.setStyleSheet("#publish_view { background-color: rgba(44, 147, 226, 20%); }") + if len(child_folders) > 0: + # delegates are rendered in a special way + # if we are on a non-leaf node in the tree (e.g there are subfolders) + self._publish_thumb_delegate.set_sub_items_mode(True) + self._publish_list_delegate.set_sub_items_mode(True) + else: + # we are at leaf level and the subitems check box is checked + # render the cells + self._publish_thumb_delegate.set_sub_items_mode(False) + self._publish_list_delegate.set_sub_items_mode(False) + else: + self.ui.publish_view.setStyleSheet("") + self._publish_thumb_delegate.set_sub_items_mode(False) + self._publish_list_delegate.set_sub_items_mode(False) + + # now finally load up the data in the publish model + publish_filters = self._entity_presets[self._current_entity_preset].publish_filters + self._publish_model.load_data(item, child_folders, show_sub_items, publish_filters) + def _load_publishes_for_entity_items(self, items): """ Given an item from the treeview, or None if no item @@ -1803,7 +1878,6 @@ def _populate_entity_breadcrumbs(self, selected_item): :param selected_item: Item currently selected in the tree view or `None` when no selection has been made. """ - crumbs = [] if selected_item: diff --git a/python/tk_multi_loader/model_latestpublish.py b/python/tk_multi_loader/model_latestpublish.py index 4e79cb0b..5831e801 100644 --- a/python/tk_multi_loader/model_latestpublish.py +++ b/python/tk_multi_loader/model_latestpublish.py @@ -122,13 +122,13 @@ def load_data(self, items, child_folders, show_sub_items, additional_sg_filters) # rather than the std entity link field # - # New sg_filter for library tags (CustomEntity14). We need to pull the tag applied to the Version associated with the publish + # New sg_filter for tags. We need to pull the tag applied to the Version associated with the publish # In the context of a media library it should be assumed that any PublishedFile WILL have a Version associated with it. - # + # We may need to add logic to cover cases where the published file has no version. if entity_type == "Task": sg_filters = [["task", "in", data]] - elif entity_type == "CustomEntity14": - sg_filters = [["version.Version.sg_library_tags", "in", data ]] + elif entity_type == "Tag": + sg_filters = [["version.Version.tags", "in", data ]] else: sg_filters = [["entity", "in", data]] @@ -154,13 +154,14 @@ def load_data(self, items, child_folders, show_sub_items, additional_sg_filters) # leaf node! # show the items associated. Handle tasks # via the task field instead of the entity field - # this only applies for library_tag entities of customentity14 + # handle tags via the publish file's Version's tags field. (Tags should always be applied + # to the Version rather than the PublishedFile as we also want to be able to filter by tag + # from the media page (which is shows only versions and as the published_files field in + # versions is ulti-entity, we cant filter by published_files.PublishedFile.tag) if sg_data.get("type") == "Task": sg_filters.append(["task", "is", {"type": sg_data["type"], "id": sg_data["id"]} ]) - elif sg_data.get("type") == "CustomEntity14": - # as the CustomEntity 14 is the only case where multiselection is enabled, this is the only case - # where we need to append filters. The other cases simply set the filter array. - sg_filters.append(["version.Version.sg_library_tags", "in", {"type": sg_data["type"], "id": sg_data["id"]} ]) + elif sg_data.get("type") == "Tag": + sg_filters.append(["version.Version.tags", "in", {"type": sg_data["type"], "id": sg_data["id"]} ]) else: sg_filters.append(["entity", "is", {"type": sg_data["type"], "id": sg_data["id"]} ]) From 4d7fcb340c50e4882364d9bd3e8280bffb56a67e Mon Sep 17 00:00:00 2001 From: "patrick.macdonald" Date: Fri, 9 Feb 2018 14:22:13 +0000 Subject: [PATCH 3/4] Merged with v1.18.3 --- python/tk_multi_loader/dialog.py | 78 +------------------ python/tk_multi_loader/model_latestpublish.py | 75 ++++++++++-------- 2 files changed, 45 insertions(+), 108 deletions(-) diff --git a/python/tk_multi_loader/dialog.py b/python/tk_multi_loader/dialog.py index 0a14a4f8..1980705a 100644 --- a/python/tk_multi_loader/dialog.py +++ b/python/tk_multi_loader/dialog.py @@ -1680,7 +1680,7 @@ def _switch_profile_tab(self, new_index, track_in_history): self._setup_details_panel([]) # tell the publish view to change - self._load_publishes_for_entity_item(selected_item) + self._load_publishes_for_entity_items([selected_item]) def _on_treeview_item_selected(self): """ @@ -1717,82 +1717,6 @@ def _on_treeview_item_selected(self): # tell publish UI to update itself self._load_publishes_for_entity_items(selected_items) - def _load_publishes_for_entity_item(self, item): - """ - Given an item from the treeview, or None if no item - is selected, prepare the publish area UI. - """ - # clear selection. If we don't clear the model at this point, - # the selection model will attempt to pair up with the model is - # data is being loaded in, resulting in many many events - self.ui.publish_view.selectionModel().clear() - - # Determine the child folders. - child_folders = [] - proxy_model = self._entity_presets[self._current_entity_preset].proxy_model - - if item is None: - # nothing is selected, bring in all the top level - # objects in the current tab - num_children = proxy_model.rowCount() - - for x in range(num_children): - # get the (proxy model) index for the child - child_idx_proxy = proxy_model.index(x, 0) - # switch to shotgun model index - child_idx = proxy_model.mapToSource(child_idx_proxy) - # resolve the index into an actual standarditem object - i = self._entity_presets[self._current_entity_preset].model.itemFromIndex(child_idx) - child_folders.append(i) - - else: - # we got a specific item to process! - - # now get the proxy model level item instead - this way we can take search into - # account as we show the folder listings. - root_model_idx = item.index() - root_model_idx_proxy = proxy_model.mapFromSource(root_model_idx) - num_children = proxy_model.rowCount(root_model_idx_proxy) - - # get all the folder children - these need to be displayed - # by the model as folders - - for x in range(num_children): - # get the (proxy model) index for the child - child_idx_proxy = root_model_idx_proxy.child(x, 0) - # switch to shotgun model index - child_idx = proxy_model.mapToSource(child_idx_proxy) - # resolve the index into an actual standarditem object - i = self._entity_presets[self._current_entity_preset].model.itemFromIndex(child_idx) - child_folders.append(i) - - # Is the show child folders checked? - # The hierarchy model cannot handle "Show items in subfolders" mode. - show_sub_items = self.ui.show_sub_items.isChecked() and \ - not isinstance(self._entity_presets[self._current_entity_preset].model, SgHierarchyModel) - - if show_sub_items: - # indicate this with a special background color - self.ui.publish_view.setStyleSheet("#publish_view { background-color: rgba(44, 147, 226, 20%); }") - if len(child_folders) > 0: - # delegates are rendered in a special way - # if we are on a non-leaf node in the tree (e.g there are subfolders) - self._publish_thumb_delegate.set_sub_items_mode(True) - self._publish_list_delegate.set_sub_items_mode(True) - else: - # we are at leaf level and the subitems check box is checked - # render the cells - self._publish_thumb_delegate.set_sub_items_mode(False) - self._publish_list_delegate.set_sub_items_mode(False) - else: - self.ui.publish_view.setStyleSheet("") - self._publish_thumb_delegate.set_sub_items_mode(False) - self._publish_list_delegate.set_sub_items_mode(False) - - # now finally load up the data in the publish model - publish_filters = self._entity_presets[self._current_entity_preset].publish_filters - self._publish_model.load_data(item, child_folders, show_sub_items, publish_filters) - def _load_publishes_for_entity_items(self, items): """ Given an item from the treeview, or None if no item diff --git a/python/tk_multi_loader/model_latestpublish.py b/python/tk_multi_loader/model_latestpublish.py index 5831e801..1c310925 100644 --- a/python/tk_multi_loader/model_latestpublish.py +++ b/python/tk_multi_loader/model_latestpublish.py @@ -122,11 +122,15 @@ def load_data(self, items, child_folders, show_sub_items, additional_sg_filters) # rather than the std entity link field # - # New sg_filter for tags. We need to pull the tag applied to the Version associated with the publish - # In the context of a media library it should be assumed that any PublishedFile WILL have a Version associated with it. + # New sg_filter for tags. We need to pull the tag applied + # to the Version associated with the publish + # In the context of a media library it should be assumed + # that any PublishedFile WILL have a Version associated with it. # We may need to add logic to cover cases where the published file has no version. if entity_type == "Task": sg_filters = [["task", "in", data]] + elif entity_type == "Version": + sg_filters = [["version", "in", data]] elif entity_type == "Tag": sg_filters = [["version.Version.tags", "in", data ]] else: @@ -147,44 +151,53 @@ def load_data(self, items, child_folders, show_sub_items, additional_sg_filters) # loop through the selected items for item in items: - # Extract the Shotgun data and field value from the node item. - (sg_data, field_value) = model_item_data.get_item_data(item) - - if sg_data: - # leaf node! - # show the items associated. Handle tasks - # via the task field instead of the entity field - # handle tags via the publish file's Version's tags field. (Tags should always be applied - # to the Version rather than the PublishedFile as we also want to be able to filter by tag - # from the media page (which is shows only versions and as the published_files field in - # versions is ulti-entity, we cant filter by published_files.PublishedFile.tag) - if sg_data.get("type") == "Task": - sg_filters.append(["task", "is", {"type": sg_data["type"], "id": sg_data["id"]} ]) - elif sg_data.get("type") == "Tag": - sg_filters.append(["version.Version.tags", "in", {"type": sg_data["type"], "id": sg_data["id"]} ]) - else: - sg_filters.append(["entity", "is", {"type": sg_data["type"], "id": sg_data["id"]} ]) - + if item is None: + # nothing selected in the treeview + # passing none to _load_data indicates that no query should be executed + sg_filters = None else: - # intermediate node. - - if isinstance(field_value, dict) and "name" in field_value and "type" in field_value: - # this is an intermediate node like a sequence or an asset which - # can have publishes of its own associated - sg_filters = [["entity", "is", field_value ]] + # Extract the Shotgun data and field value from the node item. + (sg_data, field_value) = model_item_data.get_item_data(item) + + if sg_data: + # leaf node! + # show the items associated. Handle tasks + # via the task field instead of the entity field + # handle tags via the publish file's Version's tags field. (Tags should always be applied + # to the Version rather than the PublishedFile as we also want to be able to filter by tag + # from the media page (which is shows only versions and as the published_files field in + # versions is ulti-entity, we cant filter by published_files.PublishedFile.tag) + if sg_data.get("type") == "Task": + sg_filters.append(["task", "is", {"type": sg_data["type"], "id": sg_data["id"]} ]) + elif sg_data.get("type") == "Version": + sg_filters = [["version", "is", {"type": "Version", "id": sg_data["id"]}]] + elif sg_data.get("type") == "Tag": + sg_filters.append(["version.Version.tags", "in", {"type": sg_data["type"], "id": sg_data["id"]} ]) + # the folder paradigm isn't relevant to tag view, so hide the child folders. + child_folders = [] + else: + sg_filters.append(["entity", "is", {"type": sg_data["type"], "id": sg_data["id"]} ]) else: - # this is an intermediate node like status or asset type which does not - # have any publishes of its own, because the value (e.g. the status or the asset type) - # is nothing that you could link up a publish to. - sg_filters = None + # intermediate node. + + if isinstance(field_value, dict) and "name" in field_value and "type" in field_value: + # this is an intermediate node like a sequence or an asset which + # can have publishes of its own associated + sg_filters = [["entity", "is", field_value ]] + + else: + # this is an intermediate node like status or asset type which does not + # have any publishes of its own, because the value (e.g. the status or the asset type) + # is nothing that you could link up a publish to. + sg_filters = None # now if sg_filters is not None (None indicates that no data should be fetched by the model), # add our external filter settings if sg_filters: # first apply any global sg filters, as specified in the config that we should append # to the main entity filters before getting publishes from shotgun. This may be stuff - # like 'only status appproved' + # like 'only status approved' pub_filters = app.get_setting("publish_filters", []) sg_filters.extend(pub_filters) From e41cf2d039af20bf778744950df6cad774250e1c Mon Sep 17 00:00:00 2001 From: "patrick.macdonald" Date: Fri, 16 Feb 2018 10:47:26 +0000 Subject: [PATCH 4/4] dialog.py:- - legacy "_get_selected_entity()" function removed and replaced with "_get_selected_entities()". This works for single and multiple selection modes. - _get_selected_entity calls replaced with _get_selected_entities[0] where appropriate. - Treeview selected logic updated to handle nothing in the selection (refreshes the model) and multiple selections. model_latestPublish.py:- - app decleration changed to class member to allow access from other functions. Functions' calls to 'app' replaced with 'self.app'. 'app' only needs to be declared once at __init__ - show_sub_items behaviour was broken by the previous commit. This has now been fixed and performs as expected on both Tag and other entity types. - self._bundle var removed in favour of the duplicate self.app --- python/tk_multi_loader/dialog.py | 79 ++++------- python/tk_multi_loader/model_latestpublish.py | 129 +++++++++--------- 2 files changed, 92 insertions(+), 116 deletions(-) diff --git a/python/tk_multi_loader/dialog.py b/python/tk_multi_loader/dialog.py index 1980705a..4f0c91c0 100644 --- a/python/tk_multi_loader/dialog.py +++ b/python/tk_multi_loader/dialog.py @@ -924,7 +924,7 @@ def _on_show_subitems_toggled(self): help_screen.show_help_screen(self.window(), app, help_pix) # tell publish UI to update itself - item = self._get_selected_entity() + item = self._get_selected_entities()[0] self._load_publishes_for_entity_items([item]) def _on_thumb_size_slider_change(self, value): @@ -1047,35 +1047,12 @@ def _on_reload_action(self): ######################################################################################## # entity listing tree view and presets toolbar - def _get_selected_entity(self): - """ - Returns the item currently selected in the tree view, None - if no selection has been made. - """ - selected_item = None - selection_model = self._entity_presets[self._current_entity_preset].view.selectionModel() - if selection_model.hasSelection(): - - current_idx = selection_model.selection().indexes()[0] - - model = current_idx.model() - - if not isinstance(model, (SgHierarchyModel, SgEntityModel)): - # proxy model! - current_idx = model.mapToSource(current_idx) - - # now we have arrived at our model derived from StandardItemModel - # so let's retrieve the standarditem object associated with the index - selected_item = current_idx.model().itemFromIndex(current_idx) - - return selected_item - def _get_selected_entities(self): """ Returns the items currently selected in the tree view, None if no selection has been made. """ - selected_items = None + selected_items = [None] selection_model = self._entity_presets[self._current_entity_preset].view.selectionModel() if selection_model.hasSelection(): @@ -1146,7 +1123,7 @@ def _select_item_in_entity_tree(self, tab_caption, item): # to selected is in vertically centered in the widget # get the currently selected item in our tab - selected_item = self._get_selected_entity() + selected_item = self._get_selected_entities()[0] if selected_item and selected_item.index() == item.index(): # the item is already selected! @@ -1532,7 +1509,7 @@ def _hierarchy_refreshed(self): Slot triggered when the hierarchy model has been refreshed. This allows to show all the folder items in the right-hand side for the current selection. """ - selected_item = self._get_selected_entity() + selected_item = self._get_selected_entities()[0] # tell publish UI to update itself self._load_publishes_for_entity_items([selected_item]) @@ -1668,7 +1645,7 @@ def _switch_profile_tab(self, new_index, track_in_history): if track_in_history: # figure out what is selected - selected_item = self._get_selected_entity() + selected_item = self._get_selected_entities()[0] # update breadcrumbs self._populate_entity_breadcrumbs(selected_item) @@ -1688,34 +1665,38 @@ def _on_treeview_item_selected(self): """ selected_items = self._get_selected_entities() - - - # when an item in the treeview is selected, the child - # nodes are displayed in the main view, so make sure - # they are loaded. model = self._entity_presets[self._current_entity_preset].model - multi_selection_filters = [] - for selected_item in selected_items: - # update breadcrumbs - self._populate_entity_breadcrumbs(selected_item) + # If nothing is selected, refresh the view. + if len(selected_items) == 0: + if isinstance(model, SgEntityModel): + model.async_refresh() + else: + # when an item in the treeview is selected, the child + # nodes are displayed in the main view, so make sure + # they are loaded. - selected_item_filters = model.get_filters(selected_item) - for f in selected_item_filters: - if f not in multi_selection_filters: - multi_selection_filters.append(f) + multi_selection_filters = [] + for selected_item in selected_items: + # update breadcrumbs + self._populate_entity_breadcrumbs(selected_item) - if selected_item and model.canFetchMore(selected_item.index()): - model.fetchMore(selected_item.index()) + selected_item_filters = model.get_filters(selected_item) + for f in selected_item_filters: + if f not in multi_selection_filters: + multi_selection_filters.append(f) - # notify history - self._add_history_record(self._current_entity_preset, selected_item) + if selected_item and model.canFetchMore(selected_item.index()): + model.fetchMore(selected_item.index()) - # tell details panel to clear itself - self._setup_details_panel([]) + # notify history + self._add_history_record(self._current_entity_preset, selected_item) - # tell publish UI to update itself - self._load_publishes_for_entity_items(selected_items) + # tell details panel to clear itself + self._setup_details_panel([]) + + # tell publish UI to update itself + self._load_publishes_for_entity_items(selected_items) def _load_publishes_for_entity_items(self, items): """ diff --git a/python/tk_multi_loader/model_latestpublish.py b/python/tk_multi_loader/model_latestpublish.py index 1c310925..f87d2368 100644 --- a/python/tk_multi_loader/model_latestpublish.py +++ b/python/tk_multi_loader/model_latestpublish.py @@ -39,18 +39,17 @@ def __init__(self, parent, publish_type_model, bg_task_manager): """ Model which represents the latest publishes for an entity """ - self._bundle = sgtk.platform.current_bundle() self._publish_type_model = publish_type_model self._folder_icon = QtGui.QIcon(QtGui.QPixmap(":/res/folder_512x400.png")) self._loading_icon = QtGui.QIcon(QtGui.QPixmap(":/res/loading_512x400.png")) self._associated_items = {} - app = sgtk.platform.current_bundle() + self.app = sgtk.platform.current_bundle() # init base class ShotgunModel.__init__(self, parent, - download_thumbs=app.get_setting("download_thumbnails"), + download_thumbs=self.app.get_setting("download_thumbnails"), schema_generation=6, bg_load_thumbs=True, bg_task_manager=bg_task_manager) @@ -80,69 +79,67 @@ def load_data(self, items, child_folders, show_sub_items, additional_sg_filters) 'below' the selected item in Shotgun and hides any folders items. :param additional_sg_filters: List of shotgun filters to add to the shotgun query when retrieving publishes. """ - app = sgtk.platform.current_bundle() - - if items is None: + if len(items) == 0: # nothing selected in the treeview # passing none to _load_data indicates that no query should be executed - sg_filters = None + sg_filters = [] - elif isinstance(items, (list, tuple)): + else: #if isinstance(items, (list, tuple)): # we have a multi selection! - # new logic for handling multiple entity selections - # for simplicity sake i have removed the sub-folder logic + if show_sub_items: - # special mode -- in this case we don't show any of the - # child folders and only the partial matches of all the leaf nodes - - # for example, this may return - # entity type shot, [["sequence", "is", "xxx"]] or - # entity type shot, [["status", "is", "ip"]] or - - # note! Because of nasty bug https://bugreports.qt-project.org/browse/PYSIDE-158, - # we cannot pull the model directly from the item but have to pull it from - # the model index instead. - model_idx = item.index() - model = model_idx.model() - partial_filters = model.get_filters(item) - entity_type = model.get_entity_type() - - # now get a list of matches from the above query from - # shotgun - note that this is a synchronous call so - # it may 'pause' execution briefly for the user - data = app.shotgun.find(entity_type, partial_filters) - - - # now create the final query for the model - this will be - # a big in statement listing all the ids returned from - # the previous query, asking the model to only show the - # items matching the previous query. - # - # note that for tasks, we link via the task field - # rather than the std entity link field - # - - # New sg_filter for tags. We need to pull the tag applied - # to the Version associated with the publish - # In the context of a media library it should be assumed - # that any PublishedFile WILL have a Version associated with it. - # We may need to add logic to cover cases where the published file has no version. - if entity_type == "Task": - sg_filters = [["task", "in", data]] - elif entity_type == "Version": - sg_filters = [["version", "in", data]] - elif entity_type == "Tag": - sg_filters = [["version.Version.tags", "in", data ]] - else: - sg_filters = [["entity", "in", data]] - - # lastly, when we are in this special mode, the main view - # is no longer functioning as a browsable hierarchy - # but is switching into more of a paradigm of an inverse - # database. Indicate the difference by not showing any folders - child_folders = [] + # loop through the selected items + for item in items: + # special mode -- in this case we don't show any of the + # child folders and only the partial matches of all the leaf nodes + + # for example, this may return + # entity type shot, [["sequence", "is", "xxx"]] or + # entity type shot, [["status", "is", "ip"]] or + + # note! Because of nasty bug https://bugreports.qt-project.org/browse/PYSIDE-158, + # we cannot pull the model directly from the item but have to pull it from + # the model index instead. + model_idx = item.index() + model = model_idx.model() + partial_filters = model.get_filters(item) + entity_type = model.get_entity_type() + + # now get a list of matches from the above query from + # shotgun - note that this is a synchronous call so + # it may 'pause' execution briefly for the user + data = self.app.shotgun.find(entity_type, partial_filters) + + # now create the final query for the model - this will be + # a big in statement listing all the ids returned from + # the previous query, asking the model to only show the + # items matching the previous query. + # + # note that for tasks, we link via the task field + # rather than the std entity link field + # + + # New sg_filter for tags. We need to pull the tag applied + # to the Version associated with the publish + # In the context of a media library it should be assumed + # that any PublishedFile WILL have a Version associated with it. + # We may need to add logic to cover cases where the published file has no version. + if entity_type == "Task": + sg_filters = [["task", "in", data]] + elif entity_type == "Version": + sg_filters = [["version", "in", data]] + elif entity_type == "Tag": + sg_filters = [["version.Version.tags", "in", data ]] + else: + sg_filters = [["entity", "in", data]] + + # lastly, when we are in this special mode, the main view + # is no longer functioning as a browsable hierarchy + # but is switching into more of a paradigm of an inverse + # database. Indicate the difference by not showing any folders + child_folders = [] else: - # standard mode - show folders and items for the currently selected item + # standard mode - show folders and items for the currently selected items # for leaf nodes and for tree nodes which are connected to an entity, # show matches. @@ -198,7 +195,7 @@ def load_data(self, items, child_folders, show_sub_items, additional_sg_filters) # first apply any global sg filters, as specified in the config that we should append # to the main entity filters before getting publishes from shotgun. This may be stuff # like 'only status approved' - pub_filters = app.get_setting("publish_filters", []) + pub_filters = self.app.get_setting("publish_filters", []) sg_filters.extend(pub_filters) # now, on top of that, apply any session specific filters @@ -266,8 +263,7 @@ def _do_load_data(self, sg_filters, treeview_folder_items): of folders and files. """ # first figure out which fields to get from shotgun - app = sgtk.platform.current_bundle() - publish_entity_type = sgtk.util.get_published_file_entity_type(app.tank) + publish_entity_type = sgtk.util.get_published_file_entity_type(self.app.tank) if publish_entity_type == "PublishedFile": self._publish_type_field = "published_file_type" @@ -461,11 +457,10 @@ def _before_data_processing(self, sg_data_list): :param sg_data_list: list of shotgun dictionaries, as returned by the find() call. :returns: should return a list of shotgun dictionaries, on the same form as the input. """ - app = sgtk.platform.current_bundle() # First, let the filter_publishes hook have a chance to filter the list # of publishes: - sg_data_list = utils.filter_publishes(app, sg_data_list) + sg_data_list = utils.filter_publishes(self.app, sg_data_list) # filter the shotgun data so that we only return the latest publish for each file. # also perform aggregate computations and push those summaries into the associated @@ -568,7 +563,7 @@ def _log_debug(self, msg): :param msg: debug message """ - self._bundle.log_debug("[%s] %s" % (self.__class__.__name__, msg)) + self.app.log_debug("[%s] %s" % (self.__class__.__name__, msg)) def _log_info(self, msg): """ @@ -576,5 +571,5 @@ def _log_info(self, msg): :param msg: debug message """ - self._bundle.log_info("[%s] %s" % (self.__class__.__name__, msg)) + self.app.log_info("[%s] %s" % (self.__class__.__name__, msg))