diff --git a/hooks/tk-softimage_actions.py b/hooks/tk-softimage_actions.py new file mode 100644 index 00000000..0d8a76b2 --- /dev/null +++ b/hooks/tk-softimage_actions.py @@ -0,0 +1,80 @@ +# Copyright (c) 2013 Shotgun Software Inc. +# +# CONFIDENTIAL AND PROPRIETARY +# +# This work is provided "AS IS" and subject to the Shotgun Pipeline Toolkit +# Source Code License included in this distribution package. See LICENSE. +# By accessing, using, copying or modifying this work you indicate your +# agreement to the Shotgun Pipeline Toolkit Source Code License. All rights +# not expressly granted therein are reserved by Shotgun Software Inc. + +""" +Hook that loads defines all the available actions, broken down by publish type. +""" +import sgtk + +HookBaseClass = sgtk.get_hook_baseclass() + + +class SoftimageActions(HookBaseClass): + + ########################################################################################################## + # public interface - to be overridden by deriving classes + + def generate_actions(self, sg_publish_data, actions, ui_area): + """ + Returns a list of action instances for a particular publish. + This method is called each time a user clicks a publish somewhere in the UI. + The data returned from this hook will be used to populate the actions menu for a publish. + + The mapping between Publish types and actions are kept in a different place + (in the configuration) so at the point when this hook is called, the loader app + has already established *which* actions are appropriate for this object. + + The hook should return at least one action for each item passed in via the + actions parameter. + + This method needs to return detailed data for those actions, in the form of a list + of dictionaries, each with name, params, caption and description keys. + + Because you are operating on a particular publish, you may tailor the output + (caption, tooltip etc) to contain custom information suitable for this publish. + + The ui_area parameter is a string and indicates where the publish is to be shown. + - If it will be shown in the main browsing area, "main" is passed. + - If it will be shown in the details area, "details" is passed. + - If it will be shown in the history area, "history" is passed. + + Please note that it is perfectly possible to create more than one action "instance" for + an action! You can for example do scene introspection - if the action passed in + is "character_attachment" you may for example scan the scene, figure out all the nodes + where this object can be attached and return a list of action instances: + "attach to left hand", "attach to right hand" etc. In this case, when more than + one object is returned for an action, use the params key to pass additional + data into the run_action hook. + + :param sg_publish_data: Shotgun data dictionary with all the standard publish fields. + :param actions: List of action strings which have been defined in the app configuration. + :param ui_area: String denoting the UI Area (see above). + :returns List of dictionaries, each with keys name, params, caption and description + """ + app = self.parent + app.log_debug("Generate actions called for UI element %s. " + "Actions: %s. Publish Data: %s" % (ui_area, actions, sg_publish_data)) + + action_instances = [] + return action_instances + + def execute_action(self, name, params, sg_publish_data): + """ + Execute a given action. The data sent to this be method will + represent one of the actions enumerated by the generate_actions method. + + :param name: Action name string representing one of the items returned by generate_actions. + :param params: Params data, as specified by generate_actions. + :param sg_publish_data: Shotgun data dictionary with all the standard publish fields. + :returns: No return value expected. + """ + app = self.parent + app.log_debug("Execute action called for action %s. " + "Parameters: %s. Publish Data: %s" % (name, params, sg_publish_data)) diff --git a/info.yml b/info.yml index 8d8ec06e..d39605d7 100644 --- a/info.yml +++ b/info.yml @@ -143,3 +143,4 @@ requires_engine_version: frameworks: - {"name": "tk-framework-shotgunutils", "version": "v4.x.x"} - {"name": "tk-framework-qtwidgets", "version": "v2.x.x"} + - {"name": "tk-framework-rdo", 'version': "v0.0.x"} diff --git a/python/tk_multi_loader/delegate_publish_list.py b/python/tk_multi_loader/delegate_publish_list.py index b31c51bb..ee984f70 100644 --- a/python/tk_multi_loader/delegate_publish_list.py +++ b/python/tk_multi_loader/delegate_publish_list.py @@ -372,7 +372,8 @@ def _format_publish(self, model_index, widget): small_text = "%s by %s at %s" % (pub_type_str, author_str, date_str) - + small_text += "
Description: %s" % sg_data.get("description") or "No description given" + # and set a tooltip tooltip = "Name: %s" % (sg_data.get("code") or "No name given.") tooltip += "

Path: %s" % ((sg_data.get("path") or {}).get("local_path")) diff --git a/python/tk_multi_loader/delegate_publish_thumb.py b/python/tk_multi_loader/delegate_publish_thumb.py index da61b247..7ac9d446 100644 --- a/python/tk_multi_loader/delegate_publish_thumb.py +++ b/python/tk_multi_loader/delegate_publish_thumb.py @@ -260,7 +260,7 @@ def _on_before_paint(self, widget, model_index, style_options): # this is a publish! # example data: - + # {'code': 'aaa_00010_F004_C003_0228F8_v000.%04d.dpx', # 'created_at': 1425378837.0, # 'created_by': {'id': 42, 'name': 'Manne Ohrstrom', 'type': 'HumanUser'}, @@ -299,34 +299,11 @@ def _on_before_paint(self, widget, model_index, style_options): # 'version.Version.sg_status_list': 'rev', # 'version_number': 2} - # get the name (lighting v3) - name_str = "Unnamed" - if sg_data.get("name"): - name_str = sg_data.get("name") - - if sg_data.get("version_number"): - name_str += " v%s" % sg_data.get("version_number") - - # now we are tracking whether this item has a unique task/name/type combo - # or not via the specially injected task_uniqueness boolean. - # If this is true, that means that this is the only item in the listing - # with this name/type combo, and we can render its display name on two - # lines, name first and then type, e.g.: - # MyScene, v3 - # Maya Render - # - # However, there can be multiple *different* tasks which have the same - # name/type combo - in this case, we want to display the task name too - # since this is what differentiates the data. In that case we display it: - # MyScene, v3 (Layout) - # Maya Render - # - if sg_data.get("task_uniqueness") == False and sg_data.get("task") is not None: - name_str += " (%s)" % sg_data["task"]["name"] - + name_str = self._get_name_string(sg_data) + # make this the title of the card header_text = name_str - + # and set a tooltip tooltip = "Name: %s" % (sg_data.get("code") or "No name given.") # Version 012 by John Smith at 2014-02-23 10:34 @@ -366,12 +343,81 @@ def _on_before_paint(self, widget, model_index, style_options): # std publish - render with a name and a publish type # main_body v3 # Render - details_text = shotgun_model.get_sanitized_data(model_index, - SgLatestPublishModel.PUBLISH_TYPE_NAME_ROLE) + details_text = self._get_details_text(sg_data) - widget.set_text(header_text, details_text, tooltip) + def _get_name_string(self, sg_data): + # example data: + + # {'code': 'aaa_00010_F004_C003_0228F8_v000.%04d.dpx', + # 'created_at': 1425378837.0, + # 'created_by': {'id': 42, 'name': 'Manne Ohrstrom', 'type': 'HumanUser'}, + # 'created_by.HumanUser.image': 'https://...', + # 'description': 'testing testing, 1,2,3', + # 'entity': {'id': 1660, 'name': 'aaa_00010', 'type': 'Shot'}, + # 'id': 1340, + # 'image': 'https:...', + # 'name': 'aaa_00010, F004_C003_0228F8', + # 'path': {'content_type': 'image/dpx', + # 'id': 24116, + # 'link_type': 'local', + # 'local_path': '/mnt/projects...', + # 'local_path_linux': '/mnt/projects...', + # 'local_path_mac': '/mnt/projects...', + # 'local_path_windows': 'z:\\mnt\\projects...', + # 'local_storage': {'id': 4, + # 'name': 'primary', + # 'type': 'LocalStorage'}, + # 'name': 'aaa_00010_F004_C003_0228F8_v000.%04d.dpx', + # 'type': 'Attachment', + # 'url': 'file:///mnt/projects...'}, + # 'project': {'id': 289, 'name': 'Climp', 'type': 'Project'}, + # 'published_file_type': {'id': 53, + # 'name': 'Flame Render', + # 'type': 'PublishedFileType'}, + # 'task': None, + # 'task.Task.content': None, + # 'task.Task.due_date': None, + # 'task.Task.sg_status_list': None, + # 'task_uniqueness': False, + # 'type': 'PublishedFile', + # 'version': {'id': 6697, + # 'name': 'aaa_00010_F004_C003_0228F8_v000', + # 'type': 'Version'}, + # 'version.Version.sg_status_list': 'rev', + # 'version_number': 2} + + # get the name (lighting v3) + name_str = "Unnamed" + if sg_data.get("name"): + name_str = sg_data.get("name") + + if sg_data.get("version_number"): + name_str += " v%s" % sg_data.get("version_number") + + # now we are tracking whether this item has a unique task/name/type combo + # or not via the specially injected task_uniqueness boolean. + # If this is true, that means that this is the only item in the listing + # with this name/type combo, and we can render its display name on two + # lines, name first and then type, e.g.: + # MyScene, v3 + # Maya Render + # + # However, there can be multiple *different* tasks which have the same + # name/type combo - in this case, we want to display the task name too + # since this is what differentiates the data. In that case we display it: + # MyScene, v3 (Layout) + # Maya Render + # + if sg_data.get("task_uniqueness") == False and sg_data.get("task") is not None: + name_str += " (%s)" % sg_data["task"]["name"] + + return name_str + + def _get_details_text(self, sg_data): + return "" + def sizeHint(self, style_options, model_index): """ Specify the size of the item. @@ -384,3 +430,4 @@ def sizeHint(self, style_options, model_index): return PublishThumbWidget.calculate_size(scale_factor) + diff --git a/python/tk_multi_loader/dialog.py b/python/tk_multi_loader/dialog.py index 8edf9368..8e062703 100644 --- a/python/tk_multi_loader/dialog.py +++ b/python/tk_multi_loader/dialog.py @@ -33,6 +33,9 @@ overlay_widget = sgtk.platform.import_framework("tk-framework-qtwidgets", "overlay_widget") task_manager = sgtk.platform.import_framework("tk-framework-shotgunutils", "task_manager") +app = sgtk.platform.current_bundle() +rdo_fw_path = app.frameworks.get("tk-framework-rdo").disk_location + ShotgunModelOverlayWidget = overlay_widget.ShotgunModelOverlayWidget class AppDialog(QtGui.QWidget): @@ -173,7 +176,8 @@ def __init__(self, action_manager, parent=None): self._publish_model.cache_loaded.connect(self._on_publish_content_change) self._publish_model.data_refreshed.connect(self._on_publish_content_change) self._publish_proxy_model.filter_changed.connect(self._on_publish_content_change) - + self._publish_model.data_refreshed.connect(self._on_status_filter_change) + self._publish_proxy_model.filter_changed.connect(self._on_status_filter_change) # hook up view -> proxy model -> model self.ui.publish_view.setModel(self._publish_proxy_model) @@ -184,7 +188,11 @@ def __init__(self, action_manager, parent=None): self._publish_list_delegate = SgPublishListDelegate(self.ui.publish_view, self._action_manager) # recall which the most recently mode used was and set that - main_view_mode = self._settings_manager.retrieve("main_view_mode", self.MAIN_VIEW_THUMB) + default_view_mode = self.MAIN_VIEW_THUMB + # Check value of display_thumbnail in app settings + if not sgtk.platform.current_bundle().get_setting("display_thumbnail"): + default_view_mode = self.MAIN_VIEW_LIST + main_view_mode = self._settings_manager.retrieve("main_view_mode", default_view_mode) self._set_main_view_mode(main_view_mode) # whenever the type list is checked, update the publish filters @@ -206,29 +214,17 @@ def __init__(self, action_manager, parent=None): self.ui.publish_view.addAction(self._refresh_action) self.ui.publish_view.setContextMenuPolicy(QtCore.Qt.ActionsContextMenu) - ################################################# - # popdown publish filter widget for the main view - # note: - # we parent the widget to a frame that flows around the - # main publish area - this is in order to avoid a scenario - # where the overlay that sometimes pops up on top of the - # publish area and the search widget would be competing - # for the same z-index. The result in some of these cases - # is that the search widget is hidden under the "publishes - # not found" overlay. By having it parented to the frame - # instead, it will always be above the overlay. - self._search_widget = SearchWidget(self.ui.publish_frame) - # hook it up with the search button the main toolbar - self.ui.search_publishes.clicked.connect(self._on_publish_filter_clicked) - # hook it up so that it signals the publish proxy model whenever the filter changes - self._search_widget.filter_changed.connect(self._publish_proxy_model.set_search_query) - ################################################# # checkboxes, buttons etc self.ui.show_sub_items.toggled.connect(self._on_show_subitems_toggled) self.ui.check_all.clicked.connect(self._publish_type_model.select_all) self.ui.check_none.clicked.connect(self._publish_type_model.select_none) + + ################################################# + # filter status + self._filter_status = QtGui.QComboBox() + self._sg_type_ids = None ################################################# # thumb scaling @@ -276,6 +272,18 @@ def __init__(self, action_manager, parent=None): show_details = self._settings_manager.retrieve("show_details", False) self._set_details_pane_visiblity(show_details) + # Add rdo custom UI + hlayout = QtGui.QHBoxLayout() + spacerItem = QtGui.QSpacerItem(40, 20, QtGui.QSizePolicy.Expanding, QtGui.QSizePolicy.Minimum) + self.ui.middle_area.insertLayout(1, hlayout,) + self._add_rdo_status_filter(hlayout) + hlayout.addItem(spacerItem) + + # Publishes search widget + self._search_widget = SearchWidget(self.ui.publish_view, self.ui.publish_frame) + hlayout.addWidget(self._search_widget) + self._search_widget.filter_changed.connect(self._publish_proxy_model.set_search_query) + # trigger an initial evaluation of filter proxy model self._apply_type_filters_on_publishes() @@ -417,17 +425,6 @@ def _on_history_double_clicked(self, model_index): if default_action: default_action.trigger() - def _on_publish_filter_clicked(self): - """ - Executed when someone clicks the filter button in the main UI - """ - if self.ui.search_publishes.isChecked(): - self.ui.search_publishes.setIcon(QtGui.QIcon(QtGui.QPixmap(":/res/search_active.png"))) - self._search_widget.enable() - else: - self.ui.search_publishes.setIcon(QtGui.QIcon(QtGui.QPixmap(":/res/search.png"))) - self._search_widget.disable() - def _on_thumbnail_mode_clicked(self): """ Executed when someone clicks the thumbnail mode button @@ -452,7 +449,9 @@ def _set_main_view_mode(self, mode): self.ui.thumbnail_mode.setIcon(QtGui.QIcon(QtGui.QPixmap(":/res/mode_switch_thumb.png"))) self.ui.thumbnail_mode.setChecked(False) self.ui.publish_view.setViewMode(QtGui.QListView.ListMode) - self.ui.publish_view.setItemDelegate(self._publish_list_delegate) + self.ui.publish_view.setItemDelegate(self._publish_list_delegate) + self.ui.thumb_scale.setVisible(False) + self.ui.label_2.setVisible(False) elif mode == self.MAIN_VIEW_THUMB: self.ui.list_mode.setIcon(QtGui.QIcon(QtGui.QPixmap(":/res/mode_switch_card.png"))) @@ -461,6 +460,8 @@ def _set_main_view_mode(self, mode): self.ui.thumbnail_mode.setChecked(True) self.ui.publish_view.setViewMode(QtGui.QListView.IconMode) self.ui.publish_view.setItemDelegate(self._publish_thumb_delegate) + self.ui.thumb_scale.setVisible(True) + self.ui.label_2.setVisible(True) else: raise TankError("Undefined view mode!") @@ -744,6 +745,7 @@ def _on_home_clicked(self): # first, try to find the "home" item by looking at the current app context. found_preset = None found_item = None + found_model = None # get entity portion of context ctx = sgtk.platform.current_bundle().context @@ -757,17 +759,26 @@ def _on_home_clicked(self): # now see if our context object also exists in the tree of this profile model = self._entity_presets[p].model + item = model.item_from_entity(ctx.entity["type"], ctx.entity["id"]) if item is not None: # find an absolute match! Break the search. found_item = item + found_model = model break if found_preset is None: # no suitable item found. Use the first tab found_preset = self.ui.entity_preset_tabs.tabText(0) + if found_model and found_item: + # store current selection data in case model is refreshed. If this happens selection will be lost + # and user will have to press home again. This data will be used in SgEntityModel to restore + # selection after new data is fed into model. + found_model.reSelector['entity'] = found_item.get_sg_data() + found_model.reSelector['found_preset'] = found_preset + found_model.reSelector['sel_func'] = self._select_item_in_entity_tree # select it in the left hand side tree view self._select_item_in_entity_tree(found_preset, found_item) @@ -801,9 +812,17 @@ def _apply_type_filters_on_publishes(self): # go through and figure out which checkboxes are clicked and then # update the publish proxy model so that only items of that type # is displayed - sg_type_ids = self._publish_type_model.get_selected_types() + self._sg_type_ids = self._publish_type_model.get_selected_types() show_folders = self._publish_type_model.get_show_folders() - self._publish_proxy_model.set_filter_by_type_ids(sg_type_ids, show_folders) + self._publish_proxy_model.set_filter_by_type_ids(self._sg_type_ids, show_folders) + + + def apply_status_filters_on_publishes(self): + """ + Executed when the type listing changes + """ + chosen_status = self._filter_status.currentText() + self._publish_proxy_model.set_filter_by_status(chosen_status) ######################################################################################## # publish view @@ -1122,37 +1141,6 @@ def _load_entity_presets(self): hlayout = QtGui.QHBoxLayout() layout.addLayout(hlayout) - # add search textfield - search = QtGui.QLineEdit(tab) - search.setStyleSheet("QLineEdit{ border-width: 1px; " - "background-image: url(:/res/search.png);" - "background-repeat: no-repeat;" - "background-position: center left;" - "border-radius: 5px; " - "padding-left:20px;" - "margin:4px;" - "height:22px;" - "}") - search.setToolTip("Use the search field to narrow down the items displayed in the tree above.") - - try: - # this was introduced in qt 4.7, so try to use it if we can... :) - search.setPlaceholderText("Search...") - except: - pass - - hlayout.addWidget(search) - - # and add a cancel search button, disabled by default - clear_search = QtGui.QToolButton(tab) - icon = QtGui.QIcon() - icon.addPixmap(QtGui.QPixmap(":/res/clear_search.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) - clear_search.setIcon(icon) - clear_search.setAutoRaise(True) - clear_search.clicked.connect( lambda editor=search: editor.setText("") ) - clear_search.setToolTip("Click to clear your current search.") - hlayout.addWidget(clear_search) - # set up data backend model = SgEntityModel(self, sg_entity_type, @@ -1180,8 +1168,6 @@ def _load_entity_presets(self): self._dynamic_widgets.extend( [tab, layout, hlayout, - search, - clear_search, view, overlay, action_ea, @@ -1191,9 +1177,14 @@ def _load_entity_presets(self): # set up proxy model that we connect our search to proxy_model = SgEntityProxyModel(self) proxy_model.setSourceModel(model) - search.textChanged.connect(lambda text, v=view, pm=proxy_model: self._on_search_text_changed(text, v, pm) ) - self._dynamic_widgets.extend([model, proxy_model]) + # add search textfield + search = SearchWidget(view, tab) + hlayout.addWidget(search) +# search.textChanged.connect(lambda text, v=view, pm=proxy_model: self._on_search_text_changed(text, v, pm) ) + search.filter_changed.connect(proxy_model.setFilterFixedString) + + self._dynamic_widgets.extend([search, model, proxy_model]) # configure the view view.setEditTriggers(QtGui.QAbstractItemView.NoEditTriggers) @@ -1228,32 +1219,17 @@ def _load_entity_presets(self): # data has properly arrived in the model. self._on_home_clicked() - def _on_search_text_changed(self, pattern, tree_view, proxy_model): - """ - Triggered when the text in a search editor changes. - - :param pattern: new contents of search box - :param tree_view: associated tree view. - :param proxy_model: associated proxy model - """ - - # tell proxy model to reevaulate itself given the new pattern. - proxy_model.setFilterFixedString(pattern) - - # change UI decorations based on new pattern. - if pattern and len(pattern) > 0: - # indicate with a blue border that a search is active - tree_view.setStyleSheet("""QTreeView { border-width: 3px; - border-style: solid; - border-color: #2C93E2; } - QTreeView::item { padding: 6px; } - """) - # expand all nodes in the tree - tree_view.expandAll() - else: - # revert to default style sheet - tree_view.setStyleSheet("QTreeView::item { padding: 6px; }") - + def _add_rdo_status_filter(self, hlayout): + ''' + Add a combo box to sort the latest publishes by status + Kind of copying off the _add_rdo_status_filter + ''' + filter_label = QtGui.QLabel("Filter by Status") + filter_label.setAlignment(QtCore.Qt.AlignLeft|QtCore.Qt.AlignTrailing|QtCore.Qt.AlignVCenter) + self._filter_status.setSizeAdjustPolicy(QtGui.QComboBox.AdjustToContents) + hlayout.addWidget(filter_label) + hlayout.addWidget(self._filter_status) + self._filter_status.activated.connect(self.apply_status_filters_on_publishes) def _on_entity_profile_tab_clicked(self): """ @@ -1488,6 +1464,30 @@ def _populate_entity_breadcrumbs(self): self.ui.entity_breadcrumbs.setText("%s" % breadcrumbs) + def _on_status_filter_change(self): + """ + Reload the status filter combo box + With correct count in brackets + """ + tmp_index = self._filter_status.currentIndex() + tmp_index = 0 if tmp_index< 0 else tmp_index + + publish_items = self._publish_model._publish_items + self._filter_status.clear() + + valid_ids = self._publish_proxy_model._valid_type_ids + valid_items = [item for item in publish_items if item['type_id'] in valid_ids] + + + self._filter_status.addItem("All (%d)" % len(valid_items)) + status_list = list(set(item['sg_item']['sg_status_list'] for item in valid_items)) + for status in status_list: + icon = QtGui.QIcon() + count = sum(item['sg_item']['sg_status_list'] == status for item in valid_items) + name = "%s (%d) " % (status, count) + icon.addPixmap(QtGui.QPixmap("%s/resources/%s.png" % (rdo_fw_path, status)), QtGui.QIcon.Normal, QtGui.QIcon.Off) + self._filter_status.addItem(icon, name) + self._filter_status.setCurrentIndex(tmp_index) ################################################################################################ # Helper stuff diff --git a/python/tk_multi_loader/model_entity.py b/python/tk_multi_loader/model_entity.py index f1eb419c..1d61dfb4 100644 --- a/python/tk_multi_loader/model_entity.py +++ b/python/tk_multi_loader/model_entity.py @@ -45,22 +45,38 @@ def __init__(self, parent, entity_type, filters, hierarchy, bg_task_manager): self._entity_icons["Ticket"] = QtGui.QIcon(QtGui.QPixmap(":/res/icon_Ticket_dark.png")) self._entity_icons["Version"] = QtGui.QIcon(QtGui.QPixmap(":/res/icon_Version_dark.png")) - ShotgunModel.__init__(self, + ShotgunModel.__init__(self, parent, - download_thumbs=False, - schema_generation=4, - bg_load_thumbs=True, - bg_task_manager=bg_task_manager) + download_thumbs=False, + schema_generation=4, + bg_load_thumbs=True, + bg_task_manager=bg_task_manager) fields=["image", "sg_status_list", "description"] self._load_data(entity_type, filters, hierarchy, fields) - - - - + + # this dict holds entity which was selected in dialog after model was filled. This selection + # is result of self._on_home_clicked() executed at last line of _load_entity_presets() + # reSelector will be used only once in data_refreshed_cb to restore selection if items in model + # were destroyed and created again as result of differences between local shotgun disc cache + # and actual shotgun data + # see __on_sg_data_arrived(...) in class ShotgunModel + self.reSelector = {'entity': None, + 'found_preset': None, + 'dialog': parent, + 'sel_func': None + } + self.data_refreshed.connect(self.data_refreshed_cb) ############################################################################################ # public methods - + + def data_refreshed_cb(self): + entity = self.reSelector.get('entity', None) + if entity: + item = self.item_from_entity(entity.get('type'), entity.get('id')) + self.reSelector.get('sel_func')(self.reSelector.get('found_preset'), item) + self.reSelector['entity'] = None + def async_refresh(self): """ Trigger an asynchronous refresh of the model diff --git a/python/tk_multi_loader/model_latestpublish.py b/python/tk_multi_loader/model_latestpublish.py index 2f959b3f..b5f3db72 100644 --- a/python/tk_multi_loader/model_latestpublish.py +++ b/python/tk_multi_loader/model_latestpublish.py @@ -40,7 +40,7 @@ def __init__(self, parent, publish_type_model, bg_task_manager): 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._publish_items = [] self._associated_items = {} app = sgtk.platform.current_bundle() @@ -445,50 +445,33 @@ def _before_data_processing(self, sg_data_list): # but with different tasks, indicate this with a special boolean flag unique_data = {} - name_type_aggregates = defaultdict(int) for sg_item in sg_data_list: - + # get the associated type type_id = None type_link = sg_item[self._publish_type_field] if type_link: type_id = type_link["id"] - # also get the associated task - task_id = None - task_link = sg_item["task"] - if task_link: - task_id = task_link["id"] - # key publishes in dict by type and name - unique_data[ (sg_item["name"], type_id, task_id) ] = {"sg_item": sg_item, "type_id": type_id} + unique_data[ (sg_item["name"], type_id) ] = {"sg_item": sg_item, "type_id": type_id} - # count how many items of this type we have - name_type_aggregates[ (sg_item["name"], type_id) ] += 1 # SECOND PASS # We now have the latest versions only # Go ahead count types for the aggregate # and assemble filtered sg data set new_sg_data = [] + + self._publish_items = [] for second_pass_data in unique_data.values(): # get the shotgun data for this guy sg_item = second_pass_data["sg_item"] - - # now add a flag to indicate if this item is "task unique" or not - # e.g. if there are other items in the listing with the same name - # and same type but with a different task - if name_type_aggregates[ (sg_item["name"], second_pass_data["type_id"]) ] > 1: - # there are more than one item with this same name/type combo! - sg_item["task_uniqueness"] = False - else: - # no other item with this task/name/type combo - sg_item["task_uniqueness"] = True - # append to new sg data new_sg_data.append(sg_item) + self._publish_items.append(second_pass_data) # update our aggregate counts for the publish type view type_id = second_pass_data["type_id"] @@ -500,6 +483,3 @@ def _before_data_processing(self, sg_data_list): return new_sg_data - - - diff --git a/python/tk_multi_loader/model_publishhistory.py b/python/tk_multi_loader/model_publishhistory.py index 80acdb3f..1f6163cb 100644 --- a/python/tk_multi_loader/model_publishhistory.py +++ b/python/tk_multi_loader/model_publishhistory.py @@ -65,10 +65,8 @@ def load_data(self, sg_data): # when we filter out which other publishes are associated with this one, # to effectively get the "version history", we look for items # which have the same project, same entity assocation, same name, same type - # and the same task. filters = [ ["project", "is", sg_data["project"] ], ["name", "is", sg_data["name"] ], - ["task", "is", sg_data["task"] ], ["entity", "is", sg_data["entity"] ], [publish_type_field, "is", sg_data[publish_type_field] ], ] diff --git a/python/tk_multi_loader/model_publishtype.py b/python/tk_multi_loader/model_publishtype.py index d3ed9d7c..d613ec50 100644 --- a/python/tk_multi_loader/model_publishtype.py +++ b/python/tk_multi_loader/model_publishtype.py @@ -156,8 +156,7 @@ def get_selected_types(self): type_ids.extend(associated_sg_ids) return type_ids - - + def set_active_types(self, type_aggregates): """ Specifies which types are currently active. Also adjust the sort role, @@ -186,7 +185,7 @@ def set_active_types(self, type_aggregates): for type_id in sg_type_ids: if type_id in type_aggregates: total_matches += type_aggregates[type_id] - + if total_matches > 0: # there are matches for this publish type! Add it to the active section # of the filter list. diff --git a/python/tk_multi_loader/proxymodel_latestpublish.py b/python/tk_multi_loader/proxymodel_latestpublish.py index e7811cd1..86d6a403 100644 --- a/python/tk_multi_loader/proxymodel_latestpublish.py +++ b/python/tk_multi_loader/proxymodel_latestpublish.py @@ -28,6 +28,7 @@ def __init__(self, parent): self._valid_type_ids = None self._show_folders = True self._search_filter = "" + self._valid_statuses = None def set_search_query(self, search_filter): """ @@ -48,6 +49,12 @@ def set_filter_by_type_ids(self, type_ids, show_folders): # tell model to repush data self.invalidateFilter() self.filter_changed.emit() + + def set_filter_by_status(self, statuses): + self._valid_statuses = statuses.split(' (')[0] + self.invalidateFilter() + self.filter_changed.emit() + def filterAcceptsRow(self, source_row, source_parent_idx): """ @@ -56,7 +63,26 @@ def filterAcceptsRow(self, source_row, source_parent_idx): This will check each row as it is passing through the proxy model and see if we should let it pass or not. """ + # get the search filter, as specified via setFilterFixedString() + search_exp = self.filterRegExp() + search_exp.setCaseSensitivity(QtCore.Qt.CaseInsensitive) + + model = self.sourceModel() + current_item = model.invisibleRootItem().child(source_row) # assume non-tree structure + sg_data = current_item.get_sg_data() + publish_name = '' + if sg_data: + publish_name = sg_data.get('name') + publish_status = sg_data.get('sg_status_list') + + is_folder = current_item.data(SgLatestPublishModel.IS_FOLDER_ROLE) + + # See if status matches + if self._valid_statuses not in (None, 'All'): + if publish_status != self._valid_statuses: + return False + if self._valid_type_ids is None: # accept all! return True diff --git a/python/tk_multi_loader/search_widget.py b/python/tk_multi_loader/search_widget.py index ba557bf2..45108a43 100644 --- a/python/tk_multi_loader/search_widget.py +++ b/python/tk_multi_loader/search_widget.py @@ -10,8 +10,7 @@ import sgtk from sgtk.platform.qt import QtCore, QtGui -from .ui.search_widget import Ui_SearchWidget -from .utils import ResizeEventFilter + class SearchWidget(QtGui.QWidget): """ @@ -24,112 +23,88 @@ class SearchWidget(QtGui.QWidget): You can connect to the filter_changed signal to get notified whenever the search string is changed. """ - - # widget positioning offsets, relative to their parent widget - LEFT_SIDE_OFFSET = 88 - TOP_OFFSET = 10 - + # signal emitted whenever the search filter changes filter_changed = QtCore.Signal(str) - def __init__(self, parent): + def __init__(self, view, parent): """ Constructor :param parent: QT parent object """ QtGui.QWidget.__init__(self, parent) + self.view = view - # make invisible by default - self.setVisible(False) - - # set up the UI - self._ui = Ui_SearchWidget() - self._ui.setupUi(self) - - # now grab the default background color and use that - # in addition to that, apply the same styling that the search - # bar in the tree view is using. - p = QtGui.QPalette() - bg_col = p.color(QtGui.QPalette.Active, QtGui.QPalette.Window) - - style = """ - QGroupBox - { - background-color: rgb(%s, %s, %s); - border-style: none; - border-top-left-radius: 0px; - border-top-right-radius: 0px; - border-bottom-left-radius: 10px; - border-bottom-right-radius: 10px; - } - - QLineEdit - { - border-width: 1px; - background-image: url(:/res/search.png); - background-repeat: no-repeat; - background-position: center left; - border-radius: 5px; - padding-left:20px; - margin:4px; - height:22px; - } - """ % (bg_col.red(), bg_col.green(), bg_col.blue()) - - self._ui.group.setStyleSheet(style) - - # hook up a listener to the parent window so this widget - # follows along when the parent window changes size - filter = ResizeEventFilter(parent) - filter.resized.connect(self._on_parent_resized) - parent.installEventFilter(filter) + # Setup UI + self.__setup_ui() # set up signals and slots - self._ui.search.textChanged.connect(self._on_filter_changed) + self.search.textChanged.connect(self._on_filter_changed) + + def __setup_ui(self): + """Setup UI of search widget""" + hlayout = QtGui.QHBoxLayout() + hlayout.setContentsMargins(0, 0, 0, 0) + + self.search = QtGui.QLineEdit(self.parent()) + self.search.setStyleSheet("QLineEdit{ border-width: 1px; " + "background-image: url(:/res/search.png);" + "background-repeat: no-repeat;" + "background-position: center left;" + "border-radius: 5px; " + "padding-left:20px;" + "margin:4px;" + "height:22px;" + "}") + self.search.setToolTip("Enter some text to filter the publishes shown in the view below.
\n" + "Click the magnifying glass icon above to disable the filter.") + try: + # this was introduced in qt 4.7, so try to use it if we can... :) + self.search.setPlaceholderText("Search...") + except: + pass + + clear_search = QtGui.QToolButton(self.parent()) + icon = QtGui.QIcon() + icon.addPixmap(QtGui.QPixmap(":/res/clear_search.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) + clear_search.setIcon(icon) + clear_search.setAutoRaise(True) + clear_search.clicked.connect(lambda editor=self.search: editor.setText("")) + clear_search.setToolTip("Click to clear your current search.") + + hlayout.addWidget(self.search) + hlayout.addWidget(clear_search) + + self.setLayout(hlayout) def _on_filter_changed(self): - """ - Callback for when the text changes - - :param new_text: The contents of the filter line edit box - """ - # emit our custom signal + """Callback for when the text changes""" if self.isVisible(): # emit the search text that is in the view - search_text = self._ui.search.text() + search_text = self.search.text() else: # widget is hidden - emit empty search text search_text = "" - - self.filter_changed.emit(search_text) - def disable(self): - """ - Disable search widget and clear search query. - """ - # hide and reset the search - self.setVisible(False) - self._on_filter_changed() + # Get view class + view_class = self.view.__class__.__name__ - def enable(self): - """ - Enable search widget and focus the keyboard input on it. - """ - self.setVisible(True) - self._ui.search.setFocus() - self._on_filter_changed() - - def _on_parent_resized(self): - """ - Special slot hooked up to the event filter. - When associated widget is resized this slot is being called. - """ - # offset the position in such a way that it looks like - # it is "hanging down" from the adjacent window. - # these constants are purely aesthetic, decided after some - # tweaking and trial and error. - self.move(self.parentWidget().width()-self.width()-self.LEFT_SIDE_OFFSET, - -self.TOP_OFFSET) + if search_text and len(search_text) > 0: + # indicate with a blue border that a search is active + self.view.setStyleSheet("""%s { border-width: 3px; + border-style: solid; + border-color: #2C93E2; } + %s::item { padding: 6px; } + """ % (view_class, view_class)) + # expand all nodes in the tree + if isinstance(self.view, QtGui.QTreeView): + self.view.expandAll() + else: + # revert to default style sheet + self.view.setStyleSheet("%s::item { padding: 6px; }" % view_class) + + # emit our custom signal + self.filter_changed.emit(search_text) diff --git a/python/tk_multi_loader/ui/dialog.py b/python/tk_multi_loader/ui/dialog.py index 008040f5..42732eec 100644 --- a/python/tk_multi_loader/ui/dialog.py +++ b/python/tk_multi_loader/ui/dialog.py @@ -196,14 +196,6 @@ def setupUi(self, Dialog): self.label_5.setText("") self.label_5.setObjectName("label_5") self.horizontalLayout_2.addWidget(self.label_5) - self.search_publishes = QtGui.QToolButton(Dialog) - self.search_publishes.setMinimumSize(QtCore.QSize(0, 26)) - icon3 = QtGui.QIcon() - icon3.addPixmap(QtGui.QPixmap(":/res/search.png"), QtGui.QIcon.Normal, QtGui.QIcon.Off) - self.search_publishes.setIcon(icon3) - self.search_publishes.setCheckable(True) - self.search_publishes.setObjectName("search_publishes") - self.horizontalLayout_2.addWidget(self.search_publishes) self.info = QtGui.QToolButton(Dialog) self.info.setMinimumSize(QtCore.QSize(0, 26)) self.info.setObjectName("info") @@ -372,7 +364,6 @@ def retranslateUi(self, Dialog): self.thumbnail_mode.setText(QtGui.QApplication.translate("Dialog", "...", None, QtGui.QApplication.UnicodeUTF8)) self.list_mode.setToolTip(QtGui.QApplication.translate("Dialog", "List Mode", None, QtGui.QApplication.UnicodeUTF8)) self.list_mode.setText(QtGui.QApplication.translate("Dialog", "...", None, QtGui.QApplication.UnicodeUTF8)) - self.search_publishes.setToolTip(QtGui.QApplication.translate("Dialog", "Filter Publishes", None, QtGui.QApplication.UnicodeUTF8)) self.info.setToolTip(QtGui.QApplication.translate("Dialog", "Use this button to toggle details on and off. ", None, QtGui.QApplication.UnicodeUTF8)) self.info.setText(QtGui.QApplication.translate("Dialog", "Show Details", None, QtGui.QApplication.UnicodeUTF8)) self.show_sub_items.setToolTip(QtGui.QApplication.translate("Dialog", "Enables the subfolder mode, displaying a total aggregate of all selected items.", None, QtGui.QApplication.UnicodeUTF8))