diff --git a/python/tk_multi_loader/dialog.py b/python/tk_multi_loader/dialog.py index a11ea1ab..4f0c91c0 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 @@ -923,8 +924,8 @@ 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() - self._load_publishes_for_entity_item(item) + item = self._get_selected_entities()[0] + self._load_publishes_for_entity_items([item]) def _on_thumb_size_slider_change(self, value): """ @@ -1046,29 +1047,29 @@ def _on_reload_action(self): ######################################################################################## # entity listing tree view and presets toolbar - def _get_selected_entity(self): + def _get_selected_entities(self): """ - Returns the item currently selected in the tree view, None + Returns the items currently selected in the tree view, None if no selection has been made. """ - - selected_item = None + selected_items = [None] selection_model = self._entity_presets[self._current_entity_preset].view.selectionModel() - if selection_model.hasSelection(): - current_idx = selection_model.selection().indexes()[0] + if selection_model.hasSelection(): + selection = selection_model.selection().indexes() - model = current_idx.model() + 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) + 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) + selected_item = current_idx.model().itemFromIndex(current_idx) + selected_items.append(selected_item) - return selected_item + return selected_items def _select_tab(self, tab_caption, track_in_history): """ @@ -1122,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! @@ -1208,6 +1209,12 @@ def _load_entity_presets(self): sg_entity_type = setting_dict["entity_type"] + # Check to see if we are showing a tags view. + if sg_entity_type == 'Tag': + 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 +1249,12 @@ 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: + view.setSelectionMode(QtGui.QAbstractItemView.MultiSelection) + else: + 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]) @@ -1496,10 +1509,10 @@ 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_item(selected_item) + self._load_publishes_for_entity_items([selected_item]) def _node_activated(self, incremental_paths, view, proxy_model): """ @@ -1608,6 +1621,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)) @@ -1631,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) @@ -1643,40 +1657,52 @@ 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_items = self._get_selected_entities() - selected_item = self._get_selected_entity() + model = self._entity_presets[self._current_entity_preset].model - # 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. - # 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 - if selected_item and model.canFetchMore(selected_item.index()): - model.fetchMore(selected_item.index()) + multi_selection_filters = [] + for selected_item in selected_items: + # update breadcrumbs + self._populate_entity_breadcrumbs(selected_item) - # notify history - self._add_history_record(self._current_entity_preset, selected_item) + 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) - # tell details panel to clear itself - self._setup_details_panel([]) + if selected_item and model.canFetchMore(selected_item.index()): + model.fetchMore(selected_item.index()) - # tell publish UI to update itself - self._load_publishes_for_entity_item(selected_item) + # notify history + self._add_history_record(self._current_entity_preset, selected_item) + + # tell details panel to clear itself + self._setup_details_panel([]) - def _load_publishes_for_entity_item(self, item): + # tell publish UI to update itself + self._load_publishes_for_entity_items(selected_items) + + 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 +1712,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): """ @@ -1755,7 +1783,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: @@ -1819,6 +1846,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..f87d2368 100644 --- a/python/tk_multi_loader/model_latestpublish.py +++ b/python/tk_multi_loader/model_latestpublish.py @@ -44,12 +44,12 @@ def __init__(self, parent, publish_type_model, bg_task_manager): 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) @@ -66,7 +66,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,93 +79,115 @@ 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 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 = [] - else: - # we have a selection! + else: #if isinstance(items, (list, tuple)): + # we have a multi selection! 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 - # - if entity_type == "Task": - sg_filters = [["task", "in", data]] - elif entity_type == "Version": - sg_filters = [["version", "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. - # 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. + # because we might have multiple entities selected, we need to create a list to hold all our filters + sg_filters = [] - 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. + # loop through the selected items + for item in items: + 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: + # 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: + # 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 @@ -173,13 +195,13 @@ def load_data(self, item, 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 # 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) @@ -241,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" @@ -436,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 @@ -536,3 +556,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.app.log_debug("[%s] %s" % (self.__class__.__name__, msg)) + + def _log_info(self, msg): + """ + Convenience wrapper around debug logging + + :param msg: debug message + """ + self.app.log_info("[%s] %s" % (self.__class__.__name__, msg)) +