def _export_file(self, file_path, *args, **kwargs): if not tp.is_maya(): LOGGER.warning('Shaders export is only supported in Maya!') return shaders_to_export = artellapipe.ShadersMgr().get_asset_shaders_to_export( asset=self._asset, return_only_shaders=False) locked_file = False if os.path.isfile(file_path): res = qtutils.show_question( None, 'Exporting Shaders Mapping File', 'Shaders Mapping File "{}" already exists. Do you want to overwrite it?'.format(file_path)) if res == QMessageBox.No: return artellapipe.FilesMgr().lock_file(file_path) locked_file = True try: with open(file_path, 'w') as fp: json.dump(shaders_to_export, fp) except Exception as exc: LOGGER.error('Error while exporting Shaders Mapping File "{}" | {}'.format(file_path, exc)) finally: if locked_file: artellapipe.FilesMgr().unlock_file(file_path) if os.path.isfile(file_path): return file_path
def get_shot_file(self, file_type, extension=None): """ Returns shot file object class linked to given file type for current project :param file_type: str :return: ArtellaShotType """ self._check_project() if not artellapipe.FilesMgr().is_valid_file_type(file_type): return shot_file_class_found = None for sequence_file_class in artellapipe.FilesMgr().file_classes: if sequence_file_class.FILE_TYPE == file_type: shot_file_class_found = sequence_file_class break if not shot_file_class_found: LOGGER.warning( 'No Shot File Class found for file of type: "{}"'.format( file_type)) return return shot_file_class_found
def get_light_rigs_path(project=None, config=None): """ Returns path where Light Rigs are located :param project: :param config: :return: str """ if not project: project = artellapipe.project if not config: config = get_config() light_rigs_template_name = config.get('lightrigs_template', None) if not light_rigs_template_name: msg = 'No Light Rigs Template name defined in configuration file: "{}"'.format( config.get_path()) LOGGER.warning(msg) return None template = artellapipe.FilesMgr().get_template(light_rigs_template_name) if not template: LOGGER.warning( '"{}" template is not defined in project files configuration file!' .format(light_rigs_template_name)) return None template_dict = { 'project_id': project.id, 'project_id_number': project.id_number, } light_rigs_path = template.format(template_dict) return artellapipe.FilesMgr().fix_path(light_rigs_path)
def get_path(self): """ Implements abstract get_path function Returns the path of the asset :return: str """ path_template_name = artellapipe.AssetsMgr().config.get( 'data', 'path_template_name') template = artellapipe.FilesMgr().get_template(path_template_name) if not template: LOGGER.warning( 'Impossible to retrieve asset path because template "{}" is not in configuration file' .format(path_template_name)) return None template_dict = { 'project_id': self._project.id, 'project_id_number': self._project.id_number, 'asset_type': self.get_category(), 'asset_name': self.get_name() } asset_path = template.format(template_dict) if not asset_path: LOGGER.warning( 'Impossible to retrieve asset path from template: "{} | {} | {}"' .format(template.name, template.pattern, template_dict)) return None asset_path = os.path.expandvars(asset_path) asset_path = artellapipe.FilesMgr().prefix_path_with_project_path( asset_path) return asset_path
def get_path(self): """ Implements abstract get_path function Returns the path of the sequence :return: str """ path_template_name = artellapipe.SequencesMgr().config.get('data', 'path_template_name') template = artellapipe.FilesMgr().get_template(path_template_name) if not template: LOGGER.warning( 'Impossible to retrieve sequence path because template "{}" is not in configuration file'.format( path_template_name)) return None template_dict = { 'project_id': self._project.id, 'project_id_number': self._project.id_number, 'sequence_name': self.get_name() } sequence_path = template.format(template_dict) if not sequence_path: LOGGER.warning( 'Impossible to retrieve sequence path from template: "{} | {} | {}"'.format( template.name, template.pattern, template_dict)) return None sequence_path = artellapipe.FilesMgr().prefix_path_with_project_path(sequence_path) return sequence_path
def import_file(self, status=None, extension=None, file_type=None, sync=False, reference=False): """ Implements base AbstractAsset reference_file_by_extension function References asset file with the given extension :param status: str :param extension: bool :param file_type: bool :param sync: bool :param reference: bool """ if not status: status = defines.ArtellaFileStatus.PUBLISHED available_extensions = self._project.extensions if not extension and file_type: file_type_class = artellapipe.FilesMgr().get_file_class(file_type) if file_type_class: file_type_extensions = file_type_class.FILE_EXTENSIONS if file_type_extensions: extension = file_type_extensions[0] if extension not in available_extensions: LOGGER.warning( 'Impossible to reference file with extension "{}". Supported extensions: {}'.format( extension, available_extensions)) return False file_types = artellapipe.FilesMgr().get_file_types_by_extension(extension) if not file_types: LOGGER.warning( 'Impossible to reference file by its extension ({}) because no file types are registered!'.format( extension)) return False if len(file_types) > 1 and not file_type: LOGGER.warning( 'Multiple file types found with extension: {}. Do not know which file should imported!'.format( extension)) return False elif len(file_types) == 1: file_type_to_import = file_types[0] if reference: file_type_to_import(self).import_file(status=status, sync=sync, reference=True) else: file_type_to_import(self).import_file(status=status, sync=sync, reference=False) else: for ft in file_types: if ft.FILE_TYPE != file_type: continue if reference: return ft(self).import_file(status=status, sync=sync, reference=True) else: return ft(self).import_file(status=status, sync=sync, reference=False)
def export_shot(self, shot_name, start_frame=101, new_version=False, comment=None): """ Export shots :param shot_name: str :param start_frame: 101 :param new_version: bool :param comment: str :return: """ shot_layout_file_type = self.config.get('shot_layout_file_type', default='shot_layout') shot = self.find_shot(shot_name) file_type = shot.get_file_type(shot_layout_file_type) if not file_type: LOGGER.warning( 'Impossible to export shot "{}" because file type "{}" was not found!' .format(shot_name, shot_layout_file_type)) return False valid_export = file_type.export_file(start_frame=start_frame) if not valid_export: LOGGER.warning( 'Something went wrong while exporting shot: "{}"'.format( shot_name)) return False shot_file_path = file_type.get_file() if not shot_file_path or not os.path.exists(shot_file_path): LOGGER.warning('Shot was not exported in proper path: "{}"'.format( shot_file_path)) return False if not comment: comment = 'Shot "{}" exported!'.format(shot_name) if new_version: artellapipe.FilesMgr().lock_file(file_path=shot_file_path, notify=False) valid_version = artellapipe.FilesMgr().upload_working_version( file_path=shot_file_path, skip_saving=True, notify=True, comment=comment) if not valid_version: LOGGER.warning( 'Was not possible to upload new version of shot file: {}'. format(shot_file_path)) return True
def get_dependencies(self, file_path, parent_path=None, found_files=None): """ Returns all dependencies that are currently loaded in the given file :param file_path: str, file path we want to get dependencies of :param parent_path: str :param found_files: list(str) :return: list(str) """ LOGGER.info('Getting Dependencies: {}'.format(file_path)) if not found_files: found_files = dict() if parent_path: if parent_path not in found_files: found_files[parent_path] = list() if file_path not in found_files[parent_path]: found_files[parent_path].append(file_path) if not os.path.isfile(file_path): file_path = artellapipe.FilesMgr().fix_path(file_path) if not os.path.isfile(file_path): return None ext = os.path.splitext(file_path)[-1] if ext != '.ma': return None with open(file_path, 'r') as open_file: if file_path not in found_files: found_files[file_path] = list() parser = ArtellaMayaAsciiParser(open_file) parser.parse() invalid_paths = parser.invalid_paths found_paths = parser.get_all_paths(include_references=False) for path in found_paths: if path not in found_files[file_path]: found_files[file_path].append(path) for ref in parser.references: ref = artellapipe.FilesMgr().fix_path(ref) self.get_dependencies(file_path=ref, parent_path=file_path, found_files=found_files) return found_files, invalid_paths
def get_valid_file_types(self): """ Returns a list with all valid file types of current asset :return: list(str) """ return [i for i in self.FILES if i in artellapipe.FilesMgr().files]
def get_shaders_path(self, status=defines.ArtellaFileStatus.WORKING, next_version=False): """ Returns path where asset shaders are stored :return: str """ shaders_path_file_type = artellapipe.ShadersMgr( ).get_shaders_path_file_type() if not shaders_path_file_type: LOGGER.warning('No Asset Shaders Path file type available!') return None shader_file_path_template = artellapipe.FilesMgr().get_template( 'shaders') if not shader_file_path_template: LOGGER.warning('No shaders path template found!') return None template_dict = { 'project_id': self._project.id, 'project_id_number': self._project.id_number, 'asset_type': self.get_category(), 'asset_name': self.get_name() } asset_shaders_path = self.solve_path( file_type=shaders_path_file_type, template=shader_file_path_template, template_dict=template_dict, status=status, check_file_type=False) return asset_shaders_path
def get_file_paths(self, return_first=False, fix_path=True, **kwargs): if self.FILE_TYPE not in self._asset.FILES: LOGGER.warning( 'FileType "{}" is not a valid file for Assets of type "{}"'. format(self.FILE_TYPE, self._asset.FILE_TYPE)) return list() file_paths = super(ArtellaAssetFile, self).get_file_paths(return_first=return_first, fix_path=fix_path, **kwargs) if file_paths: return file_paths status = kwargs.get('status', defines.ArtellaFileStatus.PUBLISHED) if status == defines.ArtellaFileStatus.WORKING: file_path = self.get_working_path() else: file_path = self.get_latest_local_published_path() if not file_path: return None if return_first else file_paths if fix_path: file_path = artellapipe.FilesMgr().fix_path(file_path) if return_first: return file_path else: return [file_path]
def on_file_reference(self, path): path = artellapipe.FilesMgr().fix_path(path, clean_path=False) if path not in self._references: if not os.path.isfile(path): if path not in self._invalid_paths: self._invalid_paths.append(path) else: self._references.append(path)
def export_asset_shaders_mapping(self, asset, comment=None, new_version=False): """ Exports shaders mapping of the given asset :param asset: ArtellaAsset :param publish: bool :param comment: str :param new_version: bool :return: bool """ if not asset: return False shaders_mapping_file_type = artellapipe.AssetsMgr( ).get_shaders_mapping_file_type() file_path = asset.get_file(file_type=shaders_mapping_file_type, status=defines.ArtellaFileStatus.WORKING, fix_path=True) if not file_path: return False shaders_mapping_file_class = artellapipe.FilesMgr().get_file_class( shaders_mapping_file_type) if not shaders_mapping_file_class: return False shaders_mapping_file = shaders_mapping_file_class(asset, file_path=file_path) shaders_mapping_file.export_file() if new_version: if os.path.isfile(file_path): artellapipe.FilesMgr().lock_file(file_path) valid_upload = artellapipe.FilesMgr().upload_working_version( file_path=file_path, skip_saving=True, comment=comment) if valid_upload: artellapipe.FilesMgr().unlock_file(file_path, warn_user=False)
def on_set_attr(self, name, value, attr_type): # GPU Cache File Path if name == 'cfn': value = artellapipe.FilesMgr().fix_path(value, clean_path=False) if value not in self._gpu_cache_paths: if not os.path.isfile(value): if value not in self._invalid_paths: self._invalid_paths.append(value) else: self._gpu_cache_paths.append(value) # Alembic Paths elif name == 'fn': value = artellapipe.FilesMgr().fix_path(value) if value not in self._alembic_paths: if not os.path.isfile(value): if value not in self._invalid_paths: self._invalid_paths.append(value) else: self._alembic_paths.append(value)
def replace_by_rig(self, rig_control=None): """ Replaces current asset by its rig file :param rig_control: str :return: """ if not rig_control: rig_control = 'root_ctrl' if self.is_rig(): return rig_file_class = artellapipe.FilesMgr().get_file_class('rig') if not rig_file_class: LOGGER.warning( 'Impossible to reference rig file because Rig File Class (rig) was not found!' ) return node_namespace = tp.Dcc.node_namespace(self.node, clean=True) current_matrix = tp.Dcc.node_matrix(self.node) parent_node = tp.Dcc.node_parent(self.node) self.remove() rig_file = rig_file_class(self.asset) ref_nodes = rig_file.import_file(reference=True, namespace=node_namespace, unique_namespace=False) if not ref_nodes: LOGGER.warning( 'No nodes imported into current scene for rig file!') return None root_ctrl = None for node in ref_nodes: root_ctrl = utils.get_control(node=node, rig_control=rig_control) if root_ctrl: break if not root_ctrl: return False tp.Dcc.set_node_matrix(root_ctrl, current_matrix) if parent_node and tp.Dcc.object_exists(parent_node): asset_node = artellapipe.AssetsMgr().get_asset_node_in_scene( root_ctrl) if not asset_node: return tp.Dcc.set_parent(asset_node.node, parent_node) return True
def update_shaders(self, shaders_paths=None): """ Updates shaders from Artella """ if shaders_paths: shaders_paths = python.force_list(shaders_paths) else: shaders_paths = self.get_shaders_paths() if not shaders_paths: return artellapipe.FilesMgr().sync_files(files=shaders_paths)
def synchronize_light_rigs(self): """ Synchronizes current light rigs into user computer """ light_rigs_path = self.get_light_rigs_path() if not light_rigs_path: msg = 'Impossible to synchronize light rigs because its path is not defined!' self.show_warning_message(msg) LOGGER.warning(msg) return artellapipe.FilesMgr().sync_paths([light_rigs_path], recursive=True) self._update_ui(allow_sync=False)
def get_shaders_extensions(self): """ Returns extension used for shaders in current project :return: str """ shaders_file_type = self.get_shaders_asset_file_type() extensions = artellapipe.FilesMgr().get_file_type_extensions( shaders_file_type) if not extensions: LOGGER.warning( 'Impossible to refresh shaders because shader file type is not defined in current project!' ) return None return extensions
def sync(self, file_type=None, sync_type=defines.ArtellaFileStatus.ALL): """ Synchronizes asset file type and with the given sync type (working or published) :param file_type: str, type of asset file. If None, all asset file types will be synced :param sync_type: str, type of sync (working, published or all) """ if not self.supports_file_type(file_type=file_type, status=sync_type): return paths_to_sync = self._get_paths_to_sync(file_type, sync_type) if not paths_to_sync: LOGGER.warning('No Paths to sync for "{}"'.format(self.get_name())) return artellapipe.FilesMgr().sync_paths(paths_to_sync, recursive=True)
def sync_latest_published_files(self, file_type=None, ask=False): """ Synchronizes all latest published files for current asset :param file_type: str, if not given all files will be synced """ if ask: result = qtutils.show_question( None, 'Synchronizing Latest Published Files: {}'.format(self.get_name()), 'Are you sure you want to synchronize latest published files? This can take quite some time!') if result == QMessageBox.No: return valid_types = self._get_types_to_check(file_type) if not valid_types: return files_to_sync = list() for valid_type in valid_types: file_type = self.get_file_type(valid_type) if not file_type: continue latest_published_info = file_type.get_server_versions(status=defines.ArtellaFileStatus.PUBLISHED) if not latest_published_info: continue for version_info in latest_published_info: latest_version_path = version_info.get('version_path', None) if not latest_version_path: continue # We do not get latest of the file already exists if os.path.isfile(latest_version_path): continue if os.path.isdir(latest_version_path): if not len(os.listdir(latest_version_path)) == 0: continue files_to_sync.append(latest_version_path) if files_to_sync: artellapipe.FilesMgr().sync_files(files_to_sync) return files_to_sync
def get_file_type(self, file_type, extension=None, **kwargs): """ Returns asset file object of the current asset and given file type :param file_type: str :param extension: str :return: ArtellaAssetType """ if file_type not in self.FILES: return None asset_file_class = artellapipe.FilesMgr().get_asset_file(file_type=file_type, extension=extension) if not asset_file_class: LOGGER.warning( 'File Type: {} | {} not registered in current project!'.format(file_type, extension)) return return asset_file_class(asset=self, **kwargs)
def _export_file(self, file_path, *args, **kwargs): if not file_path: return start_frame = kwargs.get('start_frame', 101) if os.path.isfile(file_path): valid_lock = artellapipe.FilesMgr().lock_file(file_path) if not valid_lock: LOGGER.warning( 'Was not possible to lock file: {}'.format(file_path)) valid_anim_export = self._shot.export_animation(file_path) valid_anim_import = self._shot.import_animation( file_path, start_frame=start_frame) return valid_anim_export and valid_anim_import
def get_asset_shader_file_class(self): """ Returns asset shader file class associated to this asset :return: class """ asset_shaders_file_type = self.get_shaders_asset_file_type() if not asset_shaders_file_type: LOGGER.warning('No Asset Shaders file type available!') return None asset_shader_file_class = artellapipe.FilesMgr().get_file_class( asset_shaders_file_type) if not asset_shader_file_class: LOGGER.warning( 'No Shader File Class found! Aborting shader loading ...') return None return asset_shader_file_class
def get_light_rig_file_type_instance(light_rig_name, light_rig_folder=None, project=None, config=None, light_rigs_path=None): if not light_rigs_path: light_rigs_path = get_light_rigs_path(project=project, config=config) if not light_rigs_path or not os.path.isdir(light_rigs_path): LOGGER.warning('Project {} has no Light Rigs!'.format( project.name.title())) return light_rig_file_type = get_light_rig_file_type(config=config) if not light_rig_file_type: LOGGER.warning( 'Project {} does not define a proper light rig file type!'.format( project.name.title())) return light_rig_file_class = artellapipe.FilesMgr().get_file_class( light_rig_file_type) if not light_rig_file_class: LOGGER.warning( 'Impossible to reference Light Rig: {} | {} | {}'.format( light_rig_name, light_rigs_path, light_rig_file_type)) return None if not light_rig_folder: light_rig_folder = light_rig_name light_rig_name = light_rig_name.title().replace(' ', '_') light_rig_path = os.path.join(light_rigs_path, light_rig_folder) if not os.path.isdir(light_rig_path): LOGGER.warning( 'Impossible to reference Light Rig: {} | {} | {}'.format( light_rig_name, light_rig_path, light_rig_file_type)) return None light_rig_file = light_rig_file_class(project, light_rig_name, file_path=light_rig_path) return light_rig_file
def _create_asset_files_buttons(self): """ Internal function that creates file buttons for the asset """ if not self._asset_widget: return file_buttons_widget = grid.GridWidget() file_buttons_widget.setStyleSheet( 'QTableWidget::item { margin-left: 6px; }') file_buttons_widget.setColumnCount(3) file_buttons_widget.setEditTriggers(QAbstractItemView.NoEditTriggers) file_buttons_widget.setShowGrid(False) file_buttons_widget.setFocusPolicy(Qt.NoFocus) file_buttons_widget.horizontalHeader().hide() file_buttons_widget.verticalHeader().hide() file_buttons_widget.resizeRowsToContents() file_buttons_widget.resizeColumnsToContents() file_buttons_widget.setSelectionMode(QAbstractItemView.NoSelection) files_btn = list() must_file_types = artellapipe.AssetsMgr().must_file_types for file_type in self._asset_widget.asset.FILES: if must_file_types: if file_type not in must_file_types: continue file_type_name = artellapipe.FilesMgr().get_file_type_name( file_type) file_btn = AssetFileButton(self._asset_widget, self.STATUS, file_type, file_type_name, tpDcc.ResourcesMgr().icon(file_type)) files_btn.append(file_btn) file_btn.checkVersions.connect(self._on_check_versions) self._file_buttons[file_type] = file_btn row, col = file_buttons_widget.first_empty_cell() file_buttons_widget.addWidget(row, col, file_btn) file_buttons_widget.resizeRowsToContents() if files_btn: file_buttons_widget.setFixedHeight(file_buttons_widget.rowCount() * files_btn[0].height() + 10) return file_buttons_widget
def _on_unlock(self): items_to_unlock = list() checked_items = self._checked_items() for item in checked_items: if not os.path.isfile(item.path): continue items_to_unlock.append(item) if not items_to_unlock: return msg = 'If changes in files are not submitted to Artella yet, submit them before unlocking the file please. ' \ '\n\n Do you want to continue?' res = qtutils.show_question(None, 'Unlock File', msg) if res != QMessageBox.Yes: return valid_unlock = False self._progress.setVisible(True) self._progress.setMinimum(0) self._progress.setMaximum(len(items_to_unlock) - 1) self._progress_lbl.setText('Unlocking files ...') self.repaint() for i, item in enumerate(items_to_unlock): self._progress.setValue(i) self._progress_lbl.setText('Unlocking: {}'.format(item.text(1))) self.repaint() valid_unlock = artellapipe.FilesMgr().unlock_file(item.path, notify=False, warn_user=False) if valid_unlock: item.setIcon(0, tp.ResourcesMgr().icon('unlock')) self.show_ok_message('File successfully unlock!') else: self.show_ok_message('Was not possible to unlock the file!') self._progress.setValue(0) self._progress_lbl.setText('') self._progress.setVisible(False) self._project.tray.show_message(title='Unlock Files', msg='Files unlocked successfully!') self.repaint() return valid_unlock
def delete_artella_folder(self, p): self.ui.progress_bar.setValue(0) self.ui.progress_lbl.setText("Locking Files") i = 0 for root, dirs, files in os.walk(p): for file in files: i += 1 if i == 0: return j = 0 self.ui.progress_bar.setMinimum(0) self.ui.progress_bar.setMaximum(i - 1) for root, dirs, files in os.walk(p): for file in files: artellapipe.FilesMgr().lock_file(file_path=os.path.join(root, file)) self.ui.progress_bar.setValue((j / float(i)) * 100) LOGGER.debug("XGEN || {} file locked".format(file)) j += 1 shutil.rmtree(p, onerror=self._on_rm_error) LOGGER.debug("XGEN || {} path deleted".format(p))
def get_file(self, file_type, status, extension=None, version=None, fix_path=False, only_local=False, extra_dict=None, must_exist=True, next_version=False): """ Returns file path of the given file type and status :param file_type: str :param status: str :param extension: str :param version: str :param fix_path: str :param only_local: bool :param extra_dict: dict, :param must_exist: bool, If True, if file path is not found None, will be returned, otherwise the function will return non-existent path. This is useful for example when getting publishing file paths that were not exported yet :return: """ if extra_dict is None: extra_dict = dict() if not extension: extensions = artellapipe.FilesMgr().get_file_type_extensions(file_type) if extensions: extension = extensions[0] else: extension = self.project.default_extension file_name = self.get_name() template_dict = self.get_template_dict(extension=extension) file_path = self.solve_path( file_type=file_type, template_dict=template_dict, extra_dict=extra_dict, version=version, fix_path=fix_path, only_local=only_local, status=status, must_exist=must_exist, next_version=next_version ) # if not file_path: # raise RuntimeError('Impossible to retrieve file because asset path for "{}" is not valid!'.format( # file_name # )) return file_path
def _add_file_to_artella(self, file_path_global, comment): """ Method that adds all the files of a given path to the artella system :param file_path_global: String with the base path to ad :param comment: String with the comment to add """ self.ui.progress_bar.setValue(0) self.ui.progress_lbl.setText("Uploading Files") i = 0 for root, dirs, files in os.walk(file_path_global): for file in files: i += 1 j = 0 self.ui.progress_bar.setMinimum(0) self.ui.progress_bar.setMaximum(i - 1) for root, dirs, files in os.walk(file_path_global): for file in files: artellapipe.FilesMgr().upload_working_version(os.path.join(root, file), comment=comment, force=True) self.ui.progress_bar.setValue((j / float(i)) * 100) LOGGER.debug("XGEN || {} file added".format(file)) j += 1
def _setup_synchronize_menu(self): """ Internal function that creates the synchronize menu """ sync_menu = QMenu(self) sync_icon = tpDcc.ResourcesMgr().icon('sync') for asset_type in artellapipe.AssetsMgr().get_asset_types(): action_icon = tpDcc.ResourcesMgr().icon(asset_type.lower()) if action_icon: sync_action = QAction(action_icon, asset_type.title(), self) else: sync_action = QAction(asset_type.title(), self) sync_menu.addAction(sync_action) asset_file_types = artellapipe.AssetsMgr().get_asset_type_files(asset_type=asset_type) or list() if asset_file_types: asset_files_menu = QMenu(sync_menu) sync_action.setMenu(asset_files_menu) for asset_file_type in asset_file_types: asset_type_icon = tpDcc.ResourcesMgr().icon(asset_file_type) asset_file_action = QAction(asset_type_icon, asset_file_type.title(), asset_files_menu) asset_files_menu.addAction(asset_file_action) asset_file_template = artellapipe.FilesMgr().get_template(asset_file_type) if not asset_file_template: LOGGER.warning('No File Template found for File Type: "{}"'.format(asset_file_type)) asset_file_action.setEnabled(False) continue asset_file_action.triggered.connect(partial(self._on_sync_file_type, asset_type, asset_file_type)) all_asset_types_action = QAction(sync_icon, 'All', asset_files_menu) all_asset_types_action.triggered.connect(partial(self._on_sync_all_assets_of_type, asset_type)) asset_files_menu.addAction(all_asset_types_action) sync_menu.addSeparator() sync_all_action = QAction(sync_icon, 'All', self) sync_all_action.triggered.connect(self._on_sync_all_types) sync_menu.addAction(sync_all_action) self._synchronize_btn.setMenu(sync_menu)