From 7f382872c80d5b677936a7c8a5b5ec60051d1963 Mon Sep 17 00:00:00 2001 From: Gergely Wootsch Date: Wed, 20 Sep 2023 13:44:07 +0200 Subject: [PATCH 01/32] Display token WIP --- bookmarks/database.py | 10 +++++- bookmarks/editor/bookmark_properties.py | 46 ++++++++++++++++++++++++- bookmarks/items/bookmark_items.py | 30 ++++++++++++---- bookmarks/publish.py | 2 ++ bookmarks/tokens/tokens_editor.py | 33 ++++++------------ 5 files changed, 90 insertions(+), 31 deletions(-) diff --git a/bookmarks/database.py b/bookmarks/database.py index e6cbf711..c2f35ac7 100644 --- a/bookmarks/database.py +++ b/bookmarks/database.py @@ -274,7 +274,15 @@ 'applications': { 'sql': 'TEXT', 'type': dict, - } + }, + 'bookmark_display_token': { + 'sql': 'TEXT', + 'type': str + }, + 'asset_display_token': { + 'sql': 'TEXT', + 'type': str + }, } } diff --git a/bookmarks/editor/bookmark_properties.py b/bookmarks/editor/bookmark_properties.py index 3735565c..4086d191 100644 --- a/bookmarks/editor/bookmark_properties.py +++ b/bookmarks/editor/bookmark_properties.py @@ -159,7 +159,27 @@ class BookmarkPropertyEditor(base.BasePropertyEditor): 'left empty, all folders in the bookmark will be ' 'interpreted as assets.', } - } + }, + 4: { + 0: { + 'name': 'Bookmark Display Name', + 'key': 'bookmark_display_token', + 'validator': base.name_validator, + 'widget': ui.LineEdit, + 'placeholder': '{server}/{job}/{root}', + 'description': 'Specify the tokens used to display bookmark items', + 'button': '+' + }, + 1: { + 'name': 'Asset Display Name', + 'key': 'asset_display_token', + 'validator': base.name_validator, + 'widget': ui.LineEdit, + 'placeholder': '{asset}', + 'description': 'Specify the tokens used to display asset items', + 'button': '+' + }, + }, } }, 1: { @@ -476,3 +496,27 @@ def applications_button_clicked(self): """ self.applications_editor.add_new_item() + + @QtCore.Slot() + def bookmark_display_token_button_clicked(self): + k = 'bookmark_display_token' + if not hasattr(self, f'{k}_editor'): + raise RuntimeError(f'{k}_editor not found') + + from ..tokens import tokens_editor + editor = getattr(self, f'{k}_editor') + w = tokens_editor.TokenEditor(self.server, self.job, self.root, parent=editor) + w.tokenSelected.connect(lambda x: editor.setText(f'{editor.text()}{x}')) + w.exec_() + + @QtCore.Slot() + def asset_display_token_button_clicked(self): + k = 'asset_display_token' + if not hasattr(self, f'{k}_editor'): + raise RuntimeError(f'{k}_editor not found') + + from ..tokens import tokens_editor + editor = getattr(self, f'{k}_editor') + w = tokens_editor.TokenEditor(self.server, self.job, self.root, parent=editor) + w.tokenSelected.connect(lambda x: editor.setText(f'{editor.text()}{x}')) + w.exec_() \ No newline at end of file diff --git a/bookmarks/items/bookmark_items.py b/bookmarks/items/bookmark_items.py index 62bdc9e8..d5f2db72 100644 --- a/bookmarks/items/bookmark_items.py +++ b/bookmarks/items/bookmark_items.py @@ -64,6 +64,8 @@ from .. import common from .. import contextmenu from .. import database +from .. import log +from ..tokens import tokens from ..threads import threads @@ -158,7 +160,23 @@ def init_data(self): job = v['job'] root = v['root'] - database.get(server, job, root) + display_name = f'{server}/{job}/{root}' + + try: + # Get the display name token from the database + db = database.get(server, job, root) + _display_name = db.value(db.source(), 'bookmark_display_token', database.BookmarkTable) + + # If a token is set, expand it + if _display_name: + config = tokens.get(server, job, root) + _display_name = config.expand_tokens('{job}') + print(_display_name) + if tokens.invalid_token not in _display_name: + display_name = _display_name + except Exception as e: + log.error(e) + file_info = QtCore.QFileInfo(k) exists = file_info.exists() @@ -179,9 +197,9 @@ def init_data(self): # bookmark exist if all( ( - server == common.active('server'), - job == common.active('job'), - root == common.active('root') + server == common.active('server'), + job == common.active('job'), + root == common.active('root') ) ) and exists: flags = flags | common.MarkedAsActive @@ -206,8 +224,8 @@ def init_data(self): data[idx] = common.DataDict( { - QtCore.Qt.DisplayRole: root, - QtCore.Qt.EditRole: root, + QtCore.Qt.DisplayRole: display_name, + QtCore.Qt.EditRole: display_name, common.PathRole: filepath, QtCore.Qt.ToolTipRole: filepath, QtCore.Qt.SizeHintRole: self.row_size, diff --git a/bookmarks/publish.py b/bookmarks/publish.py index 97e99329..a43c7927 100644 --- a/bookmarks/publish.py +++ b/bookmarks/publish.py @@ -739,6 +739,8 @@ def make_jpegs(self, payload): f = f'{folder}/jpg/{name}.jpg' files.append(f) buf.write(f) + + images.ImageCache.flush(destination) images.ImageCache.flush(f) payload['jpgs'] = files diff --git a/bookmarks/tokens/tokens_editor.py b/bookmarks/tokens/tokens_editor.py index 7fbc0e6f..6a0a1064 100644 --- a/bookmarks/tokens/tokens_editor.py +++ b/bookmarks/tokens/tokens_editor.py @@ -69,7 +69,7 @@ def _create_ui(self): editor.setSpacing(0) editor.itemClicked.connect( - lambda x: self.tokenSelected.emit(x.data(QtCore.Qt.DisplayRole)) + lambda x: self.tokenSelected.emit(x.data(QtCore.Qt.UserRole)) ) editor.itemClicked.connect( lambda x: self.done(QtWidgets.QDialog.Accepted) @@ -78,34 +78,21 @@ def _create_ui(self): self.layout().addWidget(editor, 0) config = tokens.get(self.server, self.job, self.root) - v = config.get_tokens( - user='username', - version='v001', - host='my-machine', - task='anim', - mode='anim', - element='main', - ext='ma', - prefix='MY_PROJECT', - asset='asset/with/multiple/subfolders', - seq='SQ010', - sequence='SQ010', - shot='SH0010', - project='my_project', - ) - for k in sorted(v.keys()): - token = '{{{}}}'.format(k) - editor.addItem(token) + v = config.get_tokens() + + for k in sorted(v.keys(), key=lambda x: x.strip('{}').lower()): + editor.addItem(f'{k}{" > {}".format(v[k]) if v[k] != "{invalid_token}" else ""}') item = editor.item(editor.count() - 1) item.setFlags(QtCore.Qt.ItemIsEnabled) + item.setData( QtCore.Qt.ToolTipRole, f'Current value: "{v[k]}"' ) - - @QtCore.Slot(QtWidgets.QListWidgetItem) - def item_clicked(self, item): - item = item.data(QtCore.Qt.DisplayRole) + item.setData( + QtCore.Qt.UserRole, + '{{{}}}'.format(k) + ) def sizeHint(self): """Returns a size hint. From 663f0348bc8bd4d1d37aa1d283e9ae4007f0237c Mon Sep 17 00:00:00 2001 From: Gergely Wootsch Date: Fri, 6 Oct 2023 09:09:58 +0200 Subject: [PATCH 02/32] Local maya script changes --- ...make_export_sets.py => aka_make_export_sets.py} | 14 +++++++++++++- ...odsy_dji_shaders.py => aka_shader_templates.py} | 2 +- bookmarks/maya/scripts/scripts.json | 6 +++--- bookmarks/publish.py | 7 +++++-- 4 files changed, 22 insertions(+), 7 deletions(-) rename bookmarks/maya/scripts/{aka_odsy_dji_make_export_sets.py => aka_make_export_sets.py} (89%) rename bookmarks/maya/scripts/{aka_odsy_dji_shaders.py => aka_shader_templates.py} (99%) diff --git a/bookmarks/maya/scripts/aka_odsy_dji_make_export_sets.py b/bookmarks/maya/scripts/aka_make_export_sets.py similarity index 89% rename from bookmarks/maya/scripts/aka_odsy_dji_make_export_sets.py rename to bookmarks/maya/scripts/aka_make_export_sets.py index c2a0f0a4..077b9a60 100644 --- a/bookmarks/maya/scripts/aka_odsy_dji_make_export_sets.py +++ b/bookmarks/maya/scripts/aka_make_export_sets.py @@ -1,3 +1,14 @@ +""" +This script creates sets for each export group in the scene. +Used in conjunction with Bookmarks to export character meshes for Houdini cloth simulation. + +Author: + Studio AKA, 2023 (c) All rights reserved. + https://www.studioaka.co.uk/ + Gergely Wootsch, + hello@gergely-wootsch.com + +""" import maya.cmds as cmds @@ -32,7 +43,6 @@ def create_set(set_name): def run(): - # Mapping of set names and their corresponding objects set_object_mapping = { 'IbogaineDJ_body_export': ['*DJ*:head_geo', '*DJ*:body_geo', '*DJ*:shoes_geo'], @@ -42,6 +52,7 @@ def run(): 'IbogaineMatty_cloth_export': ['*Matty*:cloth_geo', ], 'IbogaineMarcus_body_export': ['*Marcus*:head_geo', '*Marcus*:body_geo', '*Marcus*:shoes_geo'], 'IbogaineMarcus_cloth_export': ['*Marcus*:tshirt_geo', '*Marcus*:trousers_geo'], + 'IbogaineMarcus_extra_export': ['*Marcus*:eye_L', '*Marcus*:eye_R'], 'set_ground_export': ['set_ground_geo', ], 'set_background_export': ['set_background_geo', ], 'set_floor_export': ['set_floor_geo', ], @@ -49,6 +60,7 @@ def run(): } cmds.undoInfo(openChunk=True) + try: # Create each set and add its objects created_sets = [] diff --git a/bookmarks/maya/scripts/aka_odsy_dji_shaders.py b/bookmarks/maya/scripts/aka_shader_templates.py similarity index 99% rename from bookmarks/maya/scripts/aka_odsy_dji_shaders.py rename to bookmarks/maya/scripts/aka_shader_templates.py index 3e4806cb..3d786eef 100644 --- a/bookmarks/maya/scripts/aka_odsy_dji_shaders.py +++ b/bookmarks/maya/scripts/aka_shader_templates.py @@ -32,7 +32,7 @@ from PySide2 import QtCore, QtGui, QtWidgets -WINDOW_TITLE = 'AKA Odyssey Shaders' +WINDOW_TITLE = 'Aka Shader Templates' NUMERIC_TYPES = ['double', 'float', 'long', 'short', 'byte', 'int'] DEFAULT_PREFIX = 'main' diff --git a/bookmarks/maya/scripts/scripts.json b/bookmarks/maya/scripts/scripts.json index e2b510c6..ab803a50 100644 --- a/bookmarks/maya/scripts/scripts.json +++ b/bookmarks/maya/scripts/scripts.json @@ -1,12 +1,12 @@ { "0": { "name": "AKA-ODSY-DJI | Shader Import/Export...", - "module": "aka_odsy_dji_shaders", + "module": "aka_shader_templates", "description": "Shader template import and export tool" }, "1": { - "name": "AKA-ODSY-DJI | Make EXPORT Sets", - "module": "aka_odsy_dji_make_export_sets", + "name": "AKA-ODSY-DJI | Make export Sets", + "module": "aka_make_export_sets", "description": "" }, "2": { diff --git a/bookmarks/publish.py b/bookmarks/publish.py index a43c7927..c01825b2 100644 --- a/bookmarks/publish.py +++ b/bookmarks/publish.py @@ -602,8 +602,11 @@ def save_changes(self): self.prepare_publish(destination, payload=payload) self.copy_payload_files(payload=payload) self.save_thumbnail(destination, payload=payload) - jpegs = self.make_jpegs(payload=payload) - self.make_videos(destination, jpegs, payload=payload) + + # TODO: We can't implement this without more user control! + # jpegs = self.make_jpegs(payload=payload) + # self.make_videos(destination, jpegs, payload=payload) + self.write_manifest(destination, payload=payload) self.post_publish(destination, kwargs) return True From a12d6456f1d02cb7e123a04ee348fce0c9d3eed7 Mon Sep 17 00:00:00 2001 From: Gergely Wootsch Date: Mon, 9 Oct 2023 11:12:45 +0200 Subject: [PATCH 03/32] Add animation in progress attribute to the stacked widget This allows the delegate to optimise painting whilst the transition is in progress. (The performance still seems pretty poor...) --- bookmarks/common/ui.py | 2 +- bookmarks/external/ffmpeg.py | 2 +- bookmarks/external/ffmpeg_widget.py | 4 ++-- bookmarks/importexport.py | 2 +- bookmarks/items/asset_items.py | 2 +- bookmarks/items/delegate.py | 12 ++++++++++++ bookmarks/items/file_items.py | 2 +- bookmarks/items/views.py | 20 +++++++++++++++++--- bookmarks/items/widgets/filter_editor.py | 2 +- bookmarks/main.py | 2 +- bookmarks/maya/export.py | 4 ++-- bookmarks/scripts/clips.py | 4 ++-- bookmarks/scripts/sg_sync.py | 18 +++++++++--------- bookmarks/shotgun/sg_publish_clip.py | 10 +++++----- bookmarks/versioncontrol/versioncontrol.py | 4 ++-- 15 files changed, 58 insertions(+), 32 deletions(-) diff --git a/bookmarks/common/ui.py b/bookmarks/common/ui.py index ab424d39..25c915a7 100644 --- a/bookmarks/common/ui.py +++ b/bookmarks/common/ui.py @@ -398,7 +398,7 @@ def show_message(title, body='', disable_animation=False, icon='icon', message_t if disable_animation: mbox.show() mbox.raise_() - QtWidgets.QApplication.instance().processEvents() + QtWidgets.QApplication.instance().processEvents(QtCore.QEventLoop.ExcludeUserInputEvents) return mbox return mbox.open() diff --git a/bookmarks/external/ffmpeg.py b/bookmarks/external/ffmpeg.py index b8bc3061..4609d7b1 100644 --- a/bookmarks/external/ffmpeg.py +++ b/bookmarks/external/ffmpeg.py @@ -449,7 +449,7 @@ def convert( common.message_widget.body_label.setText( f'Converting frame {int(match.group(1))} of {int(endframe)}' ) - QtWidgets.QApplication.instance().processEvents() + QtWidgets.QApplication.instance().processEvents(QtCore.QEventLoop.ExcludeUserInputEvents) # Verify the output if proc.returncode == 1: diff --git a/bookmarks/external/ffmpeg_widget.py b/bookmarks/external/ffmpeg_widget.py index 959789eb..71893992 100644 --- a/bookmarks/external/ffmpeg_widget.py +++ b/bookmarks/external/ffmpeg_widget.py @@ -427,7 +427,7 @@ def preprocess_sequence(self, preconversion_format='jpg'): source_path, destination_path = items common.message_widget.body_label.setText(f'Copying image {idx} of {len(source_images)}...') - QtWidgets.QApplication.instance().processEvents() + QtWidgets.QApplication.instance().processEvents(QtCore.QEventLoop.ExcludeUserInputEvents) if not QtCore.QFile.copy(source_path, destination_path): raise RuntimeError(f'Could not copy {source_path} to {destination_path}') @@ -439,7 +439,7 @@ def preprocess_sequence(self, preconversion_format='jpg'): common.message_widget.body_label.setText( f'The sequence needs pre-converting:\nConverting {len(source_images)} images, please wait...' ) - QtWidgets.QApplication.instance().processEvents() + QtWidgets.QApplication.instance().processEvents(QtCore.QEventLoop.ExcludeUserInputEvents) if not pyimageutil.convert_images(source_images, ffmpeg_source_images, max_size=-1, release_gil=True): raise RuntimeError('Failed to convert an image using OpenImageIO.') diff --git a/bookmarks/importexport.py b/bookmarks/importexport.py index 03927c68..ae659464 100644 --- a/bookmarks/importexport.py +++ b/bookmarks/importexport.py @@ -387,7 +387,7 @@ def import_json_asset_properties(indexes, prompt=True, path=None): continue common.message_widget.body_label.setText(f'Processing {data[item]["name"]} ({n} of {count})...') - QtWidgets.QApplication.instance().processEvents() + QtWidgets.QApplication.instance().processEvents(QtCore.QEventLoop.ExcludeUserInputEvents) # Set valid database values with db.connection(): diff --git a/bookmarks/items/asset_items.py b/bookmarks/items/asset_items.py index 683f1f33..aec7edb3 100644 --- a/bookmarks/items/asset_items.py +++ b/bookmarks/items/asset_items.py @@ -216,7 +216,7 @@ def init_data(self): common.signals.showStatusBarMessage.emit( f'Loading assets ({c} found)...' ) - QtWidgets.QApplication.instance().processEvents() + QtWidgets.QApplication.instance().processEvents(QtCore.QEventLoop.ExcludeUserInputEvents) filename = filepath[len(source) + 1:] flags = models.DEFAULT_ITEM_FLAGS diff --git a/bookmarks/items/delegate.py b/bookmarks/items/delegate.py index ccf444f4..1a918652 100644 --- a/bookmarks/items/delegate.py +++ b/bookmarks/items/delegate.py @@ -2832,6 +2832,10 @@ def paint(self, painter, option, index): self.paint_hover(*args) self.paint_thumbnail_shadow(*args) self.paint_name(*args) + + if common.main_widget.stacked_widget.animation_in_progress: + return + self.paint_archived(*args) self.paint_inline_background(*args) self.paint_inline_icons(*args) @@ -2873,6 +2877,10 @@ def paint(self, painter, option, index): self.paint_hover(*args) self.paint_thumbnail_shadow(*args) self.paint_name(*args) + + if common.main_widget.stacked_widget.animation_in_progress: + return + self.paint_archived(*args) self.paint_description_editor_background(*args) self.paint_inline_background(*args) @@ -2913,6 +2921,7 @@ def paint(self, painter, option, index): args = self.get_paint_arguments(painter, option, index) if not index.data(QtCore.Qt.DisplayRole): return + p_role = index.data(common.ParentPathRole) if p_role: self.paint_background(*args) @@ -2920,6 +2929,9 @@ def paint(self, painter, option, index): self.paint_hover(*args) self.paint_name(*args) + if common.main_widget.stacked_widget.animation_in_progress: + return + self.paint_thumbnail_shadow(*args) self.paint_archived(*args) diff --git a/bookmarks/items/file_items.py b/bookmarks/items/file_items.py index bf14039a..b4bc02f8 100644 --- a/bookmarks/items/file_items.py +++ b/bookmarks/items/file_items.py @@ -368,7 +368,7 @@ def init_data(self): common.signals.showStatusBarMessage.emit( 'Loading files (found ' + str(c) + ' items)...' ) - QtWidgets.QApplication.instance().processEvents() + QtWidgets.QApplication.instance().processEvents(QtCore.QEventLoop.ExcludeUserInputEvents) flags = models.DEFAULT_ITEM_FLAGS seq, sequence_path = get_sequence_elements(filepath) diff --git a/bookmarks/items/views.py b/bookmarks/items/views.py index 0074badd..c59f20b0 100644 --- a/bookmarks/items/views.py +++ b/bookmarks/items/views.py @@ -235,8 +235,10 @@ def _get(s, color=common.color(common.color_green)): class ListsWidget(QtWidgets.QStackedWidget): - """This stacked widget contains the main bookmark, asset, file and favourite item - views. + """This stacked widget contains the main :class:`~bookmarks.items.bookmark_items.BookmarkItemView`, + :class:`~bookmarks.items.asset_items.AssetItemView`, :class:`~bookmarks.items.file_items.FileItemView` and + :class:`~bookmarks.items.favourite_items.FavouriteItemView`. + widgets. """ @@ -244,6 +246,8 @@ def __init__(self, parent=None): super().__init__(parent=parent) self.setObjectName('BrowserStackedWidget') + self.animation_in_progress = False + common.signals.tabChanged.connect(self.setCurrentIndex) def setCurrentIndex(self, idx): @@ -289,10 +293,20 @@ def animation_finished(a): super(ListsWidget, self).setCurrentIndex(_idx) common.signals.tabChanged.emit(_idx) + @QtCore.Slot() + def animation_state_changed(state, old_state): + if state == QtCore.QAbstractAnimation.Running: + self.animation_in_progress = True + if state == QtCore.QAbstractAnimation.Stopped: + self.animation_in_progress = False + + animation = QtCore.QParallelAnimationGroup() animation.finished.connect(functools.partial(animation_finished, animation)) + animation.stateChanged.connect(animation_state_changed) duration = 200 + # Create animation for outgoing widget out_anim = QtCore.QPropertyAnimation(self.widget(current_index), b"geometry") out_anim.setEasingCurve(QtCore.QEasingCurve.OutQuad) @@ -331,7 +345,7 @@ def animation_finished(a): animation.addAnimation(out_op) animation.addAnimation(in_op) - animation.start() + animation.start(QtCore.QPropertyAnimation.DeleteWhenStopped) self.widget(idx).show() def showEvent(self, event): diff --git a/bookmarks/items/widgets/filter_editor.py b/bookmarks/items/widgets/filter_editor.py index 1862e0de..33a78038 100644 --- a/bookmarks/items/widgets/filter_editor.py +++ b/bookmarks/items/widgets/filter_editor.py @@ -50,7 +50,7 @@ def set_opacity(self, value): self._opacity = value self.setWindowOpacity(value) self.repaint() - QtWidgets.QApplication.instance().processEvents() + QtWidgets.QApplication.instance().processEvents(QtCore.QEventLoop.ExcludeUserInputEvents) def eventFilter(self, source, event): if event.type() == QtCore.QEvent.KeyRelease: diff --git a/bookmarks/main.py b/bookmarks/main.py index 15d49849..18f9762f 100644 --- a/bookmarks/main.py +++ b/bookmarks/main.py @@ -239,7 +239,7 @@ def initialize(self): self._connect_signals() self.aboutToInitialize.emit() - QtWidgets.QApplication.instance().processEvents() + QtWidgets.QApplication.instance().processEvents(QtCore.QEventLoop.ExcludeUserInputEvents) # Update the window title to display the current active paths for n in range(3): diff --git a/bookmarks/maya/export.py b/bookmarks/maya/export.py index 7d0c69ae..c14e7a8a 100644 --- a/bookmarks/maya/export.py +++ b/bookmarks/maya/export.py @@ -710,7 +710,7 @@ def export_ass( start_time = time.time() for fr in range(start_frame, end_frame + 1): - QtWidgets.QApplication.instance().processEvents() + QtWidgets.QApplication.instance().processEvents(QtCore.QEventLoop.ExcludeUserInputEvents) if self._interrupt_requested: self._interrupt_requested = False return @@ -769,7 +769,7 @@ def export_obj( cmds.select(outliner_set, replace=True) for fr in range(start_frame, end_frame + 1): - QtWidgets.QApplication.instance().processEvents() + QtWidgets.QApplication.instance().processEvents(QtCore.QEventLoop.ExcludeUserInputEvents) if self._interrupt_requested: self._interrupt_requested = False return diff --git a/bookmarks/scripts/clips.py b/bookmarks/scripts/clips.py index a6b478b1..f5f21ec4 100644 --- a/bookmarks/scripts/clips.py +++ b/bookmarks/scripts/clips.py @@ -712,7 +712,7 @@ def _get_sources(path, ext): asset_data[k] = db.value(db.source(), k, database.BookmarkTable) common.message_widget.title_label.setText(f'Processing {asset}...') - QtWidgets.QApplication.instance().processEvents() + QtWidgets.QApplication.instance().processEvents(QtCore.QEventLoop.ExcludeUserInputEvents) for k in DEFAULT_SOURCES: if not hasattr(self, f'{k.replace("/", "_")}_editor'): @@ -734,7 +734,7 @@ def _get_sources(path, ext): continue common.message_widget.body_label.setText(f'Found source dir:\n{source_dir}') - QtWidgets.QApplication.instance().processEvents() + QtWidgets.QApplication.instance().processEvents(QtCore.QEventLoop.ExcludeUserInputEvents) # Get the source files for source in _get_sources(source_dir, source_ext): diff --git a/bookmarks/scripts/sg_sync.py b/bookmarks/scripts/sg_sync.py index 7b2f2da7..b04c51b8 100644 --- a/bookmarks/scripts/sg_sync.py +++ b/bookmarks/scripts/sg_sync.py @@ -619,7 +619,7 @@ def process_entities( # Get all ShotGrid entities for n, entity in enumerate(entities): common.message_widget.title_label.setText(f'Processing {entity["code"]} ({n + 1} of {len(entities)})') - QtWidgets.QApplication.instance().processEvents() + QtWidgets.QApplication.instance().processEvents(QtCore.QEventLoop.ExcludeUserInputEvents) if source_entity_type == 'Shot': seq, shot = common.get_sequence_and_shot(entity['code']) @@ -692,7 +692,7 @@ def process_entities( if update_data: common.message_widget.body_label.setText(f'Updating ShotGrid entity...') - QtWidgets.QApplication.instance().processEvents() + QtWidgets.QApplication.instance().processEvents(QtCore.QEventLoop.ExcludeUserInputEvents) sg.update( 'Shot', entity['id'], update_data, ) @@ -706,7 +706,7 @@ def process_entities( print(f'Uploading thumbnail for {entity["code"]}') common.message_widget.body_label.setText(f'Uploading ShotGrid thumbnail...') - QtWidgets.QApplication.instance().processEvents() + QtWidgets.QApplication.instance().processEvents(QtCore.QEventLoop.ExcludeUserInputEvents) sg.upload_thumbnail('Shot', entity['id'], thumbnail_path) @@ -752,7 +752,7 @@ def process_entities( items_data[task_path]['description'] = asset_data_item['description'] common.message_widget.body_label.setText(f'Finding tasks...') - QtWidgets.QApplication.instance().processEvents() + QtWidgets.QApplication.instance().processEvents(QtCore.QEventLoop.ExcludeUserInputEvents) # Find the associated task entity for task_entity in task_entities: @@ -764,7 +764,7 @@ def process_entities( # Create any missing files and folders inside the Maya work folders if self.sg_sync_create_folders_editor.isChecked(): common.message_widget.body_label.setText(f'Making folders...') - QtWidgets.QApplication.instance().processEvents() + QtWidgets.QApplication.instance().processEvents(QtCore.QEventLoop.ExcludeUserInputEvents) print(f'Patching {root}/{task_path}') self.patch_asset(asset_template_path, f'{root}/{task_path}') @@ -788,7 +788,7 @@ def process_entities( if self.sg_sync_sync_data_to_bookmarks_editor.isChecked(): common.message_widget.title_label.setText(f'Adding asset links...') common.message_widget.body_label.setText('') - QtWidgets.QApplication.instance().processEvents() + QtWidgets.QApplication.instance().processEvents(QtCore.QEventLoop.ExcludeUserInputEvents) for k in links: if not os.path.isdir(f'{bookmark_root}/{k}'): @@ -811,7 +811,7 @@ def process_entities( # Reset the filters common.message_widget.title_label.setText(f'Updating asset items...') - QtWidgets.QApplication.instance().processEvents() + QtWidgets.QApplication.instance().processEvents(QtCore.QEventLoop.ExcludeUserInputEvents) common.model(common.AssetTab).set_filter_text('') # Refresh model @@ -819,7 +819,7 @@ def process_entities( # Wait for the model to refresh common.message_widget.body_label.setText(f'Waiting for refresh...') - QtWidgets.QApplication.instance().processEvents() + QtWidgets.QApplication.instance().processEvents(QtCore.QEventLoop.ExcludeUserInputEvents) model = common.source_model(common.AssetTab) p = model.source_path() @@ -835,7 +835,7 @@ def process_entities( _time_slept = 0 _increment = 0.1 while not data.loaded: - QtWidgets.QApplication.instance().processEvents() + QtWidgets.QApplication.instance().processEvents(QtCore.QEventLoop.ExcludeUserInputEvents) if _time_slept >= max_sleep: break _time_slept += _increment diff --git a/bookmarks/shotgun/sg_publish_clip.py b/bookmarks/shotgun/sg_publish_clip.py index e5747834..b114c820 100644 --- a/bookmarks/shotgun/sg_publish_clip.py +++ b/bookmarks/shotgun/sg_publish_clip.py @@ -327,7 +327,7 @@ def save_changes(self): disable_animation=True, parent=self ) - QtWidgets.QApplication.instance().processEvents() + QtWidgets.QApplication.instance().processEvents(QtCore.QEventLoop.ExcludeUserInputEvents) existing_version = sg.find_one( 'Version', [['code', 'is', data['name']]] @@ -370,7 +370,7 @@ def save_changes(self): disable_animation=True, parent=self ) - QtWidgets.QApplication.instance().processEvents() + QtWidgets.QApplication.instance().processEvents(QtCore.QEventLoop.ExcludeUserInputEvents) cut_data = { 'project': data['project_entity'], @@ -392,7 +392,7 @@ def save_changes(self): disable_animation=True, parent=self ) - QtWidgets.QApplication.instance().processEvents() + QtWidgets.QApplication.instance().processEvents(QtCore.QEventLoop.ExcludeUserInputEvents) cut_item_data = { 'project': data['project_entity'], @@ -418,7 +418,7 @@ def save_changes(self): disable_animation=True, parent=self ) - QtWidgets.QApplication.instance().processEvents() + QtWidgets.QApplication.instance().processEvents(QtCore.QEventLoop.ExcludeUserInputEvents) published_file_data = { 'project': data['project_entity'], @@ -459,7 +459,7 @@ def save_changes(self): disable_animation=True, parent=self ) - QtWidgets.QApplication.instance().processEvents() + QtWidgets.QApplication.instance().processEvents(QtCore.QEventLoop.ExcludeUserInputEvents) # Upload the actual file to ShotGrid sg.upload("Version", version['id'], data['file_path'], field_name='sg_uploaded_movie') diff --git a/bookmarks/versioncontrol/versioncontrol.py b/bookmarks/versioncontrol/versioncontrol.py index 62c103ef..e95361b9 100644 --- a/bookmarks/versioncontrol/versioncontrol.py +++ b/bookmarks/versioncontrol/versioncontrol.py @@ -234,7 +234,7 @@ def check(): if total_length is None: # no content length header progress_widget.setMaximum(0) progress_widget.forceShow() - QtWidgets.QApplication.instance().processEvents() + QtWidgets.QApplication.instance().processEvents(QtCore.QEventLoop.ExcludeUserInputEvents) f.write(response.content) progress_widget.close() return @@ -245,7 +245,7 @@ def check(): progress_widget.forceShow() while True: - QtWidgets.QApplication.instance().processEvents() + QtWidgets.QApplication.instance().processEvents(QtCore.QEventLoop.ExcludeUserInputEvents) data = response.read(4096) if not data: break From 24267fc9eb06438d3e9e1cba9cfee5e0f3581881 Mon Sep 17 00:00:00 2001 From: Gergely Wootsch Date: Mon, 9 Oct 2023 13:28:51 +0200 Subject: [PATCH 04/32] Implement bookmark and asset item display tokens This is a more comprehensive commit to address the display of asset and bookmark items. The main issue is as Bookmarks wraps around a pipeline the display names can resolve into long and confusing names. For instance, if a bookmark item's path is `\\server\projects\WIP\Client\Job\080_Episodes\EpisodeA\Sequences` what should be the display name of the bookmark item inside bookmarks? There are various correct answers but we can probably agree that "server/projects/WIP", and "080_Episodes" might be superflous strings. So instead, we can use the tokens module and assign a display token to the item, such as`{job1}/{root1}/{root2}` resulting in `Job/EpisodeA/Sequences`, a more user-friendly name. The resulting changes also called for changes in the delegate module and more fine grained user control over what and how is being displayed. --- bookmarks/common/settings.py | 1 + bookmarks/editor/base.py | 2 + bookmarks/editor/bookmark_properties.py | 8 +- bookmarks/editor/preferences.py | 10 ++ bookmarks/file_saver/widgets.py | 3 +- bookmarks/items/asset_items.py | 56 ++++++--- bookmarks/items/bookmark_items.py | 34 ++--- bookmarks/items/delegate.py | 160 +++++------------------- bookmarks/main.py | 2 +- bookmarks/rsc/config.json | 2 - bookmarks/threads/workers.py | 5 +- bookmarks/tokens/tokens.py | 2 +- 12 files changed, 109 insertions(+), 176 deletions(-) diff --git a/bookmarks/common/settings.py b/bookmarks/common/settings.py index fbbab463..ee5cc03b 100644 --- a/bookmarks/common/settings.py +++ b/bookmarks/common/settings.py @@ -35,6 +35,7 @@ 'settings/show_menu_icons', 'settings/paint_thumbnail_bg', 'settings/disable_oiio', + 'settings/hide_item_descriptions', 'settings/always_always_on_top', 'settings/bin_ffmpeg', 'settings/bin_rv', diff --git a/bookmarks/editor/base.py b/bookmarks/editor/base.py index 605b5683..fafb9798 100644 --- a/bookmarks/editor/base.py +++ b/bookmarks/editor/base.py @@ -38,6 +38,8 @@ domain_validator.setRegExp(QtCore.QRegExp(r'[a-zA-Z0-9/:\.]+')) version_validator = QtGui.QRegExpValidator() version_validator.setRegExp(QtCore.QRegExp(r'[v]?[0-9]{1,4}')) +token_validator = QtGui.QRegExpValidator() +token_validator.setRegExp(QtCore.QRegExp(r'.*')) span = { 'start': f'', diff --git a/bookmarks/editor/bookmark_properties.py b/bookmarks/editor/bookmark_properties.py index 4086d191..c50a6d9c 100644 --- a/bookmarks/editor/bookmark_properties.py +++ b/bookmarks/editor/bookmark_properties.py @@ -164,19 +164,19 @@ class BookmarkPropertyEditor(base.BasePropertyEditor): 0: { 'name': 'Bookmark Display Name', 'key': 'bookmark_display_token', - 'validator': base.name_validator, + 'validator': base.token_validator, 'widget': ui.LineEdit, 'placeholder': '{server}/{job}/{root}', - 'description': 'Specify the tokens used to display bookmark items', + 'description': 'Specify the token used to display bookmark items', 'button': '+' }, 1: { 'name': 'Asset Display Name', 'key': 'asset_display_token', - 'validator': base.name_validator, + 'validator': base.token_validator, 'widget': ui.LineEdit, 'placeholder': '{asset}', - 'description': 'Specify the tokens used to display asset items', + 'description': 'Specify the token used to display asset items', 'button': '+' }, }, diff --git a/bookmarks/editor/preferences.py b/bookmarks/editor/preferences.py index f695843d..3412e4dc 100644 --- a/bookmarks/editor/preferences.py +++ b/bookmarks/editor/preferences.py @@ -113,6 +113,16 @@ class PreferenceEditor(base.BasePropertyEditor): 'description': 'Check to hide thumbnail background colors' }, 3: { + 'name': 'Hide item descriptions', + 'key': 'settings/hide_item_descriptions', + 'validator': None, + 'widget': functools.partial( + QtWidgets.QCheckBox, 'Enable' + ), + 'placeholder': 'Check to hide item descriptions', + 'description': 'Check to hide item descriptions', + }, + 4: { 'name': 'Disable Image Thumbnails', 'key': 'settings/disable_oiio', 'validator': None, diff --git a/bookmarks/file_saver/widgets.py b/bookmarks/file_saver/widgets.py index 9d0e4c2c..b9e0269f 100644 --- a/bookmarks/file_saver/widgets.py +++ b/bookmarks/file_saver/widgets.py @@ -328,11 +328,10 @@ def sizeHint(self): class FileNameInfo(QtWidgets.QLabel): - def __init__(self, parent=None): super().__init__(parent=parent) self.setAlignment(QtCore.Qt.AlignCenter) - self.setFixedHeight(common.size(common.size_asset_row_height)) + self.setFixedHeight(common.size(common.size_row_height) * 1.5) self.setSizePolicy( QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.MinimumExpanding diff --git a/bookmarks/items/asset_items.py b/bookmarks/items/asset_items.py index aec7edb3..eaf5135d 100644 --- a/bookmarks/items/asset_items.py +++ b/bookmarks/items/asset_items.py @@ -45,20 +45,9 @@ from .. import log from .. import progress from ..threads import threads +from ..tokens import tokens -def get_display_name(s): - """Manipulate the given file name to a display friendly name. - - Args: - s (str): Source asset item file name. - - Returns: - str: A modified asset item display name. - - """ - return s - class AssetItemViewContextMenu(contextmenu.BaseContextMenu): """The context menu associated with :class:`AssetItemView`.""" @@ -196,7 +185,20 @@ def init_data(self): database.BookmarkTable ) - nth = 1 + # ...and the display token + display_token = db.value( + source, + 'asset_display_token', + database.BookmarkTable + ) + prefix = db.value( + source, + 'prefix', + database.BookmarkTable + ) + config = tokens.get(*p) + + nth = 17 c = 0 for entry in self.item_generator(source): @@ -229,8 +231,26 @@ def init_data(self): if active and active == filename: flags = flags | common.MarkedAsActive - # Beautify the name - name = get_display_name(filename) + display_name = filename + + # Set the display name based on the bookmark item's configuration value + if display_token: + seq, shot = common.get_sequence_and_shot(filepath) + _display_name = config.expand_tokens( + display_token, + server=p[0], + job=p[1], + root=p[2], + asset=filename, + seq=seq, + sequence=seq if seq else '', + shot=shot if shot else '', + prefix=prefix + ) + if tokens.invalid_token not in _display_name: + display_name = _display_name + + parent_path_role = p + (filename,) sort_by_name_role = models.DEFAULT_SORT_BY_NAME_ROLE.copy() @@ -246,7 +266,7 @@ def init_data(self): data[idx] = common.DataDict( { - QtCore.Qt.DisplayRole: name, + QtCore.Qt.DisplayRole: display_name, QtCore.Qt.EditRole: filename, common.PathRole: filepath, QtCore.Qt.SizeHintRole: self.row_size, @@ -277,7 +297,7 @@ def init_data(self): common.SortByNameRole: sort_by_name_role, common.SortByLastModifiedRole: 0, common.SortBySizeRole: 0, - common.SortByTypeRole: name, + common.SortByTypeRole: display_name, # common.IdRole: idx, # @@ -382,7 +402,7 @@ def default_row_size(self): """Returns the default item size. """ - return QtCore.QSize(1, common.size(common.size_asset_row_height)) + return QtCore.QSize(1, common.size(common.size_row_height)) class AssetItemView(views.ThreadedItemView): diff --git a/bookmarks/items/bookmark_items.py b/bookmarks/items/bookmark_items.py index d5f2db72..3ed27101 100644 --- a/bookmarks/items/bookmark_items.py +++ b/bookmarks/items/bookmark_items.py @@ -160,23 +160,27 @@ def init_data(self): job = v['job'] root = v['root'] - display_name = f'{server}/{job}/{root}' + # Get the display name based on the value set in the database - try: - # Get the display name token from the database - db = database.get(server, job, root) - _display_name = db.value(db.source(), 'bookmark_display_token', database.BookmarkTable) + db = database.get(server, job, root) + display_name_token = db.value(db.source(), 'bookmark_display_token', database.BookmarkTable) - # If a token is set, expand it - if _display_name: - config = tokens.get(server, job, root) - _display_name = config.expand_tokens('{job}') - print(_display_name) - if tokens.invalid_token not in _display_name: - display_name = _display_name - except Exception as e: - log.error(e) + # Default display name + display_name = root + # If a token is set, expand it + if display_name_token: + config = tokens.get(server, job, root) + _display_name = config.expand_tokens( + display_name_token, + server=server, + job=job, + root=root, + prefix=db.value(db.source(), 'prefix', database.BookmarkTable) + ) + + if tokens.invalid_token not in _display_name: + display_name = _display_name file_info = QtCore.QFileInfo(k) exists = file_info.exists() @@ -308,7 +312,7 @@ def default_row_size(self): """Returns the default item size. """ - return QtCore.QSize(1, common.size(common.size_bookmark_row_height)) + return QtCore.QSize(1, common.size(common.size_row_height)) def filter_setting_dict_key(self): """The custom dictionary key used to save filter settings to the user settings diff --git a/bookmarks/items/delegate.py b/bookmarks/items/delegate.py index 1a918652..b07d4396 100644 --- a/bookmarks/items/delegate.py +++ b/bookmarks/items/delegate.py @@ -42,16 +42,14 @@ BackgroundRect = 0 IndicatorRect = 1 ThumbnailRect = 2 -AssetNameRect = 3 -AssetDescriptionRect = 4 -AddItemRect = 5 -TodoRect = 6 -RevealRect = 7 -ArchiveRect = 8 -FavouriteRect = 9 -DataRect = 10 -PropertiesRect = 11 -InlineBackgroundRect = 12 +AddItemRect = 3 +TodoRect = 4 +RevealRect = 5 +ArchiveRect = 6 +FavouriteRect = 7 +DataRect = 8 +PropertiesRect = 9 +InlineBackgroundRect = 10 #: Used to paint a DCC icon if the asset name contains any of these names DCC_ICONS = { @@ -214,69 +212,6 @@ def _get_pixmap_rect(rect_height, pixmap_width, pixmap_height): return QtCore.QRect(0, 0, int(w), int(h)) -@functools.lru_cache(maxsize=4194304) -def get_bookmark_text_segments(text, label): - """Caches and returns the text segments used to paint bookmark items. - - Used to mimic rich-text like coloring of individual text elements. - - Args: - text (str): The source text. - label (str): Item's label string. - - Returns: - dict: A dict of (str, QColor) pairs. - - """ - if not text: - return {} - label = label if label else '' - - k = f'{text}{label}' - if k in common.delegate_text_segments: - return common.delegate_text_segments[k] - - text = text.upper().strip().strip('/').strip('\\') - if not text: - return {} - - d = {} - v = text.split('|') - - s_color = common.color(common.color_dark_blue) - - for i, s in enumerate(v): - if i == 0: - c = common.color(common.color_text) - if '/' in s: - s = s.split('/')[-1] - else: - c = s_color - - _v = s.split('/') - for _i, _s in enumerate(_v): - _s = _s.strip() - - d[len(d)] = (_s, c) - if _i < (len(_v) - 1): - d[len(d)] = (' / ', s_color) - if i < (len(v) - 1): - d[len(d)] = (' | ', s_color) - - if label: - d[len(d)] = (' | ', common.color(common.color_dark_background)) - v = label.split('•') - for __i, s in enumerate(v): - s = s.strip() - - d[len(d)] = (s, s_color) - if __i != len(v) - 1: - d[len(d)] = (' ‖ ', s_color) - - common.delegate_text_segments[k] = d - return common.delegate_text_segments[k] - - @functools.lru_cache(maxsize=4194304) def get_asset_text_segments(text, label): """Caches and returns the text segments used to paint asset items. @@ -524,7 +459,7 @@ def get_subdir_cache_key(index, rect): str: The cache key. """ - return f'{index.data(common.PathRole)}_subdir_{rect.size()}' + return f'{index.data(QtCore.Qt.DisplayRole)}_subdir:[{rect.x()},{rect.y()},{rect.width()},{rect.height()}]' def get_subdir_bg_cache_key(index, rect, text_edge=None): @@ -534,8 +469,8 @@ def get_subdir_bg_cache_key(index, rect, text_edge=None): str: The cache key. """ - p = index.data(common.PathRole) - d = index.data(common.DescriptionRole) + p = index.data(QtCore.Qt.DisplayRole) + d = index.data(common.DescriptionRole) if not common.settings.value('settings/hide_item_descriptions') else '' return f'{p}:{d}:[{rect.x()},{rect.y()},{rect.width()},{rect.height()},{text_edge}]' @@ -550,10 +485,10 @@ def get_clickable_cache_key(index, rect): str: The cache key. """ - p = index.data(common.PathRole) + p = index.data(common.QtCore.Qt.DisplayRole) f = index.data(common.FileDetailsRole) - d = index.data(common.DescriptionRole) - return f'{p}{f}{d}[{rect.x()},{rect.y()},{rect.width()},{rect.height()}]' + d = index.data(common.DescriptionRole) if not common.settings.value('settings/hide_item_descriptions') else '' + return f'{p}:{f}:{d}:[{rect.x()},{rect.y()},{rect.width()},{rect.height()}]' def get_description_cache_key(index, rect, button): @@ -568,10 +503,10 @@ def get_description_cache_key(index, rect, button): str: The cache key. """ - p = index.data(common.PathRole) + p = index.data(common.QtCore.Qt.DisplayRole) f = index.data(common.FileDetailsRole) - d = index.data(common.DescriptionRole) - return f'{p}{f}{d}{button}[{rect.x()},{rect.y()},{rect.width()},{rect.height()}]' + d = index.data(common.DescriptionRole) if not common.settings.value('settings/hide_item_descriptions') else '' + return f'{p}:{f}:{d}:{button}:[{rect.x()},{rect.y()},{rect.width()},{rect.height()}]' def get_subdir_rectangles(option, index, rectangles, metrics): @@ -657,14 +592,14 @@ def get_text_segments(index): return {} if len(pp) == 3: - return get_bookmark_text_segments( + return get_asset_text_segments( index.data(QtCore.Qt.DisplayRole), - index.data(common.DescriptionRole) + index.data(common.DescriptionRole) if not common.settings.value('settings/hide_item_descriptions') else '' ) elif len(pp) == 4: return get_asset_text_segments( index.data(QtCore.Qt.DisplayRole), - index.data(common.DescriptionRole) + index.data(common.DescriptionRole) if not common.settings.value('settings/hide_item_descriptions') else '' ) elif len(pp) > 4: return get_file_text_segments( @@ -848,7 +783,7 @@ def setModelData(self, editor, model, index): """Sets the model data for the given index to the given value. """ - text = f'{index.data(common.DescriptionRole)}' + text = index.data(common.DescriptionRole) if text.lower() == editor.text().lower(): return source_path = index.data(common.ParentPathRole) @@ -1022,7 +957,11 @@ def get_paint_arguments(self, painter, option, index, antialiasing=False): archived = flags & common.MarkedAsArchived active = flags & common.MarkedAsActive rectangles = self.get_rectangles(index) - font, metrics = common.font_db.bold_font(common.size(common.size_font_medium)) + + if option.rect.height() < common.size(common.size_row_height) * 1.5: + font, metrics = common.font_db.medium_font(common.size(common.size_font_small)) + else: + font, metrics = common.font_db.bold_font(common.size(common.size_font_medium)) painter.setFont(font) cursor_position = self.parent().viewport().mapFromGlobal(common.cursor.pos()) @@ -1174,12 +1113,10 @@ def paint_name(self, *args): painter.setRenderHint(QtGui.QPainter.Antialiasing, on=True) pp = index.data(common.ParentPathRole) - if len(pp) == 3: - return self.paint_bookmark_name(*args) - elif len(pp) == 4: + if len(pp) <= 4: return self.paint_asset_name( *args, - offset=common.size(common.size_indicator) * 2 + offset=common.size(common.size_indicator) ) elif len(pp) > 4: return self.paint_file_name(*args) @@ -2402,43 +2339,6 @@ def paint_deleted(self, *args, **kwargs): painter.setBrush(common.color(common.color_separator)) painter.drawRect(rect) - @save_painter - def paint_bookmark_name(self, *args): - """Paints name of the ``BookmarkWidget``'s items.""" - ( - rectangles, - painter, - option, - index, - selected, - focused, - active, - archived, - favourite, - hover, - font, - metrics, - cursor_position - ) = args - - if not index.isValid(): - return - if not index.data(QtCore.Qt.DisplayRole): - return - if not index.data(common.ParentPathRole): - return - - # Paint the job as a clickable floating rectangle - bg_rect = draw_subdir_bg_rectangles(rectangles[DataRect].right(), *args) - text = index.data(common.ParentPathRole)[1] - add_clickable_rectangle(index, option, bg_rect, text) - - self.draw_subdir_rectangles(bg_rect, *args) - - self.paint_asset_name( - *args, offset=(bg_rect.right() - rectangles[DataRect].left()) - ) - @save_painter def paint_asset_name(self, *args, offset=0): """Paints name of the ``AssetWidget``'s items.""" @@ -2494,7 +2394,7 @@ def paint_asset_name(self, *args, offset=0): painter.setBrush(common.color(common.color_secondary_text)) text = elided_text( metrics, - 'Double-click to edit...', + 'Double-click to edit description...', QtCore.Qt.ElideRight, description_rect.width(), ) @@ -2732,7 +2632,7 @@ def paint_dcc_icon(self, *args): if not index.data(common.ParentPathRole): return - d = index.data(QtCore.Qt.DisplayRole) + d = index.data(common.PathRole) icon = next((DCC_ICONS[f] for f in DCC_ICONS if f.lower() in d.lower()), None) if not icon: return diff --git a/bookmarks/main.py b/bookmarks/main.py index 18f9762f..b9a703ba 100644 --- a/bookmarks/main.py +++ b/bookmarks/main.py @@ -383,7 +383,7 @@ def _paint_loading(self, painter): pixmaprect = QtCore.QRect(rect) center = pixmaprect.center() - s = common.size(common.size_asset_row_height) * 1.5 + s = common.size(common.size_row_height) * 3 o = common.size(common.size_margin) pixmaprect.setWidth(s) diff --git a/bookmarks/rsc/config.json b/bookmarks/rsc/config.json index 06ae0b9c..50d0e480 100644 --- a/bookmarks/rsc/config.json +++ b/bookmarks/rsc/config.json @@ -31,8 +31,6 @@ "size_font_medium": 12.0, "size_font_large": 16.0, "size_row_height": 34.0, - "size_bookmark_row_height": 52.0, - "size_asset_row_height": 64.0, "size_separator": 1.0, "size_section": 86.0, "size_margin": 18.0, diff --git a/bookmarks/threads/workers.py b/bookmarks/threads/workers.py index cac67d56..7bf2e2c0 100644 --- a/bookmarks/threads/workers.py +++ b/bookmarks/threads/workers.py @@ -432,10 +432,9 @@ def get_bookmark_description(bookmark_row_data): description = f'{sep}{v["description"]}' if v['description'] else '' width = v['width'] if (v['width'] and v['height']) else '' height = f'*{v["height"]}px' if (v['width'] and v['height']) else '' - framerate = f'{sep}{v["framerate"]}fps' if v['framerate'] else '' - prefix = f'{sep}{v["prefix"]}' if v['prefix'] else '' + framerate = f', {v["framerate"]}fps' if v['framerate'] else '' - s = f'{width}{height}{framerate}{prefix}{description}' + s = f'{description}{sep}{width}{height}{framerate}' s = s.replace(sep + sep, sep) s = s.strip(sep).strip() return s diff --git a/bookmarks/tokens/tokens.py b/bookmarks/tokens/tokens.py index b1d84ebf..cb50360d 100644 --- a/bookmarks/tokens/tokens.py +++ b/bookmarks/tokens/tokens.py @@ -747,7 +747,7 @@ def expand_tokens( del kwargs[k] # To avoid KeyErrors when invalid tokens are passed we will replace - # the these with a custom marker + # these with a custom marker, e.g. {invalid_token} # via https://stackoverflow.com/questions/17215400/format-string-unused # -named-arguments return string.Formatter().vformat( From 11ced3275f7f881aaf435d40732e05c2a027b539 Mon Sep 17 00:00:00 2001 From: Gergely Wootsch Date: Mon, 9 Oct 2023 13:36:49 +0200 Subject: [PATCH 05/32] Bump version number. --- README.md | 2 +- bookmarks/__init__.py | 4 ++-- bookmarks/maya/plugin.py | 2 +- docs/src/conf.py | 2 +- docs/src/guide.rst | 2 +- package/CMakeLists.txt | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 2b387a24..b91664b0 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ - +

diff --git a/bookmarks/__init__.py b/bookmarks/__init__.py index a03bc878..fc469554 100644 --- a/bookmarks/__init__.py +++ b/bookmarks/__init__.py @@ -76,7 +76,7 @@ .. |label3| image:: https://img.shields.io/badge/Platform-Windows-lightgrey :height: 18 -.. |label4| image:: https://img.shields.io/badge/Version-v0.8.5-green +.. |label4| image:: https://img.shields.io/badge/Version-v0.8.6-green :height: 18 .. |image1| image:: ./images/active_bookmark.png @@ -100,7 +100,7 @@ __email__ = 'hello@gergely-wootsch.com' #: Project version -__version__ = '0.8.5' +__version__ = '0.8.6' #: Project version __version_info__ = __version__.split('.') diff --git a/bookmarks/maya/plugin.py b/bookmarks/maya/plugin.py index e6e231ee..aa3a003a 100644 --- a/bookmarks/maya/plugin.py +++ b/bookmarks/maya/plugin.py @@ -17,7 +17,7 @@ product = 'bookmarks' __author__ = 'Gergely Wootsch' -__version__ = '0.8.5' +__version__ = '0.8.6' maya_useNewAPI = True diff --git a/docs/src/conf.py b/docs/src/conf.py index e5da1e39..3901e7c1 100644 --- a/docs/src/conf.py +++ b/docs/src/conf.py @@ -33,7 +33,7 @@ author = 'Gergely Wootsch' # The full version, including alpha/beta/rc tags -release = '0.8.5' +release = '0.8.6' html_baseurl = 'https://bookmarks-vfx.com' html_extra_path = [ diff --git a/docs/src/guide.rst b/docs/src/guide.rst index 0a5be9e9..8d95811b 100644 --- a/docs/src/guide.rst +++ b/docs/src/guide.rst @@ -25,7 +25,7 @@ Get Bookmarks The project is hosted on `Github `_. -.. admonition:: Download the latest Windows release: `Bookmarks v0.8.5 `_ +.. admonition:: Download the latest Windows release: `Bookmarks v0.8.6 `_ ☹ Currently, Bookmarks only supports Windows. diff --git a/package/CMakeLists.txt b/package/CMakeLists.txt index e9fc1153..83365d2a 100644 --- a/package/CMakeLists.txt +++ b/package/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.20) project( Bookmarks - VERSION 0.8.5 + VERSION 0.8.6 DESCRIPTION "Bookmarks is a lightweight asset manager for digital artists working in the animation, motion graphics and VFX industries." HOMEPAGE_URL "https://bookmarks-vfx.com" ) From c8fed9adff131a25acbb6575e75715b1a60bbca7 Mon Sep 17 00:00:00 2001 From: Gergely Wootsch Date: Sat, 14 Oct 2023 12:24:46 +0200 Subject: [PATCH 06/32] Preparing for script content changes --- bookmarks/external/ffmpeg_widget.py | 17 ++++++----------- bookmarks/scripts/scripts.json | 13 ++++++++----- .../scripts/{sg_sync.py => sync_asset_data.py} | 0 3 files changed, 14 insertions(+), 16 deletions(-) rename bookmarks/scripts/{sg_sync.py => sync_asset_data.py} (100%) diff --git a/bookmarks/external/ffmpeg_widget.py b/bookmarks/external/ffmpeg_widget.py index 71893992..877139d9 100644 --- a/bookmarks/external/ffmpeg_widget.py +++ b/bookmarks/external/ffmpeg_widget.py @@ -16,7 +16,6 @@ from .. import ui from ..editor import base from ..external import rv -from ..threads import threads from ..tokens import tokens @@ -219,7 +218,7 @@ class FFMpegWidget(base.BasePropertyEditor): def __init__(self, index, parent=None): super().__init__( None, None, None, fallback_thumb='convert', hide_thumbnail_editor=True, buttons=( - 'Convert', 'Cancel'), parent=parent + 'Convert', 'Cancel'), parent=parent ) self._index = index self._connect_settings_save_signals(common.SECTIONS['ffmpeg']) @@ -287,9 +286,9 @@ def save_changes(self): if _f.exists(): if common.show_message( 'File already exists', f'{destination} already exists.\nDo you want to replace it with a new ' - f'version?', buttons=[ - common.YesButton, - common.NoButton], message_type='error', modal=True, ) == QtWidgets.QDialog.Rejected: + f'version?', buttons=[common.YesButton, + common.NoButton], message_type='error', modal=True, + ) == QtWidgets.QDialog.Rejected: return False if not _f.remove(): raise RuntimeError(f'Could not remove {destination}') @@ -312,8 +311,6 @@ def save_changes(self): for f in source_image_paths: images.ImageCache.flush(f) - if not QtCore.QFile(f).remove(): - log.error(f'Failed to remove {f}') if not mov: common.close_message() @@ -340,7 +337,7 @@ def save_changes(self): def preprocess_sequence(self, preconversion_format='jpg'): """Preprocesses the source image sequence. - FFMpeg can't handle missing frames, so we'll check for them and fill in the gaps and convert the source images + FFMpeg can't handle missing frames, so we'll check and fill in the gaps and convert the source images to jpeg images using OpenImageIO if they're not already supported by FFMpeg. Args: @@ -432,8 +429,6 @@ def preprocess_sequence(self, preconversion_format='jpg'): if not QtCore.QFile.copy(source_path, destination_path): raise RuntimeError(f'Could not copy {source_path} to {destination_path}') - raise RuntimeError('needs_conversion') - # Convert the source images to jpeg images using OpenImageIO if needs_conversion: common.message_widget.body_label.setText( @@ -471,7 +466,7 @@ def showEvent(self, event): self.move(corner) self.setGeometry( self.geometry().x() + (item_rect.width() / 2) - ( - self.geometry().width() / 2), self.geometry().y(), self.geometry().width(), self.geometry( + self.geometry().width() / 2), self.geometry().y(), self.geometry().width(), self.geometry( ).height() ) diff --git a/bookmarks/scripts/scripts.json b/bookmarks/scripts/scripts.json index 1ca033f9..824f8e64 100644 --- a/bookmarks/scripts/scripts.json +++ b/bookmarks/scripts/scripts.json @@ -1,20 +1,23 @@ { "0": { - "name": "SG Sync (experimental)", - "module": "sg_sync", - "description": "Syncs editorial data with ShotGrid and server assets", + "name": "Synchronize Asset & Entity Data", + "module": "sync_asset_data", + "description": "Synchronizes bookmark asset items and ShotGrid entities with a source data file.", "needs_active": "root", "icon": "sg" }, "1": { - "name": "Auto-o-matic", + "name": "Build Playlists", "module": "clips", "description": "View clips and export them to editorial", "needs_active": "root", "icon": "sg" }, "2": { - "name": "Export renders to After Effects", + "name": "separator" + }, + "3": { + "name": "Send images to After Effects", "module": "render_layers_to_ae", "description": "View clips and export them to editorial", "needs_active": "task", diff --git a/bookmarks/scripts/sg_sync.py b/bookmarks/scripts/sync_asset_data.py similarity index 100% rename from bookmarks/scripts/sg_sync.py rename to bookmarks/scripts/sync_asset_data.py From 72de3794c43f9d14017de58bdaf30fbaedc73a16 Mon Sep 17 00:00:00 2001 From: Gergely Wootsch Date: Wed, 18 Oct 2023 08:34:07 +0100 Subject: [PATCH 07/32] Script menu changes Tweaks to add support for separators for the Scripts menus. Also, made the Maya scripts menu behave the same way as the main Scripts menus but the code is duplicated at the moment. I'd like to make another pass on this at a later stage to add in the options to display scripts per job or root values. Also, is there any merit to controlling the source of the scripts? E.g. via an environment value, e.g. "BOOKMARKS_SCRIPTS_ROOT", and "BOOKMARKS_MAYA_SCRIPTS_ROOT"? --- bookmarks/actions.py | 30 +- bookmarks/contextmenu.py | 23 +- bookmarks/maya/contextmenu.py | 39 +- bookmarks/maya/export.py | 734 +++++++++--------- bookmarks/maya/scripts/aka_cloth_cache.py | 268 +++++++ .../maya/scripts/aka_cloth_cache_animated.py | 268 +++++++ bookmarks/maya/scripts/scripts.json | 19 +- bookmarks/scripts/scripts.json | 15 +- ..._ae.py => send_images_to_after_effects.py} | 126 ++- 9 files changed, 1095 insertions(+), 427 deletions(-) create mode 100644 bookmarks/maya/scripts/aka_cloth_cache.py create mode 100644 bookmarks/maya/scripts/aka_cloth_cache_animated.py rename bookmarks/scripts/{render_layers_to_ae.py => send_images_to_after_effects.py} (59%) diff --git a/bookmarks/actions.py b/bookmarks/actions.py index 670280d5..27a5a429 100644 --- a/bookmarks/actions.py +++ b/bookmarks/actions.py @@ -1505,20 +1505,36 @@ def pick_thumbnail_from_library(index): ) -def execute_detached(path): +def execute_detached(path, args=None): """Utility function used to execute a file as a detached process. - On Windows, we'll call the give file through the explorer. This is so, that the - new process does not inherit the current environment. + On Windows, we'll call the given file using the file explorer as we want to + avoid the process inheriting the parent process' environment variables. """ if common.get_platform() == common.PlatformWindows: proc = QtCore.QProcess() - proc.setProgram('cmd.exe') - proc.setArguments( - ['/c', 'start', '/i', "%windir%\explorer.exe", os.path.normpath(path)] - ) + + proc.setProgram(os.path.normpath(path)) + if args: + proc.setArguments(args) + + # We don't want to pass on our current environment (we might be calling from inside a DCC) + env = QtCore.QProcessEnvironment.systemEnvironment() + + # But we do want to pass on the currently active items. This information can be used in an + # unsupported DCC to manipulate context + env.insert('BOOKMARKS_ROOT', os.environ['BOOKMARKS_ROOT']) + env.insert('BOOKMARKS_ACTIVE_SERVER', common.active('server')) + env.insert('BOOKMARKS_ACTIVE_JOB', common.active('job')) + env.insert('BOOKMARKS_ACTIVE_ROOT', common.active('root')) + env.insert('BOOKMARKS_ACTIVE_ASSET', common.active('asset')) + env.insert('BOOKMARKS_ACTIVE_TASK', common.active('task')) + + proc.setProcessEnvironment(env) proc.startDetached() + else: + raise NotImplementedError('Not implemented.') @common.debug diff --git a/bookmarks/contextmenu.py b/bookmarks/contextmenu.py index 21e0f7f0..6b03e368 100644 --- a/bookmarks/contextmenu.py +++ b/bookmarks/contextmenu.py @@ -1669,19 +1669,38 @@ def scripts_menu(self): @common.error def _run(name): module = importlib.import_module(f'.scripts.{name}', package=__package__) + + if not hasattr(module, 'run'): + raise RuntimeError(f'Failed to run module: {name} - Missing run() function in {module}!') + module.run() for v in data.values(): - # Check if the script needs_active + if v['name'] == 'separator': + self.separator(menu=self.menu[k]) + continue + + # Check if the script needs active item if 'needs_active' in v and v['needs_active']: if not common.active(v['needs_active'], args=True): continue + # Check if the script needs an application to be set + if 'needs_application' in v and v['needs_application']: + if not common.active('root', args=True): + continue + # Get the bookmark database + db = database.get(*common.active('root', args=True)) + applications = db.value(db.source(), 'applications', database.BookmarkTable) + if not applications: + continue + if not [app for app in applications.values() if v['needs_application'].lower() in app['name'].lower()]: + continue if 'icon' in v and v['icon']: icon = ui.get_icon(v['icon']) else: icon = ui.get_icon('icon') - self.menu[k][v['name']] = { + self.menu[k][key()] = { 'text': v['name'], 'action': functools.partial(_run, v['module']), 'icon': icon, diff --git a/bookmarks/maya/contextmenu.py b/bookmarks/maya/contextmenu.py index 051cc4cd..c8b2a7ca 100644 --- a/bookmarks/maya/contextmenu.py +++ b/bookmarks/maya/contextmenu.py @@ -17,6 +17,7 @@ from .. import common from .. import ui from .. import contextmenu +from .. import database from . import actions from . import export @@ -213,12 +214,12 @@ def show_window_menu(self): } def scripts_menu(self): - """Custom Maya scripts deployed with the Maya module. + """Custom scripts deployed with the Maya module. """ k = 'Scripts' self.menu[k] = collections.OrderedDict() - self.menu[f'{k}:icon'] = ui.get_icon('maya') + self.menu[f'{k}:icon'] = ui.get_icon('icon') p = os.path.normpath(f'{__file__}/../scripts/scripts.json') if not os.path.isfile(p): @@ -227,15 +228,45 @@ def scripts_menu(self): with open(p, 'r') as f: data = json.load(f) + @common.debug + @common.error def _run(name): module = importlib.import_module(f'.scripts.{name}', package=__package__) + + if not hasattr(module, 'run'): + raise RuntimeError(f'Failed to run module: {name} - Missing run() function in {module}!') + module.run() for v in data.values(): - self.menu[k][v['name']] = { + if v['name'] == 'separator': + self.separator(menu=self.menu[k]) + continue + + # Check if the script needs_active + if 'needs_active' in v and v['needs_active']: + if not common.active(v['needs_active'], args=True): + continue + # Check if the script needs_active + if 'needs_application' in v and v['needs_application']: + if not common.active('root', args=True): + continue + # Get the bookmark database + db = database.get(*common.active('root', args=True)) + applications = db.value(db.source(), 'applications', database.BookmarkTable) + if not applications: + continue + if not [app for app in applications.values() if v['needs_application'].lower() in app['name'].lower()]: + continue + if 'icon' in v and v['icon']: + icon = ui.get_icon(v['icon']) + else: + icon = ui.get_icon('icon') + + self.menu[k][contextmenu.key()] = { 'text': v['name'], 'action': functools.partial(_run, v['module']), - 'icon': ui.get_icon('maya'), + 'icon': icon, } def hud_menu(self): diff --git a/bookmarks/maya/export.py b/bookmarks/maya/export.py index c14e7a8a..aa0d8654 100644 --- a/bookmarks/maya/export.py +++ b/bookmarks/maya/export.py @@ -42,34 +42,389 @@ def show(): return common.maya_export_widget + +def export_maya( + destination, outliner_set, start_frame, end_frame, step=1.0 +): + """Main Maya scene export function. + + Args: + start_frame (int): Start frame. + end_frame (int): End frame. + destination (str): Path to the output file. + outliner_set (tuple): A list of transforms contained in a geometry set. + step (float): Frame step. + + """ + common.check_type(destination, str) + common.check_type(outliner_set, (tuple, list)) + common.check_type(start_frame, (int, float)) + common.check_type(end_frame, (int, float)) + common.check_type(step, (float, int)) + + _destination = str(destination) + + cmds.select(outliner_set, replace=True) + + cmds.file( + _destination, + force=True, + preserveReferences=True, + type='mayaAscii', + exportSelected=True, + options="v=0;" + ) + +def export_alembic( + destination, outliner_set, start_frame, end_frame, step=1.0 +): + """Main alembic export function. + + Only shapes, normals and uvs are exported by this implementation. The list + of shapes contained in the `outliner_set` will be rebuilt in the root of + the scene to avoid parenting issues. + + Args: + start_frame (int): Start frame. + end_frame (int): End frame. + destination (str): Path to the output file. + outliner_set (tuple): A list of transforms contained in a geometry set. + step (int, float): Frame step. + + """ + common.check_type(destination, str) + common.check_type(outliner_set, (tuple, list)) + common.check_type(start_frame, (int, float)) + common.check_type(end_frame, (int, float)) + common.check_type(step, (float, int)) + + def _is_intermediate(s): + return cmds.getAttr(f'{s}.intermediateObject') + + def teardown(): + """We will delete the previously created namespace and the objects + contained inside. I wrapped the call into an evalDeferred to let maya + recover after the export and delete the objects more safely. + + """ + + def _teardown(): + if cmds.namespace(exists=mayabase.TEMP_NAMESPACE): + cmds.namespace( + removeNamespace=mayabase.TEMP_NAMESPACE, + deleteNamespaceContent=True + ) + + cmds.evalDeferred(_teardown) + + # We'll need to use the DecomposeMatrix Nodes, let's check if the plugin + # is loaded and ready to use + + world_shapes = [] + valid_shapes = [] + + # First, we will collect the available shapes from the given set + for item in outliner_set: + shapes = cmds.listRelatives(item, fullPath=True) + for shape in shapes: + if _is_intermediate(shape): + continue + + basename = shape.split('|')[-1] + try: + # AbcExport will fail if a transform or a shape node's name is + # not unique. This was suggested on a forum - listing the + # relatives for an object without a unique name should raise a + # ValueError + cmds.listRelatives(basename) + except ValueError as err: + s = f'"{shape}" does not have a unique name. This is not ' \ + f'usually allowed for alembic exports and might cause the ' \ + f'export to fail.' + log.error(s) + + # Cameras don't have mesh nodes, but we still want to export them! + if cmds.nodeType(shape) != 'camera': + if not cmds.attributeQuery('outMesh', node=shape, exists=True): + continue + valid_shapes.append(shape) + + if not valid_shapes: + nodes = '", "'.join(outliner_set) + raise RuntimeError( + f'Could not find any nodes to export in the set. The set contains:\n' + f'"{nodes}"' + ) + + cmds.select(clear=True) + + # Creating a temporary namespace to avoid name-clashes later when we + # duplicate the meshes. We will delete this namespace, and it's contents + # after the export + if cmds.namespace(exists=mayabase.TEMP_NAMESPACE): + cmds.namespace( + removeNamespace=mayabase.TEMP_NAMESPACE, + deleteNamespaceContent=True + ) + cmds.namespace(add=mayabase.TEMP_NAMESPACE) + ns = mayabase.TEMP_NAMESPACE + + world_transforms = [] + + try: + # For meshes, we will create an empty mesh node and connect the + # outMesh and UV attributes from our source. We will also apply the + # source mesh's transform matrix to the newly created mesh + for shape in valid_shapes: + basename = shape.split('|').pop() + if cmds.nodeType(shape) != 'camera': + # Create new empty shape node + world_shape = cmds.createNode('mesh', name=f'{ns}:{basename}') + + # outMesh -> inMesh + cmds.connectAttr( + f'{shape}.outMesh', + f'{world_shape}.inMesh', + force=True + ) + # uvSet -> uvSet + cmds.connectAttr( + f'{shape}.uvSet', + f'{world_shape}.uvSet', + force=True + ) + + # worldMatrix -> transform + decompose_matrix = cmds.createNode( + 'decomposeMatrix', + name=f'{ns}:decomposeMatrix#' + ) + cmds.connectAttr( + f'{shape}.worldMatrix[0]', + f'{decompose_matrix}.inputMatrix', + force=True + ) + + transform = cmds.listRelatives( + world_shape, + fullPath=True, + type='transform', + parent=True + )[0] + world_transforms.append(transform) + + cmds.connectAttr( + f'{decompose_matrix}.outputTranslate', + f'{transform}.translate', + force=True + ) + cmds.connectAttr( + f'{decompose_matrix}.outputRotate', + f'{transform}.rotate', + force=True + ) + cmds.connectAttr( + f'{decompose_matrix}.outputScale', + f'{transform}.scale', + force=True + ) + else: + world_shape = shape + world_transforms.append( + cmds.listRelatives( + world_shape, + fullPath=True, + type='transform', + parent=True + )[0] + ) + world_shapes.append(world_shape) + except: + teardown() + raise RuntimeError('Failed to prepare scene.') + + try: + # Our custom progress callback + perframecallback = f'"from bookmarks.maya import base;' \ + f'base.report_export_progress(' \ + f'{start_frame}, #FRAME#, {end_frame}, ' \ + f'{time.time()})"' + + # Build the export command + cmd = '{f} {fr} {s} {uv} {ws} {wv} {wuvs} {wcs} {wfs} {sn} {rt} {df} {pfc} {ro}' + cmd = cmd.format( + f=f'-file "{destination}"', + fr=f'-framerange {start_frame} {end_frame}', + s=f'-step {step}', + uv='-uvWrite', + ws='-worldSpace', + wv='-writeVisibility', + # eu='-eulerFilter', + wuvs='-writeuvsets', + wcs='-writeColorSets', + wfs='-writeFaceSets', + sn='-stripNamespaces', + rt=f'-root {" -root ".join(world_transforms)}', + df='-dataFormat ogawa', + pfc=f'-pythonperframecallback {perframecallback}', + ro='-renderableOnly' + ) + s = f'Alembic Export Job Arguments:\n{cmd}' + log.success(s) + cmds.AbcExport(jobArg=cmd) + log.success(f'{destination} exported successfully.') + except Exception: + log.error('The alembic export failed.') + raise + finally: + teardown() + +def export_ass( + destination, outliner_set, start_frame, end_frame, step=1.0 +): + """Main Arnold ASS export function. + + Args: + start_frame (int): Start frame. + end_frame (int): End frame. + destination (str): Path to the output file. + outliner_set (tuple): A list of transforms contained in a geometry set. + step (float, int): Frame step. + + """ + common.check_type(destination, str) + common.check_type(outliner_set, (tuple, list)) + common.check_type(start_frame, (int, float)) + common.check_type(end_frame, (int, float)) + common.check_type(step, (float, int)) + + try: + import arnold + except ImportError: + raise ImportError('Could not find arnold.') + + # Let's get the first renderable camera. This is a bit of a leap of faith but + # ideally there's only one renderable camera in the scene. + cams = cmds.ls(cameras=True) + cam = None + for cam in cams: + if cmds.getAttr(f'{cam}.renderable'): + break + + cmds.select(outliner_set, replace=True) + + ext = destination.split('.')[-1] + _destination = str(destination) + start_time = time.time() + + for fr in range(start_frame, end_frame + 1): + cmds.currentTime(fr, edit=True) + + if not start_frame == end_frame: + # Create a mock version, if it does not exist + open(destination, 'a').close() + _destination = destination.replace(f'.{ext}', '') + _destination += '_' + _destination += str(fr).zfill(mayabase.DefaultPadding) + _destination += '.' + _destination += ext + + cmds.arnoldExportAss( + f=_destination, + cam=cam, + s=True, # selected + mask=arnold.AI_NODE_CAMERA | + arnold.AI_NODE_SHAPE | + arnold.AI_NODE_SHADER | + arnold.AI_NODE_OVERRIDE | + arnold.AI_NODE_LIGHT + ) + + mayabase.report_export_progress(start_frame, fr, end_frame, start_time) + +def export_obj( + destination, outliner_set, start_frame, end_frame, step=1.0 +): + """Main obj export function. + + Args: + start_frame (int): Start frame. + end_frame (int): End frame. + destination (str): Path to the output file. + outliner_set (tuple): A list of transforms contained in a geometry set. + step (float, int): Frame step. + + """ + common.check_type(destination, str) + common.check_type(outliner_set, (tuple, list)) + common.check_type(start_frame, (int, float)) + common.check_type(end_frame, (int, float)) + common.check_type(step, (float, int)) + + ext = destination.split('.')[-1] + _destination = str(destination) + start_time = time.time() + + cmds.select(outliner_set, replace=True) + + for fr in range(start_frame, end_frame + 1): + cmds.currentTime(fr, edit=True) + + if not start_frame == end_frame: + # Create a mock version, if it does not exist + open(destination, 'a').close() + _destination = destination.replace(f'.{ext}', '') + _destination += '_' + _destination += str(fr).zfill(mayabase.DefaultPadding) + _destination += '.' + _destination += ext + + if ( + QtCore.QFileInfo(_destination).exists() and + not QtCore.QFile(_destination).remove() + ): + raise RuntimeError(f'Failed to remove {_destination}') + + cmds.file( + _destination, + preserveReferences=True, + type='OBJexport', + exportSelected=True, + options='groups=1;ptgroups=1;materials=1;smoothing=1; normals=1' + ) + + mayabase.report_export_progress(start_frame, fr, end_frame, start_time) + + + #: Maya cache export presets PRESETS = { 'alembic': { 'name': 'Alembic', 'extension': 'abc', 'plugins': ('AbcExport.mll', 'matrixNodes.mll'), - 'action': 'export_alembic', + 'action': export_alembic, 'ogs_pause': True, }, 'ass': { 'name': 'Arnold ASS', 'extension': 'ass', 'plugins': ('mtoa.mll',), - 'action': 'export_ass', + 'action': export_ass, 'ogs_pause': True, }, 'obj': { 'name': 'OBJ', 'extension': 'obj', 'plugins': ('objExport.mll',), - 'action': 'export_obj', + 'action': export_obj, 'ogs_pause': True, }, 'ma': { 'name': 'Maya Scene', 'extension': 'ma', 'plugins': (), - 'action': 'export_maya', + 'action': export_maya, 'ogs_pause': False, }, } @@ -359,7 +714,7 @@ def save_changes(self): self.progress_widget.setRange(int(start), int(end)) self.progress_widget.open() - action = getattr(self, PRESETS[k]['action']) + action = PRESETS[k]['action'] action(file_path, items, int(start), int(end)) common.signals.fileAdded.emit(file_path) @@ -435,372 +790,3 @@ def sizeHint(self): common.size(common.size_width) * 0.66, common.size(common.size_height * 1.2) ) - - def export_maya( - self, destination, outliner_set, start_frame, end_frame, step=1.0 - ): - """Main Maya scene export function. - - Args: - start_frame (int): Start frame. - end_frame (int): End frame. - destination (str): Path to the output file. - outliner_set (tuple): A list of transforms contained in a geometry set. - step (float): Frame step. - - """ - common.check_type(destination, str) - common.check_type(outliner_set, (tuple, list)) - common.check_type(start_frame, (int, float)) - common.check_type(end_frame, (int, float)) - common.check_type(step, (float, int)) - - _destination = str(destination) - - cmds.select(outliner_set, replace=True) - - cmds.file( - _destination, - force=True, - preserveReferences=True, - type='mayaAscii', - exportSelected=True, - options="v=0;" - ) - - def export_alembic( - self, destination, outliner_set, start_frame, end_frame, step=1.0 - ): - """Main alembic export function. - - Only shapes, normals and uvs are exported by this implementation. The list - of shapes contained in the `outliner_set` will be rebuilt in the root of - the scene to avoid parenting issues. - - Args: - start_frame (int): Start frame. - end_frame (int): End frame. - destination (str): Path to the output file. - outliner_set (tuple): A list of transforms contained in a geometry set. - step (int, float): Frame step. - - """ - common.check_type(destination, str) - common.check_type(outliner_set, (tuple, list)) - common.check_type(start_frame, (int, float)) - common.check_type(end_frame, (int, float)) - common.check_type(step, (float, int)) - - def _is_intermediate(s): - return cmds.getAttr(f'{s}.intermediateObject') - - def teardown(): - """We will delete the previously created namespace and the objects - contained inside. I wrapped the call into an evalDeferred to let maya - recover after the export and delete the objects more safely. - - """ - - def _teardown(): - cmds.namespace( - removeNamespace=mayabase.TEMP_NAMESPACE, - deleteNamespaceContent=True - ) - - cmds.evalDeferred(_teardown) - - # We'll need to use the DecomposeMatrix Nodes, let's check if the plugin - # is loaded and ready to use - - world_shapes = [] - valid_shapes = [] - - # First, we will collect the available shapes from the given set - for item in outliner_set: - shapes = cmds.listRelatives(item, fullPath=True) - for shape in shapes: - if _is_intermediate(shape): - continue - - basename = shape.split('|')[-1] - try: - # AbcExport will fail if a transform or a shape node's name is - # not unique. This was suggested on a forum - listing the - # relatives for an object without a unique name should raise a - # ValueError - cmds.listRelatives(basename) - except ValueError as err: - s = f'"{shape}" does not have a unique name. This is not ' \ - f'usually allowed for alembic exports and might cause the ' \ - f'export to fail.' - log.error(s) - - # Camera's don't have mesh nodes, but we still want to export them! - if cmds.nodeType(shape) != 'camera': - if not cmds.attributeQuery('outMesh', node=shape, exists=True): - continue - valid_shapes.append(shape) - - if not valid_shapes: - nodes = '", "'.join(outliner_set) - raise RuntimeError( - f'Could not find any nodes to export in the set. The set contains:\n' - f'"{nodes}"' - ) - - cmds.select(clear=True) - - # Creating a temporary namespace to avoid name-clashes later when we - # duplicate the meshes. We will delete this namespace, and it's contents - # after the export - if cmds.namespace(exists=mayabase.TEMP_NAMESPACE): - cmds.namespace( - removeNamespace=mayabase.TEMP_NAMESPACE, - deleteNamespaceContent=True - ) - cmds.namespace(add=mayabase.TEMP_NAMESPACE) - ns = mayabase.TEMP_NAMESPACE - - world_transforms = [] - - try: - # For meshes, we will create an empty mesh node and connect the - # outMesh and UV attributes from our source. We will also apply the - # source mesh's transform matrix to the newly created mesh - for shape in valid_shapes: - basename = shape.split('|').pop() - if cmds.nodeType(shape) != 'camera': - # Create new empty shape node - world_shape = cmds.createNode('mesh', name=f'{ns}:{basename}') - - # outMesh -> inMesh - cmds.connectAttr( - f'{shape}.outMesh', - f'{world_shape}.inMesh', - force=True - ) - # uvSet -> uvSet - cmds.connectAttr( - f'{shape}.uvSet', - f'{world_shape}.uvSet', - force=True - ) - - # worldMatrix -> transform - decompose_matrix = cmds.createNode( - 'decomposeMatrix', - name=f'{ns}:decomposeMatrix#' - ) - cmds.connectAttr( - f'{shape}.worldMatrix[0]', - f'{decompose_matrix}.inputMatrix', - force=True - ) - - transform = cmds.listRelatives( - world_shape, - fullPath=True, - type='transform', - parent=True - )[0] - world_transforms.append(transform) - - cmds.connectAttr( - f'{decompose_matrix}.outputTranslate', - f'{transform}.translate', - force=True - ) - cmds.connectAttr( - f'{decompose_matrix}.outputRotate', - f'{transform}.rotate', - force=True - ) - cmds.connectAttr( - f'{decompose_matrix}.outputScale', - f'{transform}.scale', - force=True - ) - else: - world_shape = shape - world_transforms.append( - cmds.listRelatives( - world_shape, - fullPath=True, - type='transform', - parent=True - )[0] - ) - world_shapes.append(world_shape) - except: - teardown() - raise RuntimeError('Failed to prepare scene.') - - try: - # Our custom progress callback - perframecallback = f'"from bookmarks.maya import base;' \ - f'base.report_export_progress(' \ - f'{start_frame}, #FRAME#, {end_frame}, ' \ - f'{time.time()})"' - - # Build the export command - cmd = '{f} {fr} {s} {uv} {ws} {wv} {wuvs} {wcs} {wfs} {sn} {rt} {df} {pfc} {ro}' - cmd = cmd.format( - f=f'-file "{destination}"', - fr=f'-framerange {start_frame} {end_frame}', - s=f'-step {step}', - uv='-uvWrite', - ws='-worldSpace', - wv='-writeVisibility', - # eu='-eulerFilter', - wuvs='-writeuvsets', - wcs='-writeColorSets', - wfs='-writeFaceSets', - sn='-stripNamespaces', - rt=f'-root {" -root ".join(world_transforms)}', - df='-dataFormat ogawa', - pfc=f'-pythonperframecallback {perframecallback}', - ro='-renderableOnly' - ) - s = f'Alembic Export Job Arguments:\n{cmd}' - log.success(s) - cmds.AbcExport(jobArg=cmd) - log.success(f'{destination} exported successfully.') - except Exception: - log.error('The alembic export failed.') - raise - finally: - teardown() - - def export_ass( - self, destination, outliner_set, start_frame, end_frame, step=1.0 - ): - """Main Arnold ASS export function. - - Args: - start_frame (int): Start frame. - end_frame (int): End frame. - destination (str): Path to the output file. - outliner_set (tuple): A list of transforms contained in a geometry set. - step (float, int): Frame step. - - """ - common.check_type(destination, str) - common.check_type(outliner_set, (tuple, list)) - common.check_type(start_frame, (int, float)) - common.check_type(end_frame, (int, float)) - common.check_type(step, (float, int)) - - try: - import arnold - except ImportError: - raise ImportError('Could not find arnold.') - - # Let's get the first renderable camera. This is a bit of a leap of faith but - # ideally there's only one renderable camera in the scene. - cams = cmds.ls(cameras=True) - cam = None - for cam in cams: - if cmds.getAttr(f'{cam}.renderable'): - break - - cmds.select(outliner_set, replace=True) - - ext = destination.split('.')[-1] - _destination = str(destination) - start_time = time.time() - - for fr in range(start_frame, end_frame + 1): - QtWidgets.QApplication.instance().processEvents(QtCore.QEventLoop.ExcludeUserInputEvents) - if self._interrupt_requested: - self._interrupt_requested = False - return - - cmds.currentTime(fr, edit=True) - if self.progress_widget.wasCanceled(): - return - else: - self.progress_widget.setValue(fr) - - if not start_frame == end_frame: - # Create a mock version, if it does not exist - open(destination, 'a').close() - _destination = destination.replace(f'.{ext}', '') - _destination += '_' - _destination += str(fr).zfill(mayabase.DefaultPadding) - _destination += '.' - _destination += ext - - cmds.arnoldExportAss( - f=_destination, - cam=cam, - s=True, # selected - mask=arnold.AI_NODE_CAMERA | - arnold.AI_NODE_SHAPE | - arnold.AI_NODE_SHADER | - arnold.AI_NODE_OVERRIDE | - arnold.AI_NODE_LIGHT - ) - - mayabase.report_export_progress(start_frame, fr, end_frame, start_time) - - def export_obj( - self, destination, outliner_set, start_frame, end_frame, step=1.0 - ): - """Main obj export function. - - Args: - start_frame (int): Start frame. - end_frame (int): End frame. - destination (str): Path to the output file. - outliner_set (tuple): A list of transforms contained in a geometry set. - step (float, int): Frame step. - - """ - common.check_type(destination, str) - common.check_type(outliner_set, (tuple, list)) - common.check_type(start_frame, (int, float)) - common.check_type(end_frame, (int, float)) - common.check_type(step, (float, int)) - - ext = destination.split('.')[-1] - _destination = str(destination) - start_time = time.time() - - cmds.select(outliner_set, replace=True) - - for fr in range(start_frame, end_frame + 1): - QtWidgets.QApplication.instance().processEvents(QtCore.QEventLoop.ExcludeUserInputEvents) - if self._interrupt_requested: - self._interrupt_requested = False - return - - cmds.currentTime(fr, edit=True) - if self.progress_widget.wasCanceled(): - return - else: - self.progress_widget.setValue(fr) - - if not start_frame == end_frame: - # Create a mock version, if it does not exist - open(destination, 'a').close() - _destination = destination.replace(f'.{ext}', '') - _destination += '_' - _destination += str(fr).zfill(mayabase.DefaultPadding) - _destination += '.' - _destination += ext - - if ( - QtCore.QFileInfo(_destination).exists() and - not QtCore.QFile(_destination).remove() - ): - raise RuntimeError(f'Failed to remove {_destination}') - - cmds.file( - _destination, - preserveReferences=True, - type='OBJexport', - exportSelected=True, - options='groups=1;ptgroups=1;materials=1;smoothing=1; normals=1' - ) - - mayabase.report_export_progress(start_frame, fr, end_frame, start_time) diff --git a/bookmarks/maya/scripts/aka_cloth_cache.py b/bookmarks/maya/scripts/aka_cloth_cache.py new file mode 100644 index 00000000..523f2f74 --- /dev/null +++ b/bookmarks/maya/scripts/aka_cloth_cache.py @@ -0,0 +1,268 @@ +"""Export script for Odyssey cloth sims. + +The scripts automates the animation cache export from Maya. + +It performs the follow steps: + - Saves the current animation to studio library + - Removes the animation from the body and root controllers + - Adds a preroll with a reset pose + - Saves the animation caches + +Gergely Wootsch. +hello@gergely-wootsch.com +Studio Aka, 2023 October + +""" +import os + +import maya.cmds as cmds +from PySide2 import QtCore +from ... import common +from ... import database +from .. import base as mayabase +from .. import export +from . import aka_make_export_sets +from ...tokens import tokens + + +studiolibrary_dir = f'{common.active("server")}/{common.active("job")}/070_Assets/Character/studiolibrary' +reset_pose = f'{studiolibrary_dir}/Characters/IbogaineMarcus/Reset/A-Pose-v2-FullFK.pose/pose.json' + +cache_destination_dir = '{studiolibrary_dir}/Shots/MAB/{asset0}_{shot}' + +namespace = 'IbogaineMarcus_01' +controllers_set = f'{namespace}:rig_controllers_grp' + +# These controllers should not be animated when exporting the caches +exclude_controllers1 = [ + f'{namespace}:body_C0_ctl', + f'{namespace}:world_ctl', + f'{namespace}:root_C0_ctl' +] + +# These controllers should be excluded from the a-pose +exclude_controllers2 = [ + f'{namespace}:legUI_L0_ctl', + f'{namespace}:armUI_R0_ctl', + f'{namespace}:faceUI_C0_ctl', + f'{namespace}:legUI_R0_ctl', + f'{namespace}:spineUI_C0_ctl', + f'{namespace}:armUI_L0_ctl' +] + + +def get_cache_path(set_name, ext, makedir=True): + """Get the path of the output cache file. + + """ + workspace = cmds.workspace(q=True, fn=True) + + export_dir = mayabase.DEFAULT_CACHE_DIR.format( + export_dir=tokens.get_folder(tokens.CacheFolder), + ext=tokens.get_subfolder(tokens.CacheFolder, ext) + ) + file_path = mayabase.CACHE_PATH.format( + workspace=workspace, + export_dir=export_dir, + set=set_name, + ext=ext + ) + file_path = mayabase.sanitize_namespace(file_path) + + file_info = QtCore.QFileInfo(file_path) + _version = 1 + while True: + version = 'v' + f'{_version}'.zfill(3) + + file_path = '{dir}/{basename}_{version}.{ext}'.format( + dir=file_info.dir().path(), + basename=file_info.completeBaseName(), + version=version, + ext=file_info.suffix() + ) + + if not QtCore.QFileInfo(file_path).exists(): + break + if _version >= 999: + break + + _version += 1 + + if makedir: + QtCore.QFileInfo(file_path).dir().mkpath('.') + return file_path + + +@common.debug +@common.error +def run(): + try: + import mutils + except ImportError: + raise RuntimeError('Could not export caches. Is Studio Library installed?') + + config = tokens.get(*common.active('root', args=True)) + + seq, shot = common.get_sequence_and_shot(common.active('asset')) + destination_dir = config.expand_tokens( + cache_destination_dir, + asset=common.active('asset'), + shot=shot, + sequence=seq, + studiolibrary_dir=studiolibrary_dir + ) + + # Save the current options + timeline_start = cmds.playbackOptions(animationStartTime=True, query=True) + timeline_end = cmds.playbackOptions(animationEndTime=True, query=True) + animation_start = cmds.playbackOptions(minTime=True, query=True) + animation_end = cmds.playbackOptions(maxTime=True, query=True) + + db = database.get(*common.active('root', args=True)) + cut_in = db.value(common.active('asset', path=True), 'cut_in', database.AssetTable) + cut_out = db.value(common.active('asset', path=True), 'cut_out', database.AssetTable) + + cmds.select(clear=True) + + # Create the export groups + aka_make_export_sets.run() + + # Set the cache range up + cmds.playbackOptions(animationStartTime=-50, minTime=-50, animationEndTime=cut_out, maxTime=cut_out) + cmds.currentTime(cut_in) + + for n in (cut_in, cut_out): + cmds.currentTime(n) + cmds.select(cmds.sets(controllers_set, query=True), replace=True, ne=True) + cmds.setKeyframe( + cmds.sets(controllers_set, query=True), + breakdown=False, + preserveCurveShape=False, + hierarchy='none', + controlPoints=False, + shape=True, + ) + + for node in cmds.sets(controllers_set, query=True): + attrs = cmds.listAnimatable(node) + if not attrs: + continue + for attr in attrs: + if not cmds.keyframe(attr, query=True, time=(cut_in - 51, cut_in - 1)): + continue + cmds.cutKey(attr, time=(cut_in - 51, cut_in - 1)) + + cmds.currentTime(cut_in) + + # Save the full animation + try: + mutils.saveAnim( + cmds.sets(controllers_set, query=True), + os.path.normpath(f'{destination_dir}/IbogaineMarcus_fullanim.anim'), + time=(cut_in, cut_out), + bakeConnected=False, + metadata='' + ) + except UnicodeDecodeError as e: + print(e) + + # Save the animation start pose + try: + mutils.savePose( + os.path.normpath(f'{destination_dir}/IbogaineMarcus_animstart.pose/pose.json'), + exclude_controllers1, + ) + except UnicodeDecodeError as e: + print(e) + + # Parent a null locator to the hip bake it to world and save the world animation for alter use + locator = cmds.spaceLocator(name="nullLocator")[0] + constraint = cmds.parentConstraint('IbogaineMarcus_01:body_C0_ctl', locator, maintainOffset=False)[0] + cmds.bakeResults( + locator, + time=(cut_in, cut_out), + simulation=True, + sampleBy=1, + oversamplingRate=1, + disableImplicitControl=True, + preserveOutsideKeys=True, + sparseAnimCurveBake=False, + removeBakedAttributeFromLayer=False, + bakeOnOverrideLayer=False, + minimizeRotation=True, + controlPoints=False, + shape=True + ) + + # Save the hip animation + try: + mutils.saveAnim( + [locator, ], + os.path.normpath(f'{destination_dir}/IbogaineMarcus_hip.anim'), + time=(cut_in, cut_out), + bakeConnected=False, + metadata='' + ) + except UnicodeDecodeError as e: + print(e) + + cmds.delete(constraint) + cmds.delete(locator) + + # Remove animation from body and world controllers... + for obj in exclude_controllers1: + animated_attrs = cmds.listAnimatable(obj) + animated_attrs = animated_attrs if animated_attrs else [] + for attr in animated_attrs: + cmds.cutKey(attr, clear=True) + + # ...but keep the pose at cut_in + cmds.currentTime(cut_in) + mutils.loadPose( + os.path.normpath(f'{destination_dir}/IbogaineMarcus_animstart.pose/pose.json'), + key=True + ) + + cmds.currentTime(cut_in - 51) + + mutils.loadPose( + os.path.normpath(reset_pose), + objects=list( + set(cmds.sets(controllers_set, query=True)) - set(exclude_controllers1) - set( + exclude_controllers2 + ) + ), + key=True, + namespaces=[namespace, ] + ) + + export.export_maya( + get_cache_path('camera_export', 'ma'), + cmds.sets('camera_export', query=True), cut_in, cut_out, step=1.0 + ) + export.export_alembic( + get_cache_path('camera_export', 'abc'), + cmds.sets('camera_export', query=True), cut_in, cut_out, step=1.0 + ) + export.export_alembic( + get_cache_path('IbogaineMarcus_body_export', 'abc'), + cmds.sets('IbogaineMarcus_body_export', query=True), cut_in - 51, cut_out, step=1.0 + ) + export.export_alembic( + get_cache_path('IbogaineMarcus_cloth_export', 'abc'), + cmds.sets('IbogaineMarcus_cloth_export', query=True), cut_in - 51, cut_in - 51, step=1.0 + ) + export.export_alembic( + get_cache_path('IbogaineMarcus_extra_export', 'abc'), + cmds.sets('IbogaineMarcus_extra_export', query=True), cut_in - 51, cut_out, step=1.0 + ) + + mutils.loadAnims( + [os.path.normpath(f'{destination_dir}/IbogaineMarcus_fullanim.anim'), ], + objects=cmds.sets(controllers_set, query=True), + currentTime=False, + option='replaceCompletely', + namespaces=[namespace, ] + ) + + cmds.playbackOptions(animationStartTime=cut_in, minTime=cut_in, animationEndTime=cut_out, maxTime=cut_out) diff --git a/bookmarks/maya/scripts/aka_cloth_cache_animated.py b/bookmarks/maya/scripts/aka_cloth_cache_animated.py new file mode 100644 index 00000000..a44bf13c --- /dev/null +++ b/bookmarks/maya/scripts/aka_cloth_cache_animated.py @@ -0,0 +1,268 @@ +"""Export script for Odyssey cloth sims. + +The scripts automates the animation cache export from Maya. + +It performs the follow steps: + - Saves the current animation to studio library + - Removes the animation from the body and root controllers + - Adds a preroll with a reset pose + - Saves the animation caches + +Gergely Wootsch. +hello@gergely-wootsch.com +Studio Aka, 2023 October + +""" +import os + +import maya.cmds as cmds +from PySide2 import QtCore +from ... import common +from ... import database +from .. import base as mayabase +from .. import export +from . import aka_make_export_sets +from ...tokens import tokens + + +studiolibrary_dir = f'{common.active("server")}/{common.active("job")}/070_Assets/Character/studiolibrary' +reset_pose = f'{studiolibrary_dir}/Characters/IbogaineMarcus/Reset/A-Pose-v2-FullFK.pose/pose.json' + +cache_destination_dir = '{studiolibrary_dir}/Shots/MAB/{asset0}_{shot}' + +namespace = 'IbogaineMarcus_01' +controllers_set = f'{namespace}:rig_controllers_grp' + +# These controllers should not be animated when exporting the caches +exclude_controllers1 = [ + f'{namespace}:body_C0_ctl', + f'{namespace}:world_ctl', + f'{namespace}:root_C0_ctl' +] + +# These controllers should be excluded from the a-pose +exclude_controllers2 = [ + f'{namespace}:legUI_L0_ctl', + f'{namespace}:armUI_R0_ctl', + f'{namespace}:faceUI_C0_ctl', + f'{namespace}:legUI_R0_ctl', + f'{namespace}:spineUI_C0_ctl', + f'{namespace}:armUI_L0_ctl' +] + + +def get_cache_path(set_name, ext, makedir=True): + """Get the path of the output cache file. + + """ + workspace = cmds.workspace(q=True, fn=True) + + export_dir = mayabase.DEFAULT_CACHE_DIR.format( + export_dir=tokens.get_folder(tokens.CacheFolder), + ext=tokens.get_subfolder(tokens.CacheFolder, ext) + ) + file_path = mayabase.CACHE_PATH.format( + workspace=workspace, + export_dir=export_dir, + set=set_name, + ext=ext + ) + file_path = mayabase.sanitize_namespace(file_path) + + file_info = QtCore.QFileInfo(file_path) + _version = 1 + while True: + version = 'v' + f'{_version}'.zfill(3) + + file_path = '{dir}/{basename}_{version}.{ext}'.format( + dir=file_info.dir().path(), + basename=file_info.completeBaseName(), + version=version, + ext=file_info.suffix() + ) + + if not QtCore.QFileInfo(file_path).exists(): + break + if _version >= 999: + break + + _version += 1 + + if makedir: + QtCore.QFileInfo(file_path).dir().mkpath('.') + return file_path + + +@common.debug +@common.error +def run(): + try: + import mutils + except ImportError: + raise RuntimeError('Could not export caches. Is Studio Library installed?') + + config = tokens.get(*common.active('root', args=True)) + + seq, shot = common.get_sequence_and_shot(common.active('asset')) + destination_dir = config.expand_tokens( + cache_destination_dir, + asset=common.active('asset'), + shot=shot, + sequence=seq, + studiolibrary_dir=studiolibrary_dir + ) + + # Save the current options + timeline_start = cmds.playbackOptions(animationStartTime=True, query=True) + timeline_end = cmds.playbackOptions(animationEndTime=True, query=True) + animation_start = cmds.playbackOptions(minTime=True, query=True) + animation_end = cmds.playbackOptions(maxTime=True, query=True) + + db = database.get(*common.active('root', args=True)) + cut_in = db.value(common.active('asset', path=True), 'cut_in', database.AssetTable) + cut_out = db.value(common.active('asset', path=True), 'cut_out', database.AssetTable) + + cmds.select(clear=True) + + # Create the export groups + aka_make_export_sets.run() + + # Set the cache range up + cmds.playbackOptions(animationStartTime=-50, minTime=-50, animationEndTime=cut_out, maxTime=cut_out) + cmds.currentTime(cut_in) + + for n in (cut_in, cut_out): + cmds.currentTime(n) + cmds.select(cmds.sets(controllers_set, query=True), replace=True, ne=True) + cmds.setKeyframe( + cmds.sets(controllers_set, query=True), + breakdown=False, + preserveCurveShape=False, + hierarchy='none', + controlPoints=False, + shape=True, + ) + + for node in cmds.sets(controllers_set, query=True): + attrs = cmds.listAnimatable(node) + if not attrs: + continue + for attr in attrs: + if not cmds.keyframe(attr, query=True, time=(cut_in - 51, cut_in - 1)): + continue + cmds.cutKey(attr, time=(cut_in - 51, cut_in - 1)) + + cmds.currentTime(cut_in) + + # Save the full animation + try: + mutils.saveAnim( + cmds.sets(controllers_set, query=True), + os.path.normpath(f'{destination_dir}/IbogaineMarcus_fullanim.anim'), + time=(cut_in, cut_out), + bakeConnected=False, + metadata='' + ) + except UnicodeDecodeError as e: + print(e) + + # Save the animation start pose + try: + mutils.savePose( + os.path.normpath(f'{destination_dir}/IbogaineMarcus_animstart.pose/pose.json'), + exclude_controllers1, + ) + except UnicodeDecodeError as e: + print(e) + + # Parent a null locator to the hip bake it to world and save the world animation for alter use + locator = cmds.spaceLocator(name="nullLocator")[0] + constraint = cmds.parentConstraint('IbogaineMarcus_01:body_C0_ctl', locator, maintainOffset=False)[0] + cmds.bakeResults( + locator, + time=(cut_in, cut_out), + simulation=True, + sampleBy=1, + oversamplingRate=1, + disableImplicitControl=True, + preserveOutsideKeys=True, + sparseAnimCurveBake=False, + removeBakedAttributeFromLayer=False, + bakeOnOverrideLayer=False, + minimizeRotation=True, + controlPoints=False, + shape=True + ) + + # Save the hip animation + try: + mutils.saveAnim( + [locator, ], + os.path.normpath(f'{destination_dir}/IbogaineMarcus_hip.anim'), + time=(cut_in, cut_out), + bakeConnected=False, + metadata='' + ) + except UnicodeDecodeError as e: + print(e) + + cmds.delete(constraint) + cmds.delete(locator) + + # Remove animation from body and world controllers... + # for obj in exclude_controllers1: + # animated_attrs = cmds.listAnimatable(obj) + # animated_attrs = animated_attrs if animated_attrs else [] + # for attr in animated_attrs: + # cmds.cutKey(attr, clear=True) + + # ...but keep the pose at cut_in + cmds.currentTime(cut_in) + mutils.loadPose( + os.path.normpath(f'{destination_dir}/IbogaineMarcus_animstart.pose/pose.json'), + key=True + ) + + cmds.currentTime(cut_in - 51) + + mutils.loadPose( + os.path.normpath(reset_pose), + objects=list( + set(cmds.sets(controllers_set, query=True)) - set(exclude_controllers1) - set( + exclude_controllers2 + ) + ), + key=True, + namespaces=[namespace, ] + ) + + export.export_maya( + get_cache_path('camera_export', 'ma'), + cmds.sets('camera_export', query=True), cut_in, cut_out, step=1.0 + ) + export.export_alembic( + get_cache_path('camera_export', 'abc'), + cmds.sets('camera_export', query=True), cut_in, cut_out, step=1.0 + ) + export.export_alembic( + get_cache_path('IbogaineMarcus_body_export', 'abc'), + cmds.sets('IbogaineMarcus_body_export', query=True), cut_in - 51, cut_out, step=1.0 + ) + export.export_alembic( + get_cache_path('IbogaineMarcus_cloth_export', 'abc'), + cmds.sets('IbogaineMarcus_cloth_export', query=True), cut_in - 51, cut_in - 51, step=1.0 + ) + export.export_alembic( + get_cache_path('IbogaineMarcus_extra_export', 'abc'), + cmds.sets('IbogaineMarcus_extra_export', query=True), cut_in - 51, cut_out, step=1.0 + ) + + mutils.loadAnims( + [os.path.normpath(f'{destination_dir}/IbogaineMarcus_fullanim.anim'), ], + objects=cmds.sets(controllers_set, query=True), + currentTime=False, + option='replaceCompletely', + namespaces=[namespace, ] + ) + + cmds.playbackOptions(animationStartTime=cut_in, minTime=cut_in, animationEndTime=cut_out, maxTime=cut_out) diff --git a/bookmarks/maya/scripts/scripts.json b/bookmarks/maya/scripts/scripts.json index ab803a50..d426ae81 100644 --- a/bookmarks/maya/scripts/scripts.json +++ b/bookmarks/maya/scripts/scripts.json @@ -1,17 +1,30 @@ { "0": { - "name": "AKA-ODSY-DJI | Shader Import/Export...", + "name": "Studio Aka | Odyssey | Shader Import/Export...", "module": "aka_shader_templates", "description": "Shader template import and export tool" }, "1": { - "name": "AKA-ODSY-DJI | Make export Sets", + "name": "Studio Aka | Odyssey | Make Export Sets", "module": "aka_make_export_sets", "description": "" }, "2": { + "name": "Studio Aka | Odyssey | Export IbogaineMarcus Cloth caches for Simulation (static)", + "module": "aka_cloth_cache", + "description": "Exports cloth caches for simulation" + }, + "3": { + "name": "Studio Aka | Odyssey | Export IbogaineMarcus Cloth caches for Simulation (animated)", + "module": "aka_cloth_cache_animated", + "description": "Exports cloth caches for simulation" + }, + "4": { + "name": "separator" + }, + "5": { "name": "Reset joint orientations", "module": "reset_joint_orientations", - "description": "Resets bone orientation values too zero without moving the bones" + "description": "Resets bone orientation values to zero without moving the bones" } } \ No newline at end of file diff --git a/bookmarks/scripts/scripts.json b/bookmarks/scripts/scripts.json index 824f8e64..61fd1a60 100644 --- a/bookmarks/scripts/scripts.json +++ b/bookmarks/scripts/scripts.json @@ -1,26 +1,27 @@ { "0": { - "name": "Synchronize Asset & Entity Data", + "name": "Synchronize Asset and Entity Data", "module": "sync_asset_data", "description": "Synchronizes bookmark asset items and ShotGrid entities with a source data file.", "needs_active": "root", - "icon": "sg" + "icon": "spinner" }, "1": { - "name": "Build Playlists", + "name": "Preview and Save Playlists", "module": "clips", "description": "View clips and export them to editorial", "needs_active": "root", - "icon": "sg" + "icon": "image" }, "2": { "name": "separator" }, "3": { - "name": "Send images to After Effects", - "module": "render_layers_to_ae", + "name": "Send Images to After Effects", + "module": "send_images_to_after_effects", "description": "View clips and export them to editorial", "needs_active": "task", - "icon": "file" + "needs_application": "after effects", + "icon": "add_file" } } \ No newline at end of file diff --git a/bookmarks/scripts/render_layers_to_ae.py b/bookmarks/scripts/send_images_to_after_effects.py similarity index 59% rename from bookmarks/scripts/render_layers_to_ae.py rename to bookmarks/scripts/send_images_to_after_effects.py index a431c23a..fda6329e 100644 --- a/bookmarks/scripts/render_layers_to_ae.py +++ b/bookmarks/scripts/send_images_to_after_effects.py @@ -1,8 +1,16 @@ +""" +This is a utility script to send image sequences to After Effects rendered with Maya. + +The script expects the images sequences to use the Maya specified render template format. +Any valid image sequences will be included in an ExtendScript export script. + +""" import os import re from PySide2 import QtCore +from .. import actions from .. import common from .. import database from ..tokens import tokens @@ -27,6 +35,9 @@ def recursive_parse(path): def get_footage_sources(): + """Traverse over the active task folder's subdirectories and return a dictionary of image sequences. + + """ source_dir = common.active('task', path=True) if not source_dir: raise RuntimeError('No active task') @@ -109,30 +120,49 @@ def generate_jsx_script(footage_sources): } function importFootage(path, framerate, name) { + var existingFootageItem = null; + // Check if the footage item already exists - for (var i = 1; i <= project.numItems; i++) { - if (project.item(i) instanceof FootageItem && project.item(i).name == name) { - return; // Footage item already exists, skip importing + for (var i = 1; i <= app.project.numItems; i++) { + var currentItem = app.project.item(i); + if (currentItem instanceof FootageItem && currentItem.name == name) { + existingFootageItem = currentItem; + break; } } - var importOptions = new ImportOptions(File(path)); - importOptions.sequence = true; - var footageItem = project.importFile(importOptions); - footageItem.name = name; - footageItem.mainSource.conformFrameRate = framerate; - footageItem.parentFolder = cgi_folder; - return footageItem; -} -app.beginUndoGroup('Import Footage'); - """ + var io = new ImportOptions(); + io.file = File(path); + io.sequence = true; + + if (io.canImportAs(ImportAsType.FOOTAGE)) { + io.importAs = ImportAsType.FOOTAGE; + } else { + alert('Could not import footage as a sequence. Please check the file path and try again.'); + return; + } - # Iterate over footage sources and add to the ExtendScript - for name, data in footage_sources.items(): - # Sort the files to get the first one - files = sorted(data['files']) - first_file = files[0] - jsx_script += f'var footageItem = importFootage("{first_file}", {data["framerate"]}, "{data["name"]}");\n' + // If the footage exists and its source differs from the intended source + if (existingFootageItem && existingFootageItem.mainSource.file.path != path) { + var newFootage = app.project.importFile(io); + + existingFootageItem.replaceWithSequence(newFootage.mainSource.file, true); + existingFootageItem.mainSource.conformFrameRate = framerate; + + // Clean up by removing the temporarily imported footage + newFootage.remove(); + + } else if (!existingFootageItem) { // If footage doesn't exist + + var footageItem = app.project.importFile(io); + footageItem.name = name; + footageItem.mainSource.conformFrameRate = framerate; + footageItem.parentFolder = cgi_folder; + return footageItem; + } +} + +app.beginUndoGroup('Import Footage');""" # Create new composition if it doesn't exist yet jsx_script += f""" @@ -145,23 +175,59 @@ def generate_jsx_script(footage_sources): }} if (!comp) {{ comp = project.items.addComp('{comp_name}', {width}, {height}, 1, ({cut_out}-{cut_in})/{framerate}, {framerate}); - comp.layers.add(footageItem); + comp.displayStartTime = {cut_in}/{framerate}; }} -comp.workAreaStart = {cut_in}/{framerate}; - """ + +""" + + # Iterate over footage sources and add to the ExtendScript + for name, data in footage_sources.items(): + # Sort the files to get the first one + files = sorted(data['files']) + first_file = files[0] + jsx_script += f'var footageItem = importFootage("{first_file}", {data["framerate"]}, "{data["name"]}");\n' # Close the undo group jsx_script += "app.endUndoGroup();\n" - return jsx_script + return jsx_script, footage_sources + + +def send_to_after_effects(script_path): + if not common.active('root', args=True): + return False + + db = database.get(*common.active('root', args=True)) + applications = db.value(db.source(), 'applications', database.BookmarkTable) + + if not applications: + return False + apps = [app for app in applications.values() if 'after effects' in app['name'].lower()] + if not apps: + return False + app = apps[0]['path'] + if not os.path.isfile(app): + return False + + # Call after effects using with the generated script as an argument: + actions.execute_detached(app, ['-r', os.path.normpath(script_path)]) + + return True def run(): data = get_footage_sources() - with open(f'{common.active("task", path=True)}/import_footage.jsx', 'w') as f: - f.write(generate_jsx_script(data)) - common.show_message( - 'Render Layers to After Effects', - body=f'Import script generated successfully and saved to:\n{common.active("task", path=True)}/import_footage.jsx', - message_type='success', - ) \ No newline at end of file + script_path = f'{common.active("task", path=True)}/import_footage.jsx' + jsx_script, footage_sources = generate_jsx_script(data) + with open(script_path, 'w') as f: + f.write(jsx_script) + + if not send_to_after_effects(script_path): + print('Could not find After Effects. Footage was not sent.') + + common.show_message( + 'Success.', + body=f'Found {len(footage_sources)} items. An import script was saved to:\n' + f'{common.active("task", path=True)}/import_footage.jsx', + message_type='success', + ) \ No newline at end of file From 23a6ca1e42ee03549bc95a8528ef298a1aa3a217 Mon Sep 17 00:00:00 2001 From: Gergely Wootsch Date: Wed, 18 Oct 2023 11:54:21 +0100 Subject: [PATCH 08/32] Implement asset list caching --- bookmarks/actions.py | 9 +++++ bookmarks/common/core.py | 15 ++++---- bookmarks/items/asset_items.py | 46 ++++++++++++------------ bookmarks/items/models.py | 5 ++- bookmarks/progress.py | 10 ++++++ bookmarks/threads/workers.py | 9 +++++ bookmarks/tokens/tokens.py | 64 ++++++++++++++++------------------ 7 files changed, 92 insertions(+), 66 deletions(-) diff --git a/bookmarks/actions.py b/bookmarks/actions.py index 27a5a429..d7138e96 100644 --- a/bookmarks/actions.py +++ b/bookmarks/actions.py @@ -16,6 +16,7 @@ from . import common from . import database from . import images +from. import log def must_be_initialized(func): @@ -883,6 +884,14 @@ def refresh(idx=None): """ w = common.widget(idx=idx) model = w.model().sourceModel() + + # Remove the asset list cache if we're forcing a refresh on the asset tab + if common.current_tab() == common.AssetTab: + cache = f'{common.active("root", path=True)}/{common.bookmark_cache_dir}/assets.cache' + if os.path.exists(cache): + print('Removing asset cache:', cache) + os.remove(cache) + model.reset_data(force=True) diff --git a/bookmarks/common/core.py b/bookmarks/common/core.py index a6abbdde..82e22dc5 100644 --- a/bookmarks/common/core.py +++ b/bookmarks/common/core.py @@ -465,19 +465,21 @@ def get_sequence_and_shot(s): return seq, shot -def get_entry_from_path(path, is_dir=True): +def get_entry_from_path(path, is_dir=True, force_exists=False): """Returns a scandir entry of the given file path. Args: path (str): Path to directory. is_dir (bool): Is the path a directory or a file. + force_exists (bool): Force skip checking the existence of the path if we know path exist. Returns: scandir.DirEntry: A scandir entry, or None if not found. """ file_info = QtCore.QFileInfo(path) - if not file_info.exists(): + + if not force_exists and not file_info.exists(): return None for entry in os.scandir(file_info.dir().path()): @@ -496,22 +498,21 @@ def get_links(path, section='links/asset'): inside job templates. If a .links file contains two relative paths, - `subfolder1/nested_asset1` and `subfolder2/nested_asset2`... + `subfolder1/nested_asset1` and `subfolder2/nested_asset2` .. code-block:: text - asset/ + root_asset_folder/ ├─ .links ├─ subfolder1/ │ ├─ nested_asset1/ ├─ subfolder2/ │ ├─ nested_asset2/ - ...two asset will be read, `nested_asset1` and `nested_asset2` - (but not the original root `asset`). + ...two asset items will be read - `nested_asset1` and `nested_asset2` but not the original root `root_asset_folder`. Args: - path (str): Path to a folder where the link file resides. E.g. an asset root folder. + path (str): Path to a folder where the link file resides. section (str): The settings section to look for links in. Optional. Defaults to 'links/asset'. diff --git a/bookmarks/items/asset_items.py b/bookmarks/items/asset_items.py index eaf5135d..468e8e65 100644 --- a/bookmarks/items/asset_items.py +++ b/bookmarks/items/asset_items.py @@ -48,7 +48,6 @@ from ..tokens import tokens - class AssetItemViewContextMenu(contextmenu.BaseContextMenu): """The context menu associated with :class:`AssetItemView`.""" @@ -179,11 +178,6 @@ def init_data(self): # Let's get the identifier from the bookmark database db = database.get(*p) - asset_identifier = db.value( - source, - 'identifier', - database.BookmarkTable - ) # ...and the display token display_token = db.value( @@ -201,19 +195,12 @@ def init_data(self): nth = 17 c = 0 - for entry in self.item_generator(source): + for filepath in self.item_generator(source): if self._interrupt_requested: break - filepath = entry.path.replace('\\', '/') - - if asset_identifier: - identifier = f'{filepath}/{asset_identifier}' - if not os.path.isfile(identifier): - continue - # Progress bar - c += 9 + c += 1 if not c % nth: common.signals.showStatusBarMessage.emit( f'Loading assets ({c} found)...' @@ -238,6 +225,7 @@ def init_data(self): seq, shot = common.get_sequence_and_shot(filepath) _display_name = config.expand_tokens( display_token, + use_database=False, server=p[0], job=p[1], root=p[2], @@ -250,7 +238,6 @@ def init_data(self): if tokens.invalid_token not in _display_name: display_name = _display_name - parent_path_role = p + (filename,) sort_by_name_role = models.DEFAULT_SORT_BY_NAME_ROLE.copy() @@ -280,7 +267,7 @@ def init_data(self): common.DataTypeRole: t, common.ItemTabRole: common.AssetTab, # - common.EntryRole: [entry, ], + common.EntryRole: [], common.FlagsRole: flags, common.ParentPathRole: parent_path_role, common.DescriptionRole: '', @@ -307,6 +294,10 @@ def init_data(self): } ) + # Cache the list of assets for later use + with open(f'{common.active("root", path=True)}/{common.bookmark_cache_dir}/assets.cache', 'w') as f: + f.write('\n'.join([v[common.PathRole] for v in data.values()])) + # Explicitly emit `activeChanged` to notify other dependent models self.activeChanged.emit(self.active_index()) @@ -326,10 +317,21 @@ def source_path(self): def item_generator(self, path): """Yields the asset items to be processed by :meth:`init_data`. + Args: + path (string): The path to a directory containing asset folders. + Yields: DirEntry: Entry instances of valid asset folders. """ + # Read from the cache if it exists + cache = f'{common.active("root", path=True)}/{common.bookmark_cache_dir}/assets.cache' + if os.path.isfile(cache): + with open(cache, 'r') as f: + for line in f: + yield line.strip() + return + try: it = os.scandir(path) except OSError as e: @@ -352,17 +354,13 @@ def item_generator(self, path): ) for link in links: - v = f'{path}/{entry.name}/{link}' - _entry = common.get_entry_from_path(v) - if not _entry: - log.error(f'Could not get entry from link {v}') - continue - yield _entry + yield f'{path}/{entry.name}/{link}' + # Don't yield the asset if it has links if links: continue - yield entry + yield entry.path.replace('\\', '/') def save_active(self): """Saves the active item. diff --git a/bookmarks/items/models.py b/bookmarks/items/models.py index 2058101d..cfb61539 100644 --- a/bookmarks/items/models.py +++ b/bookmarks/items/models.py @@ -412,10 +412,13 @@ def _drop_properties_action(self, mime, action, row, column, parent): log.error('Could not remove temp file.') return - def item_generator(self): + def item_generator(self, path): """A generator method used by :func:`init_data` to yield the items the model should load. + Args: + path (string): Path to a directory. + Yields: DirEntry: os.scandir DirEntry objects. diff --git a/bookmarks/progress.py b/bookmarks/progress.py index 54e4b17e..c593b5f5 100644 --- a/bookmarks/progress.py +++ b/bookmarks/progress.py @@ -155,6 +155,16 @@ def paint(self, painter, option, index): t = common.FileItem _data = common.get_data(p, k, t) + + if not _data: + return + if source_index.row() not in _data: + return + if not _data[source_index.row()][common.AssetProgressRole]: + return + if index.column() - 1 not in _data[source_index.row()][common.AssetProgressRole]: + return + data = _data[source_index.row()][common.AssetProgressRole][index.column() - 1] right_edge = self._draw_background(painter, option, data) diff --git a/bookmarks/threads/workers.py b/bookmarks/threads/workers.py index 7bf2e2c0..a4ea0686 100644 --- a/bookmarks/threads/workers.py +++ b/bookmarks/threads/workers.py @@ -570,6 +570,15 @@ def _process_data(self, ref): # Asset Progress Data if len(pp) == 4 and asset_row_data['progress']: ref()[common.AssetProgressRole] = asset_row_data['progress'] + # Asset entry data + if len(pp) == 4: + ref()[common.EntryRole].append( + common.get_entry_from_path( + ref()[common.PathRole], + is_dir=True, + force_exists=True + ) + ) # ShotGrid status if len(pp) <= 4: update_shotgun_configured(pp, bookmark_row_data, asset_row_data, ref) diff --git a/bookmarks/tokens/tokens.py b/bookmarks/tokens/tokens.py index cb50360d..2838392b 100644 --- a/bookmarks/tokens/tokens.py +++ b/bookmarks/tokens/tokens.py @@ -40,9 +40,6 @@ from .. import database from .. import log -#: The database column name -TOKENS_DB_KEY = 'tokens' - FileFormatConfig = 'FileFormatConfig' FileNameConfig = 'FileNameConfig' PublishConfig = 'PublishConfig' @@ -598,7 +595,7 @@ def data(self, force=False): db = database.get(self.server, self.job, self.root) v = db.value( db.source(), - TOKENS_DB_KEY, + 'tokens', database.BookmarkTable ) if not v or not isinstance(v, dict): @@ -631,7 +628,7 @@ def set_data(self, data): db = database.get(self.server, self.job, self.root) db.set_value( db.source(), - TOKENS_DB_KEY, + 'tokens', data, table=database.BookmarkTable ) @@ -701,7 +698,7 @@ def get_description(self, token, force=False): return '' def expand_tokens( - self, s, user=common.get_username(), version='v001', + self, s, use_database=True, user=common.get_username(), version='v001', host=socket.gethostname(), task='main', ext=None, prefix=None, **_kwargs ): @@ -712,12 +709,7 @@ def expand_tokens( Args: s (str): The string containing tokens to be expanded. - user (str, optional): Username. - version (str, optional): The version string. - host (str, optional): The name of the current machine/host. - task (str, optional): Task folder name. - ext (str, optional): File format extension. - prefix (str, optional): Bookmark item prefix. + use_database (bool, optional): Use values stored in the database. _kwargs (dict, optional): Optional token/value pairs. Returns: @@ -726,6 +718,7 @@ def expand_tokens( """ kwargs = self.get_tokens( + use_database=use_database, user=user, version=version, ver=version, @@ -756,11 +749,11 @@ def expand_tokens( collections.defaultdict(lambda: invalid_token, **kwargs) ) - def get_tokens(self, force=False, **kwargs): + def get_tokens(self, force=False, use_database=True, **kwargs): """Get all available tokens. Args: - force (bool, optional): Force retrieve tokens from the database. + force (bool, optional): Force retrieve defined tokens from the database. Returns: dict: A dictionary of token/value pairs. @@ -791,34 +784,37 @@ def get_tokens(self, force=False, **kwargs): tokens[k] = v # We can also use bookmark item properties as tokens - db = database.get(self.server, self.job, self.root) - for _k in database.TABLES[database.BookmarkTable]: - if _k == 'id': - continue - if database.TABLES[database.BookmarkTable][_k]['type'] == dict: - continue - if _k not in kwargs or not kwargs[_k]: - _v = db.value(db.source(), _k, database.BookmarkTable) - _v = _v if _v else invalid_token - tokens[_k] = _v - - # The asset root token will only be available when the asset is manually - # specified - if 'asset' in kwargs and kwargs['asset']: - source = f'{self.server}/{self.job}/{self.root}/{kwargs["asset"]}' + if use_database: + db = database.get(self.server, self.job, self.root) - for _k in database.TABLES[database.AssetTable]: + for _k in database.TABLES[database.BookmarkTable]: if _k == 'id': continue - if database.TABLES[database.AssetTable][_k]['type'] == dict: + if database.TABLES[database.BookmarkTable][_k]['type'] == dict: continue if _k not in kwargs or not kwargs[_k]: - _v = db.value(source, _k, database.AssetTable) + _v = db.value(db.source(), _k, database.BookmarkTable) _v = _v if _v else invalid_token tokens[_k] = _v - # Let's also override width, height and fps tokens - tokens[_k.replace('asset_', '')] = _v + # The asset root token will only be available when the asset is manually + # specified + if 'asset' in kwargs and kwargs['asset']: + source = f'{self.server}/{self.job}/{self.root}/{kwargs["asset"]}' + + if use_database: + for _k in database.TABLES[database.AssetTable]: + if _k == 'id': + continue + if database.TABLES[database.AssetTable][_k]['type'] == dict: + continue + if _k not in kwargs or not kwargs[_k]: + _v = db.value(source, _k, database.AssetTable) + _v = _v if _v else invalid_token + tokens[_k] = _v + + # Let's also override width, height and fps tokens + tokens[_k.replace('asset_', '')] = _v # We also want to use the path elements as tokens. for k in ('server', 'job', 'root', 'asset', 'task'): From c92fffb716e09e3f2c2c1cf0e7e64a84b8dfbd8f Mon Sep 17 00:00:00 2001 From: Gergely Wootsch Date: Fri, 20 Oct 2023 08:14:10 +0200 Subject: [PATCH 09/32] Local script changes --- bookmarks/maya/scripts/aka_cloth_cache.py | 12 +++++++----- bookmarks/maya/scripts/aka_cloth_cache_animated.py | 12 +++++++----- bookmarks/maya/scripts/aka_make_export_sets.py | 3 +++ 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/bookmarks/maya/scripts/aka_cloth_cache.py b/bookmarks/maya/scripts/aka_cloth_cache.py index 523f2f74..9873a28b 100644 --- a/bookmarks/maya/scripts/aka_cloth_cache.py +++ b/bookmarks/maya/scripts/aka_cloth_cache.py @@ -28,7 +28,7 @@ studiolibrary_dir = f'{common.active("server")}/{common.active("job")}/070_Assets/Character/studiolibrary' reset_pose = f'{studiolibrary_dir}/Characters/IbogaineMarcus/Reset/A-Pose-v2-FullFK.pose/pose.json' -cache_destination_dir = '{studiolibrary_dir}/Shots/MAB/{asset0}_{shot}' +cache_destination_dir = '{studiolibrary_dir}/Shots/{prefix}/{asset0}_{shot}' namespace = 'IbogaineMarcus_01' controllers_set = f'{namespace}:rig_controllers_grp' @@ -103,12 +103,18 @@ def run(): config = tokens.get(*common.active('root', args=True)) + db = database.get(*common.active('root', args=True)) + cut_in = db.value(common.active('asset', path=True), 'cut_in', database.AssetTable) + cut_out = db.value(common.active('asset', path=True), 'cut_out', database.AssetTable) + prefix = db.value(common.active('root', path=True), 'prefix', database.BookmarkTable) + seq, shot = common.get_sequence_and_shot(common.active('asset')) destination_dir = config.expand_tokens( cache_destination_dir, asset=common.active('asset'), shot=shot, sequence=seq, + prefix=prefix.split('_')[-1].upper(), studiolibrary_dir=studiolibrary_dir ) @@ -118,10 +124,6 @@ def run(): animation_start = cmds.playbackOptions(minTime=True, query=True) animation_end = cmds.playbackOptions(maxTime=True, query=True) - db = database.get(*common.active('root', args=True)) - cut_in = db.value(common.active('asset', path=True), 'cut_in', database.AssetTable) - cut_out = db.value(common.active('asset', path=True), 'cut_out', database.AssetTable) - cmds.select(clear=True) # Create the export groups diff --git a/bookmarks/maya/scripts/aka_cloth_cache_animated.py b/bookmarks/maya/scripts/aka_cloth_cache_animated.py index a44bf13c..7335728d 100644 --- a/bookmarks/maya/scripts/aka_cloth_cache_animated.py +++ b/bookmarks/maya/scripts/aka_cloth_cache_animated.py @@ -28,7 +28,7 @@ studiolibrary_dir = f'{common.active("server")}/{common.active("job")}/070_Assets/Character/studiolibrary' reset_pose = f'{studiolibrary_dir}/Characters/IbogaineMarcus/Reset/A-Pose-v2-FullFK.pose/pose.json' -cache_destination_dir = '{studiolibrary_dir}/Shots/MAB/{asset0}_{shot}' +cache_destination_dir = '{studiolibrary_dir}/Shots/{prefix}/{asset0}_{shot}' namespace = 'IbogaineMarcus_01' controllers_set = f'{namespace}:rig_controllers_grp' @@ -103,12 +103,18 @@ def run(): config = tokens.get(*common.active('root', args=True)) + db = database.get(*common.active('root', args=True)) + cut_in = db.value(common.active('asset', path=True), 'cut_in', database.AssetTable) + cut_out = db.value(common.active('asset', path=True), 'cut_out', database.AssetTable) + prefix = db.value(common.active('root', path=True), 'prefix', database.BookmarkTable) + seq, shot = common.get_sequence_and_shot(common.active('asset')) destination_dir = config.expand_tokens( cache_destination_dir, asset=common.active('asset'), shot=shot, sequence=seq, + prefix=prefix.split('_')[-1].upper(), studiolibrary_dir=studiolibrary_dir ) @@ -118,10 +124,6 @@ def run(): animation_start = cmds.playbackOptions(minTime=True, query=True) animation_end = cmds.playbackOptions(maxTime=True, query=True) - db = database.get(*common.active('root', args=True)) - cut_in = db.value(common.active('asset', path=True), 'cut_in', database.AssetTable) - cut_out = db.value(common.active('asset', path=True), 'cut_out', database.AssetTable) - cmds.select(clear=True) # Create the export groups diff --git a/bookmarks/maya/scripts/aka_make_export_sets.py b/bookmarks/maya/scripts/aka_make_export_sets.py index 077b9a60..ce7c9131 100644 --- a/bookmarks/maya/scripts/aka_make_export_sets.py +++ b/bookmarks/maya/scripts/aka_make_export_sets.py @@ -53,6 +53,9 @@ def run(): 'IbogaineMarcus_body_export': ['*Marcus*:head_geo', '*Marcus*:body_geo', '*Marcus*:shoes_geo'], 'IbogaineMarcus_cloth_export': ['*Marcus*:tshirt_geo', '*Marcus*:trousers_geo'], 'IbogaineMarcus_extra_export': ['*Marcus*:eye_L', '*Marcus*:eye_R'], + 'IbogaineMarcus_body_mirrored_export': ['*Marcus*:head_geo_mirrored', '*Marcus*:body_geo_mirrored', '*Marcus*:shoes_geo_mirrored'], + 'IbogaineMarcus_cloth_mirrored_export': ['*Marcus*:tshirt_geo_mirrored', '*Marcus*:trousers_geo_mirrored'], + 'IbogaineMarcus_extra_mirrored_export': ['*Marcus*:eye_L_mirrored', '*Marcus*:eye_R_mirrored'], 'set_ground_export': ['set_ground_geo', ], 'set_background_export': ['set_background_geo', ], 'set_floor_export': ['set_floor_geo', ], From a76363a8cd04aea2c5d387760f7c40fefc2a5005 Mon Sep 17 00:00:00 2001 From: Gergely Wootsch Date: Fri, 20 Oct 2023 16:34:41 +0200 Subject: [PATCH 10/32] Add ShotGrid entity and task filter menus This commit adds two new filter setter ui menus to the top bar to set the ShotGrid entity OR the ShotGrid task. The process involved a cleanup pass on some of the ui and backend. I fixed some bugs related to how the model's internal data was sorted previously and this made me realise the DataDict implementation had a bug - it wasn't copying over all properties correctly. The new filter menus values are populated by the worker threads and are saved into the main DataDict instances the filter combo box models read. --- bookmarks/actions.py | 6 +- bookmarks/common/core.py | 25 +++- bookmarks/common/data.py | 11 ++ bookmarks/common/signals.py | 7 ++ bookmarks/contextmenu.py | 2 +- bookmarks/items/asset_items.py | 2 + bookmarks/items/bookmark_items.py | 2 + bookmarks/items/favourite_items.py | 2 + bookmarks/items/file_items.py | 2 + bookmarks/items/models.py | 18 +-- bookmarks/items/task_items.py | 2 + bookmarks/maya/main.py | 2 +- bookmarks/shortcuts.py | 10 +- bookmarks/standalone.py | 4 +- bookmarks/statusbar.py | 2 +- bookmarks/threads/workers.py | 77 +++++++++--- bookmarks/topbar/sgfilter.py | 195 +++++++++++++++++++++++++++++ bookmarks/topbar/topbar.py | 11 +- bookmarks/ui.py | 14 ++- 19 files changed, 349 insertions(+), 45 deletions(-) create mode 100644 bookmarks/topbar/sgfilter.py diff --git a/bookmarks/actions.py b/bookmarks/actions.py index d7138e96..f8d6d5ec 100644 --- a/bookmarks/actions.py +++ b/bookmarks/actions.py @@ -1846,11 +1846,9 @@ def pick_template(mode): dialog.setNameFilters(['*.zip', ]) dialog.setFilter(QtCore.QDir.Files | QtCore.QDir.NoDotAndDotDot) dialog.setLabelText( - QtWidgets.QFileDialog.Accept, 'Select a {} template'.format(mode.title()) - ) - dialog.setWindowTitle( - 'Select *.zip archive to use as a {} template'.format(mode.lower()) + QtWidgets.QFileDialog.Accept, f'Select a {mode.title()} template' ) + dialog.setWindowTitle(f'Select *.zip archive to use as a {mode.lower()} template') if dialog.exec_() == QtWidgets.QDialog.Rejected: return source = next((f for f in dialog.selectedFiles()), None) diff --git a/bookmarks/common/core.py b/bookmarks/common/core.py index 82e22dc5..a97b2aa4 100644 --- a/bookmarks/common/core.py +++ b/bookmarks/common/core.py @@ -121,6 +121,8 @@ def _idx_func(reset=False, start=None): QueueRole = QtCore.Qt.ItemDataRole(idx()) #: List item role for getting the item's data type (sequence or file) DataTypeRole = QtCore.Qt.ItemDataRole(idx()) +#: List item role for getting the container data dictionary +DataDictRole = QtCore.Qt.ItemDataRole(idx()) #: The view tab associated with the item ItemTabRole = QtCore.Qt.ItemDataRole(idx()) #: Data used to sort the items by name @@ -640,15 +642,18 @@ class DataDict(dict): """Custom dictionary class used to store model item data. This class adds compatibility for :class:`weakref.ref` referencing - and custom attributes for storing data state. + and custom attributes for storing data states. """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + self._loaded = False self._refresh_needed = False self._data_type = None + self._shotgun_names = [] + self._sg_task_names = [] @property def loaded(self): @@ -686,6 +691,24 @@ def data_type(self): def data_type(self, v): self._data_type = v + @property + def shotgun_names(self): + """Returns a list of Shotgun task names associated with the data dictionary.""" + return self._shotgun_names + + @shotgun_names.setter + def shotgun_names(self, v): + self._shotgun_names = v + + @property + def sg_task_names(self): + """Returns a list of Shotgun task names associated with the data dictionary.""" + return self._sg_task_names + + @sg_task_names.setter + def sg_task_names(self, v): + self._sg_task_names = v + class Timer(QtCore.QTimer): """A custom QTimer class used across the app. diff --git a/bookmarks/common/data.py b/bookmarks/common/data.py index d6f4b538..be3b6419 100644 --- a/bookmarks/common/data.py +++ b/bookmarks/common/data.py @@ -41,14 +41,20 @@ def sort_key(_idx): ) d = common.DataDict() + + # Copy over the property values from the source data d.loaded = ref().loaded d.data_type = ref().data_type + d.refresh_needed = ref().refresh_needed + d.shotgun_names = ref().shotgun_names + d.sg_task_names = ref().sg_task_names for n, idx in enumerate(sorted_idxs): if not ref(): raise RuntimeError('Model mutated during sorting.') d[n] = ref()[idx] d[n][common.IdRole] = n + return d @@ -209,6 +215,9 @@ def set_data(key, task, data_type, data): data_type (int): One of :attr:`bookmarks.common.FileItem` or :attr:`bookmarks.common.SequenceItem`. data (common.DataDict): The data to set in the cache. + Returns: + common.DataDict: The cached data. + """ common.check_type(key, tuple) common.check_type(task, str) @@ -219,3 +228,5 @@ def set_data(key, task, data_type, data): elif task not in common.item_data[key]: reset_data(key, task) common.item_data[key][task][data_type] = data + + return common.item_data[key][task][data_type] diff --git a/bookmarks/common/signals.py b/bookmarks/common/signals.py index 91c4f7c9..fea575c4 100644 --- a/bookmarks/common/signals.py +++ b/bookmarks/common/signals.py @@ -2,6 +2,7 @@ """ import functools +import weakref from PySide2 import QtCore @@ -17,6 +18,9 @@ class CoreSignals(QtCore.QObject): """A utility class used to store application-wide signals. """ + #: Signal emitted by worker threads when the internal data of a model is fully loaded + internalDataReady = QtCore.Signal(weakref.ref) + #: Update top bar widget buttons updateTopBarButtons = QtCore.Signal() @@ -78,6 +82,9 @@ class CoreSignals(QtCore.QObject): #: Signals a filter button state change toggleFavouritesButton = QtCore.Signal() + #: Signal emitted when the filter text changes of a list view's proxy model + filterTextChanged = QtCore.Signal(str) + #: Signal emitted when the active path mode changes activeModeChanged = QtCore.Signal(int) diff --git a/bookmarks/contextmenu.py b/bookmarks/contextmenu.py index 6b03e368..1a4d3dfc 100644 --- a/bookmarks/contextmenu.py +++ b/bookmarks/contextmenu.py @@ -832,7 +832,7 @@ def quit_menu(self): if common.init_mode == common.EmbeddedMode: return self.menu[key()] = { - 'text': 'Quit {}'.format(common.product), + 'text': f'Quit {common.product.title()}', 'action': common.uninitialize, 'icon': ui.get_icon('close'), 'shortcut': shortcuts.get( diff --git a/bookmarks/items/asset_items.py b/bookmarks/items/asset_items.py index 468e8e65..50147109 100644 --- a/bookmarks/items/asset_items.py +++ b/bookmarks/items/asset_items.py @@ -32,6 +32,7 @@ import copy import functools import os +import weakref from PySide2 import QtCore, QtWidgets @@ -265,6 +266,7 @@ def init_data(self): # common.QueueRole: self.queues, common.DataTypeRole: t, + common.DataDictRole: weakref.ref(data), common.ItemTabRole: common.AssetTab, # common.EntryRole: [], diff --git a/bookmarks/items/bookmark_items.py b/bookmarks/items/bookmark_items.py index 3ed27101..aeb71d50 100644 --- a/bookmarks/items/bookmark_items.py +++ b/bookmarks/items/bookmark_items.py @@ -54,6 +54,7 @@ """ +import weakref from PySide2 import QtCore, QtWidgets @@ -236,6 +237,7 @@ def init_data(self): # common.QueueRole: self.queues, common.DataTypeRole: t, + common.DataDictRole: weakref.ref(data), common.ItemTabRole: common.BookmarkTab, # common.FlagsRole: flags, diff --git a/bookmarks/items/favourite_items.py b/bookmarks/items/favourite_items.py index bd74714a..4164fd04 100644 --- a/bookmarks/items/favourite_items.py +++ b/bookmarks/items/favourite_items.py @@ -2,6 +2,7 @@ """ import os +import weakref from PySide2 import QtCore, QtWidgets @@ -131,6 +132,7 @@ def init_data(self): # common.QueueRole: self.queues, common.DataTypeRole: t, + common.DataDictRole: weakref.ref(data), common.ItemTabRole: common.FavouriteTab, # common.EntryRole: [entry, ], diff --git a/bookmarks/items/file_items.py b/bookmarks/items/file_items.py index b4bc02f8..7c894a7b 100644 --- a/bookmarks/items/file_items.py +++ b/bookmarks/items/file_items.py @@ -40,6 +40,7 @@ """ import functools import os +import weakref from PySide2 import QtWidgets, QtCore @@ -399,6 +400,7 @@ def init_data(self): # common.QueueRole: self.queues, common.DataTypeRole: common.FileItem, + common.DataDictRole: weakref.ref(data), common.ItemTabRole: common.FileTab, # common.EntryRole: [entry, ], diff --git a/bookmarks/items/models.py b/bookmarks/items/models.py index cfb61539..1a638089 100644 --- a/bookmarks/items/models.py +++ b/bookmarks/items/models.py @@ -161,8 +161,6 @@ class ItemModel(QtCore.QAbstractTableModel): missing data. coreDataReset (QtCore.Signal): Signals that the underlying model data has been reset. Used by the thread workers to empty their queues. - dataTypeSorted (QtCore.Signal -> int): Signals that the underlying model - data was sorted. sortingChanged (QtCore.Signal -> int, bool): Emitted when the sorting order or sorting role was changed by the user. activeChanged (QtCore.Signal): Signals :meth:`.ItemModel.active_index` @@ -177,7 +175,6 @@ class ItemModel(QtCore.QAbstractTableModel): coreDataLoaded = QtCore.Signal(weakref.ref, weakref.ref) coreDataReset = QtCore.Signal() - dataTypeSorted = QtCore.Signal(int) sortingChanged = QtCore.Signal(int, bool) # (SortRole, SortOrder) activeChanged = QtCore.Signal(QtCore.QModelIndex) @@ -207,7 +204,6 @@ def __init__(self, parent=None): self._datatype = {} # used by the files model only self.sortingChanged.connect(self.set_sorting) - self.dataTypeSorted.connect(self.emit_reset_model) self.modelAboutToBeReset.connect(common.signals.updateTopBarButtons) self.modelReset.connect(common.signals.updateTopBarButtons) @@ -485,14 +481,19 @@ def reset_data(self, *args, force=False, emit_active=True): self.set_active(index) @QtCore.Slot(int) - def emit_reset_model(self, data_type): + def internal_data_about_to_be_sorted(self, data_type): + if self.data_type() != data_type: + return + self.layoutAboutToBeChanged.emit() + + @QtCore.Slot(int) + def internal_data_sorted(self, data_type): """Slot used to emit the reset model signals. """ if self.data_type() != data_type: return - self.beginResetModel() - self.endResetModel() + self.layoutChanged.emit() @QtCore.Slot(bool) @QtCore.Slot(int) @@ -848,6 +849,9 @@ def __init__(self, parent=None): self.queued_invalidate_timer.timeout.connect(self.delayed_invalidate) + # Notify the outside world that the filter text has changed + self.filterTextChanged.connect(common.signals.filterTextChanged) + def verify(self): """Verify the filter model contents to make sure archived items remain hidden when they're not meant to be visible. diff --git a/bookmarks/items/task_items.py b/bookmarks/items/task_items.py index 825f71ba..6a50d92c 100644 --- a/bookmarks/items/task_items.py +++ b/bookmarks/items/task_items.py @@ -9,6 +9,7 @@ """ import functools import os +import weakref from PySide2 import QtWidgets, QtGui, QtCore @@ -308,6 +309,7 @@ def init_data(self): # common.QueueRole: self.queues, common.DataTypeRole: common.FileItem, + common.DataDictRole: weakref.ref(data), common.ItemTabRole: common.TaskTab, # common.EntryRole: [entry, ], diff --git a/bookmarks/maya/main.py b/bookmarks/maya/main.py index 984a45c4..73b87774 100644 --- a/bookmarks/maya/main.py +++ b/bookmarks/maya/main.py @@ -464,7 +464,7 @@ def __init__(self, parent=None): self._workspacecontrol = None self._callbacks = [] # Maya api callbacks - self.setWindowTitle(common.product) + self.setWindowTitle(common.product.title()) common.set_stylesheet(self) # Rename object diff --git a/bookmarks/shortcuts.py b/bookmarks/shortcuts.py index fc27e5d6..e7a3b0df 100644 --- a/bookmarks/shortcuts.py +++ b/bookmarks/shortcuts.py @@ -78,7 +78,7 @@ 'value': 'Ctrl+Shift+N', 'default': 'Ctrl+Shift+N', 'repeat': False, - 'description': f'Open a new {common.product} instance...', + 'description': f'Open a new {common.product.title()} instance...', 'shortcut': None, }, RowIncrease: { @@ -281,28 +281,28 @@ 'value': 'Ctrl+.', 'default': 'Ctrl+.', 'repeat': False, - 'description': 'Show {} Preferences'.format(common.product), + 'description': f'Show {common.product.title()} Preferences', 'shortcut': None, }, OpenTodo: { 'value': 'Alt+N', 'default': 'Alt+N', 'repeat': False, - 'description': 'Show {} Preferences'.format(common.product), + 'description': f'Show {common.product.title()} Preferences', 'shortcut': None, }, ToggleItemArchived: { 'value': 'Ctrl+A', 'default': 'Ctrl+A', 'repeat': False, - 'description': 'Show {} Preferences'.format(common.product), + 'description': f'Show {common.product.title()} Preferences', 'shortcut': None, }, ToggleItemFavourite: { 'value': 'Ctrl+S', 'default': 'Ctrl+S', 'repeat': False, - 'description': 'Show {} Preferences'.format(common.product), + 'description': f'Show {common.product.title()} Preferences', 'shortcut': None, }, PushToRV: { diff --git a/bookmarks/standalone.py b/bookmarks/standalone.py index d9a846fa..8a435858 100644 --- a/bookmarks/standalone.py +++ b/bookmarks/standalone.py @@ -141,7 +141,7 @@ def __init__(self, parent=None): w = TrayMenu(parent=self.window()) self.setContextMenu(w) - self.setToolTip(common.product) + self.setToolTip(common.product.title()) self.activated.connect(self.tray_activated) @@ -405,7 +405,7 @@ def __init__(self, args): super().__init__([__file__, '-platform', 'windows:dpiawareness=2']) _set_application_properties(app=self) self.setApplicationVersion(__version__) - self.setApplicationName(common.product) + self.setApplicationName(common.product.title()) self.setAttribute(QtCore.Qt.AA_UseHighDpiPixmaps, bool=True) self._set_model_id() diff --git a/bookmarks/statusbar.py b/bookmarks/statusbar.py index 62a89631..47e054ca 100644 --- a/bookmarks/statusbar.py +++ b/bookmarks/statusbar.py @@ -73,7 +73,7 @@ def __init__(self, parent=None): 'check', (common.color(common.color_green), common.color(common.color_red)), common.size(common.size_margin), - description=f'Click to toggle {common.product}.', + description=f'Click to toggle {common.product.title()}.', parent=parent ) self.setMouseTracking(True) diff --git a/bookmarks/threads/workers.py b/bookmarks/threads/workers.py index a4ea0686..d330fc97 100644 --- a/bookmarks/threads/workers.py +++ b/bookmarks/threads/workers.py @@ -86,14 +86,19 @@ def func_wrapper(self, *args, **kwargs): if not ref() or self.interrupt: return - # Let the source model know that we loaded a data segment fully if ref().data_type in (common.FileItem, common.SequenceItem): - # Mark the model loaded + # Mark the internal model loaded ref().loaded = True if not ref() or self.interrupt: return - self.sort_data_type(ref) + + # Sort the data + data = self.sort_internal_data(ref) + + # Signal the world + common.signals.internalDataReady.emit(weakref.ref(data)) + return if not ref() or self.interrupt: @@ -136,6 +141,7 @@ class BaseWorker(QtCore.QObject): coreDataLoaded = QtCore.Signal(weakref.ref, weakref.ref) coreDataReset = QtCore.Signal() + dataTypeAboutToBeSorted = QtCore.Signal(int) dataTypeSorted = QtCore.Signal(int) queueItems = QtCore.Signal(list) @@ -170,7 +176,7 @@ def init_worker(self): from . import threads self.queue_timer = common.Timer(parent=self) - self.queue_timer.setObjectName('{}Timer_{}'.format(self.queue, uuid.uuid1().hex)) + self.queue_timer.setObjectName(f'{self.queue}Timer_{uuid.uuid1().hex}') self.queue_timer.setInterval(1) # Local direct worker signal connections @@ -205,7 +211,10 @@ def init_worker(self): if threads.THREADS[q]['preload'] and model and widget: model.coreDataLoaded.connect(self.coreDataLoaded, cnx) - self.dataTypeSorted.connect(model.dataTypeSorted, cnx) + + self.dataTypeAboutToBeSorted.connect(model.internal_data_about_to_be_sorted, cnx) + self.dataTypeSorted.connect(model.internal_data_sorted, cnx) + common.signals.databaseValueUpdated.connect(self.databaseValueUpdated, cnx) self.sgEntityDataReady.connect(common.signals.sgEntityDataReady, cnx) @@ -214,10 +223,10 @@ def update_changed_database_value(self, table, source, key, value): """Process changes when any bookmark database value changes. Args: - tab_type (idx): A tab type used to match the slot with the model type. + table (str): The database table. source (str): A file path. - role (int): An item role. - v (object): The value to set. + key (str): The database value key (column). + value (object): The value to set. Returns: type: Description of returned object. @@ -320,7 +329,7 @@ def queue_model(self, data_type_ref1, data_type_ref2): self.queue_timer.start() @common.error - def sort_data_type(self, ref): + def sort_internal_data(self, ref): """Sorts the data of the given data type. Args: @@ -331,7 +340,7 @@ def sort_data_type(self, ref): model = _model(self.queue) if not model: - return + return None sort_by = model.sort_by() sort_order = model.sort_order() @@ -341,15 +350,23 @@ def sort_data_type(self, ref): t = ref().data_type if not ref(): - return + return None + + if model.data_type() == t: + self.dataTypeAboutToBeSorted.emit(t) + d = common.sort_data(ref, sort_by, sort_order) + if not ref(): - return - common.set_data(p, k, t, d) + return None + + data = common.set_data(p, k, t, d) if model.data_type() == t: self.dataTypeSorted.emit(t) + return data + @QtCore.Slot() def clear_queue(self): """Slot called by the `resetQueue` signal and is responsible for @@ -371,6 +388,12 @@ def clear_queue(self): @common.error @QtCore.Slot(weakref.ref) def process_data(self, ref): + """Processes the given data item. + + Args: + ref (weakref.ref): A data item. + + """ # Do nothing by default if not ref() or self.interrupt: return False @@ -511,7 +534,7 @@ def process_data(self, ref): """Populates the item with the missing file information. Args: - ref (weakref): An internal model data item's weakref. + ref (weakref.ref): A data item as created by the :meth:`bookmarks.items.models.ItemModel.init_data` method. Returns: bool: `True` on success, `False` otherwise. @@ -537,6 +560,8 @@ def process_data(self, ref): def _process_data(self, ref): """Utility method for :meth:`process_data. + ref (weakref): A data item as created by the :meth:`bookmarks.items.models.ItemModel.init_data` method. + """ pp = ref()[common.ParentPathRole] st = ref()[common.PathRole] @@ -579,6 +604,24 @@ def _process_data(self, ref): force_exists=True ) ) + + # Asset ShotGrid task to list + if len(pp) == 4 and asset_row_data['sg_task_name'] and ref()[common.DataDictRole]: + if ref(): + _ref = ref()[common.DataDictRole] + + # TODO: This does not seem to be thread safe (?) but since we only have one asset worker + # it should be fine for now. + if _ref(): + if asset_row_data['sg_task_name'] and asset_row_data['sg_task_name'].lower() not in _ref().sg_task_names: + if _ref(): + _ref().sg_task_names.append(asset_row_data['sg_task_name'].lower()) + + if _ref(): + if asset_row_data['shotgun_name'] and asset_row_data['shotgun_name'].lower() not in _ref().shotgun_names: + if _ref(): + _ref().shotgun_names.append(asset_row_data['shotgun_name'].lower()) + # ShotGrid status if len(pp) <= 4: update_shotgun_configured(pp, bookmark_row_data, asset_row_data, ref) @@ -730,7 +773,7 @@ def process_data(self, ref): for details. Args: - ref (weakref.ref): A weakref to a data segment. + ref (weakref.ref): A data item as created by the :meth:`bookmarks.items.models.ItemModel.init_data` method. Returns: ref or None: `ref` if loaded successfully, else `None`. @@ -824,7 +867,7 @@ class TransactionsWorker(BaseWorker): """ @common.error - def process_data(self): + def process_data(self, *args, **kwargs): verify_thread_affinity() if self.interrupt: @@ -843,7 +886,7 @@ class SGWorker(BaseWorker): """This worker is used to retrieve data from ShotGrid.""" @common.error - def process_data(self): + def process_data(self, *args, **kwargs): verify_thread_affinity() if self.interrupt: diff --git a/bookmarks/topbar/sgfilter.py b/bookmarks/topbar/sgfilter.py new file mode 100644 index 00000000..57a71726 --- /dev/null +++ b/bookmarks/topbar/sgfilter.py @@ -0,0 +1,195 @@ +"""""" +import weakref + +from PySide2 import QtCore, QtGui, QtWidgets + + +from .. import common +from .. import ui + + +class BaseFilterModel(ui.AbstractListModel): + + def __init__(self, section_name_label, data_source, parent=None): + + self.show_all_label = ' - Show All -' + self.section_name_label = section_name_label + self.data_source = data_source + + super().__init__(parent=parent) + + common.signals.internalDataReady.connect(self.internal_data_ready) + common.signals.bookmarksChanged.connect(self.reset_data) + + @QtCore.Slot(weakref.ref) + def internal_data_ready(self, ref): + if not ref(): + return + + source_model = common.source_model(common.AssetTab) + data = common.get_data( + source_model.source_path(), + source_model.task(), + source_model.data_type() + ) + + if ref() != data: + return + + self.reset_data() + + def reset_data(self): + self.beginResetModel() + self.init_data() + self.endResetModel() + + def init_data(self, *args, **kwargs): + """Initializes data. + + """ + self._data = common.DataDict() + + source_model = common.source_model(common.AssetTab) + + data = common.get_data( + source_model.source_path(), + source_model.task(), + source_model.data_type() + ) + if not getattr(data, self.data_source): + return + + self._data[len(self._data)] = { + QtCore.Qt.DisplayRole: self.section_name_label, + QtCore.Qt.SizeHintRole: QtCore.QSize(1, common.size(common.size_row_height) * 0.66), + common.FlagsRole: QtCore.Qt.NoItemFlags, + QtCore.Qt.TextAlignmentRole: QtCore.Qt.AlignCenter, + } + + self._data[len(self._data)] = { + QtCore.Qt.DisplayRole: self.show_all_label, + QtCore.Qt.SizeHintRole: self.row_size, + QtCore.Qt.TextAlignmentRole: QtCore.Qt.AlignCenter, + } + + icon = ui.get_icon('sg') + + for task in sorted(getattr(data, self.data_source)): + self._data[len(self._data)] = { + QtCore.Qt.DisplayRole: task, + QtCore.Qt.SizeHintRole: self.row_size, + QtCore.Qt.DecorationRole: icon, + QtCore.Qt.StatusTipRole: task, + QtCore.Qt.AccessibleDescriptionRole: task, + QtCore.Qt.WhatsThisRole: task, + QtCore.Qt.ToolTipRole: task, + QtCore.Qt.TextAlignmentRole: QtCore.Qt.AlignCenter, + } + + +class BaseFilterButton(QtWidgets.QComboBox): + """The combo box used to set a text filter based on the available ShotGrid task names. + + """ + def __init__(self, Model, parent=None): + super().__init__(parent=parent) + self.setView(QtWidgets.QListView(parent=self)) + self.view().setMinimumWidth(int(common.size(common.size_width) * 0.66)) + self.setModel(Model()) + + self.setFixedHeight(common.size(common.size_margin)) + self.setMinimumWidth(common.size(common.size_margin) * 6) + + common.signals.updateTopBarButtons.connect(lambda: self.setHidden(not common.current_tab() == common.AssetTab)) + common.model(common.AssetTab).filterTextChanged.connect(self.select_text) + common.signals.internalDataReady.connect(self.select_text) + + self.textActivated.connect(self.update_filter_text) + + @QtCore.Slot(str) + def update_filter_text(self, text): + """Update the filter text. + + Args: + text (str): The text to set as the filter text. + + """ + if text == self.model().show_all_label: + text = '' + else: + text = f'"{text.lower().strip()}"' + + common.model(common.AssetTab).set_filter_text(text) + + @QtCore.Slot() + def select_text(self, *args, **kwargs): + """Update the filter text. + + """ + self.setCurrentIndex(-1) + + _text = common.model(common.AssetTab).filter_text() + _text = _text.lower().strip() if _text else '' + + if not _text: + return + + for i in range(self.count()): + text = self.itemText(i) + if not text: + continue + if text == self.model().show_all_label: + continue + if text == self.model().section_name_label: + continue + + text = text.lower().strip() + if _text == text: + self.setCurrentIndex(i) + return + + if _text == f'"{text}"': + self.setCurrentIndex(i) + return + + +class TaskFilterModel(BaseFilterModel): + + def __init__(self, parent=None): + super().__init__( + 'ShotGrid Tasks', + 'sg_task_names', + parent=parent + ) + + +class TaskFilterButton(BaseFilterButton): + """The combo box used to set a text filter based on the available ShotGrid task names. + + """ + def __init__(self, parent=None): + super().__init__( + TaskFilterModel, + parent=parent + ) + + +class EntityFilterModel(BaseFilterModel): + + def __init__(self, parent=None): + super().__init__( + 'ShotGrid Entities', + 'shotgun_names', + parent=parent + ) + + +class EntityFilterButton(BaseFilterButton): + """The combo box used to set a text filter based on the available ShotGrid task names. + + """ + def __init__(self, parent=None): + super().__init__( + EntityFilterModel, + parent=parent + ) \ No newline at end of file diff --git a/bookmarks/topbar/topbar.py b/bookmarks/topbar/topbar.py index 94dd42b5..d8943b75 100644 --- a/bookmarks/topbar/topbar.py +++ b/bookmarks/topbar/topbar.py @@ -5,6 +5,7 @@ from . import buttons from . import tabs +from . import sgfilter from .. import common from .. import images @@ -28,13 +29,21 @@ 'hidden': False, }, next(n): { - 'widget': buttons.RefreshButton, + 'widget': sgfilter.EntityFilterButton, + 'hidden': False, + }, + next(n): { + 'widget': sgfilter.TaskFilterButton, 'hidden': False, }, next(n): { 'widget': buttons.FilterButton, 'hidden': False, }, + next(n): { + 'widget': buttons.RefreshButton, + 'hidden': False, + }, next(n): { 'widget': buttons.ToggleSequenceButton, 'hidden': False, diff --git a/bookmarks/ui.py b/bookmarks/ui.py index 8fae55e8..71040c67 100644 --- a/bookmarks/ui.py +++ b/bookmarks/ui.py @@ -1372,6 +1372,7 @@ def __init__( ): super().__init__(parent=parent) + self.anim = None self.scroll_area = None self.columns = columns self._label = label @@ -1524,7 +1525,7 @@ def done(self, r): self.anim.start() self.anim.finished.connect(lambda: super(GalleryWidget, self).done(r)) else: - super(GalleryWidget, self).done(r) + super().done(r) class AbstractListModel(QtCore.QAbstractListModel): @@ -1538,10 +1539,7 @@ def __init__(self, parent=None): super().__init__(parent=parent) self._data = {} - - self.beginResetModel() - self.init_data() - self.endResetModel() + self.reset_data() def rowCount(self, parent=QtCore.QModelIndex()): return len(self._data) @@ -1549,6 +1547,12 @@ def rowCount(self, parent=QtCore.QModelIndex()): def display_name(self, v): return v.replace('/', ' | ') + def reset_data(self): + """Resets the model's data.""" + self.beginResetModel() + self.init_data() + self.endResetModel() + def init_data(self, *args, **kwargs): raise NotImplementedError('Abstract method must be implemented by subclass.') From 78ddd04d3c6c4f570cc4f369e4b042759faffe82 Mon Sep 17 00:00:00 2001 From: Gergely Wootsch Date: Fri, 20 Oct 2023 08:14:10 +0200 Subject: [PATCH 11/32] Local script changes --- bookmarks/maya/scripts/aka_cloth_cache.py | 12 +++++++----- bookmarks/maya/scripts/aka_cloth_cache_animated.py | 12 +++++++----- bookmarks/maya/scripts/aka_make_export_sets.py | 3 +++ 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/bookmarks/maya/scripts/aka_cloth_cache.py b/bookmarks/maya/scripts/aka_cloth_cache.py index 523f2f74..9873a28b 100644 --- a/bookmarks/maya/scripts/aka_cloth_cache.py +++ b/bookmarks/maya/scripts/aka_cloth_cache.py @@ -28,7 +28,7 @@ studiolibrary_dir = f'{common.active("server")}/{common.active("job")}/070_Assets/Character/studiolibrary' reset_pose = f'{studiolibrary_dir}/Characters/IbogaineMarcus/Reset/A-Pose-v2-FullFK.pose/pose.json' -cache_destination_dir = '{studiolibrary_dir}/Shots/MAB/{asset0}_{shot}' +cache_destination_dir = '{studiolibrary_dir}/Shots/{prefix}/{asset0}_{shot}' namespace = 'IbogaineMarcus_01' controllers_set = f'{namespace}:rig_controllers_grp' @@ -103,12 +103,18 @@ def run(): config = tokens.get(*common.active('root', args=True)) + db = database.get(*common.active('root', args=True)) + cut_in = db.value(common.active('asset', path=True), 'cut_in', database.AssetTable) + cut_out = db.value(common.active('asset', path=True), 'cut_out', database.AssetTable) + prefix = db.value(common.active('root', path=True), 'prefix', database.BookmarkTable) + seq, shot = common.get_sequence_and_shot(common.active('asset')) destination_dir = config.expand_tokens( cache_destination_dir, asset=common.active('asset'), shot=shot, sequence=seq, + prefix=prefix.split('_')[-1].upper(), studiolibrary_dir=studiolibrary_dir ) @@ -118,10 +124,6 @@ def run(): animation_start = cmds.playbackOptions(minTime=True, query=True) animation_end = cmds.playbackOptions(maxTime=True, query=True) - db = database.get(*common.active('root', args=True)) - cut_in = db.value(common.active('asset', path=True), 'cut_in', database.AssetTable) - cut_out = db.value(common.active('asset', path=True), 'cut_out', database.AssetTable) - cmds.select(clear=True) # Create the export groups diff --git a/bookmarks/maya/scripts/aka_cloth_cache_animated.py b/bookmarks/maya/scripts/aka_cloth_cache_animated.py index a44bf13c..7335728d 100644 --- a/bookmarks/maya/scripts/aka_cloth_cache_animated.py +++ b/bookmarks/maya/scripts/aka_cloth_cache_animated.py @@ -28,7 +28,7 @@ studiolibrary_dir = f'{common.active("server")}/{common.active("job")}/070_Assets/Character/studiolibrary' reset_pose = f'{studiolibrary_dir}/Characters/IbogaineMarcus/Reset/A-Pose-v2-FullFK.pose/pose.json' -cache_destination_dir = '{studiolibrary_dir}/Shots/MAB/{asset0}_{shot}' +cache_destination_dir = '{studiolibrary_dir}/Shots/{prefix}/{asset0}_{shot}' namespace = 'IbogaineMarcus_01' controllers_set = f'{namespace}:rig_controllers_grp' @@ -103,12 +103,18 @@ def run(): config = tokens.get(*common.active('root', args=True)) + db = database.get(*common.active('root', args=True)) + cut_in = db.value(common.active('asset', path=True), 'cut_in', database.AssetTable) + cut_out = db.value(common.active('asset', path=True), 'cut_out', database.AssetTable) + prefix = db.value(common.active('root', path=True), 'prefix', database.BookmarkTable) + seq, shot = common.get_sequence_and_shot(common.active('asset')) destination_dir = config.expand_tokens( cache_destination_dir, asset=common.active('asset'), shot=shot, sequence=seq, + prefix=prefix.split('_')[-1].upper(), studiolibrary_dir=studiolibrary_dir ) @@ -118,10 +124,6 @@ def run(): animation_start = cmds.playbackOptions(minTime=True, query=True) animation_end = cmds.playbackOptions(maxTime=True, query=True) - db = database.get(*common.active('root', args=True)) - cut_in = db.value(common.active('asset', path=True), 'cut_in', database.AssetTable) - cut_out = db.value(common.active('asset', path=True), 'cut_out', database.AssetTable) - cmds.select(clear=True) # Create the export groups diff --git a/bookmarks/maya/scripts/aka_make_export_sets.py b/bookmarks/maya/scripts/aka_make_export_sets.py index 077b9a60..ce7c9131 100644 --- a/bookmarks/maya/scripts/aka_make_export_sets.py +++ b/bookmarks/maya/scripts/aka_make_export_sets.py @@ -53,6 +53,9 @@ def run(): 'IbogaineMarcus_body_export': ['*Marcus*:head_geo', '*Marcus*:body_geo', '*Marcus*:shoes_geo'], 'IbogaineMarcus_cloth_export': ['*Marcus*:tshirt_geo', '*Marcus*:trousers_geo'], 'IbogaineMarcus_extra_export': ['*Marcus*:eye_L', '*Marcus*:eye_R'], + 'IbogaineMarcus_body_mirrored_export': ['*Marcus*:head_geo_mirrored', '*Marcus*:body_geo_mirrored', '*Marcus*:shoes_geo_mirrored'], + 'IbogaineMarcus_cloth_mirrored_export': ['*Marcus*:tshirt_geo_mirrored', '*Marcus*:trousers_geo_mirrored'], + 'IbogaineMarcus_extra_mirrored_export': ['*Marcus*:eye_L_mirrored', '*Marcus*:eye_R_mirrored'], 'set_ground_export': ['set_ground_geo', ], 'set_background_export': ['set_background_geo', ], 'set_floor_export': ['set_floor_geo', ], From 7a30a1780762ce4598908bc43e9e2d427ad0e1bd Mon Sep 17 00:00:00 2001 From: Gergely Wootsch Date: Fri, 20 Oct 2023 16:34:41 +0200 Subject: [PATCH 12/32] Add ShotGrid entity and task filter menus This commit adds two new filter setter ui menus to the top bar to set the ShotGrid entity OR the ShotGrid task. The process involved a cleanup pass on some of the ui and backend. I fixed some bugs related to how the model's internal data was sorted previously and this made me realise the DataDict implementation had a bug - it wasn't copying over all properties correctly. The new filter menus values are populated by the worker threads and are saved into the main DataDict instances the filter combo box models read. --- bookmarks/actions.py | 6 +- bookmarks/common/core.py | 25 +++- bookmarks/common/data.py | 11 ++ bookmarks/common/signals.py | 7 ++ bookmarks/contextmenu.py | 2 +- bookmarks/items/asset_items.py | 2 + bookmarks/items/bookmark_items.py | 2 + bookmarks/items/favourite_items.py | 2 + bookmarks/items/file_items.py | 2 + bookmarks/items/models.py | 18 +-- bookmarks/items/task_items.py | 2 + bookmarks/maya/main.py | 2 +- bookmarks/shortcuts.py | 10 +- bookmarks/standalone.py | 4 +- bookmarks/statusbar.py | 2 +- bookmarks/threads/workers.py | 77 +++++++++--- bookmarks/topbar/sgfilter.py | 195 +++++++++++++++++++++++++++++ bookmarks/topbar/topbar.py | 11 +- bookmarks/ui.py | 14 ++- 19 files changed, 349 insertions(+), 45 deletions(-) create mode 100644 bookmarks/topbar/sgfilter.py diff --git a/bookmarks/actions.py b/bookmarks/actions.py index d7138e96..f8d6d5ec 100644 --- a/bookmarks/actions.py +++ b/bookmarks/actions.py @@ -1846,11 +1846,9 @@ def pick_template(mode): dialog.setNameFilters(['*.zip', ]) dialog.setFilter(QtCore.QDir.Files | QtCore.QDir.NoDotAndDotDot) dialog.setLabelText( - QtWidgets.QFileDialog.Accept, 'Select a {} template'.format(mode.title()) - ) - dialog.setWindowTitle( - 'Select *.zip archive to use as a {} template'.format(mode.lower()) + QtWidgets.QFileDialog.Accept, f'Select a {mode.title()} template' ) + dialog.setWindowTitle(f'Select *.zip archive to use as a {mode.lower()} template') if dialog.exec_() == QtWidgets.QDialog.Rejected: return source = next((f for f in dialog.selectedFiles()), None) diff --git a/bookmarks/common/core.py b/bookmarks/common/core.py index 82e22dc5..a97b2aa4 100644 --- a/bookmarks/common/core.py +++ b/bookmarks/common/core.py @@ -121,6 +121,8 @@ def _idx_func(reset=False, start=None): QueueRole = QtCore.Qt.ItemDataRole(idx()) #: List item role for getting the item's data type (sequence or file) DataTypeRole = QtCore.Qt.ItemDataRole(idx()) +#: List item role for getting the container data dictionary +DataDictRole = QtCore.Qt.ItemDataRole(idx()) #: The view tab associated with the item ItemTabRole = QtCore.Qt.ItemDataRole(idx()) #: Data used to sort the items by name @@ -640,15 +642,18 @@ class DataDict(dict): """Custom dictionary class used to store model item data. This class adds compatibility for :class:`weakref.ref` referencing - and custom attributes for storing data state. + and custom attributes for storing data states. """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + self._loaded = False self._refresh_needed = False self._data_type = None + self._shotgun_names = [] + self._sg_task_names = [] @property def loaded(self): @@ -686,6 +691,24 @@ def data_type(self): def data_type(self, v): self._data_type = v + @property + def shotgun_names(self): + """Returns a list of Shotgun task names associated with the data dictionary.""" + return self._shotgun_names + + @shotgun_names.setter + def shotgun_names(self, v): + self._shotgun_names = v + + @property + def sg_task_names(self): + """Returns a list of Shotgun task names associated with the data dictionary.""" + return self._sg_task_names + + @sg_task_names.setter + def sg_task_names(self, v): + self._sg_task_names = v + class Timer(QtCore.QTimer): """A custom QTimer class used across the app. diff --git a/bookmarks/common/data.py b/bookmarks/common/data.py index d6f4b538..be3b6419 100644 --- a/bookmarks/common/data.py +++ b/bookmarks/common/data.py @@ -41,14 +41,20 @@ def sort_key(_idx): ) d = common.DataDict() + + # Copy over the property values from the source data d.loaded = ref().loaded d.data_type = ref().data_type + d.refresh_needed = ref().refresh_needed + d.shotgun_names = ref().shotgun_names + d.sg_task_names = ref().sg_task_names for n, idx in enumerate(sorted_idxs): if not ref(): raise RuntimeError('Model mutated during sorting.') d[n] = ref()[idx] d[n][common.IdRole] = n + return d @@ -209,6 +215,9 @@ def set_data(key, task, data_type, data): data_type (int): One of :attr:`bookmarks.common.FileItem` or :attr:`bookmarks.common.SequenceItem`. data (common.DataDict): The data to set in the cache. + Returns: + common.DataDict: The cached data. + """ common.check_type(key, tuple) common.check_type(task, str) @@ -219,3 +228,5 @@ def set_data(key, task, data_type, data): elif task not in common.item_data[key]: reset_data(key, task) common.item_data[key][task][data_type] = data + + return common.item_data[key][task][data_type] diff --git a/bookmarks/common/signals.py b/bookmarks/common/signals.py index 91c4f7c9..fea575c4 100644 --- a/bookmarks/common/signals.py +++ b/bookmarks/common/signals.py @@ -2,6 +2,7 @@ """ import functools +import weakref from PySide2 import QtCore @@ -17,6 +18,9 @@ class CoreSignals(QtCore.QObject): """A utility class used to store application-wide signals. """ + #: Signal emitted by worker threads when the internal data of a model is fully loaded + internalDataReady = QtCore.Signal(weakref.ref) + #: Update top bar widget buttons updateTopBarButtons = QtCore.Signal() @@ -78,6 +82,9 @@ class CoreSignals(QtCore.QObject): #: Signals a filter button state change toggleFavouritesButton = QtCore.Signal() + #: Signal emitted when the filter text changes of a list view's proxy model + filterTextChanged = QtCore.Signal(str) + #: Signal emitted when the active path mode changes activeModeChanged = QtCore.Signal(int) diff --git a/bookmarks/contextmenu.py b/bookmarks/contextmenu.py index 6b03e368..1a4d3dfc 100644 --- a/bookmarks/contextmenu.py +++ b/bookmarks/contextmenu.py @@ -832,7 +832,7 @@ def quit_menu(self): if common.init_mode == common.EmbeddedMode: return self.menu[key()] = { - 'text': 'Quit {}'.format(common.product), + 'text': f'Quit {common.product.title()}', 'action': common.uninitialize, 'icon': ui.get_icon('close'), 'shortcut': shortcuts.get( diff --git a/bookmarks/items/asset_items.py b/bookmarks/items/asset_items.py index 468e8e65..50147109 100644 --- a/bookmarks/items/asset_items.py +++ b/bookmarks/items/asset_items.py @@ -32,6 +32,7 @@ import copy import functools import os +import weakref from PySide2 import QtCore, QtWidgets @@ -265,6 +266,7 @@ def init_data(self): # common.QueueRole: self.queues, common.DataTypeRole: t, + common.DataDictRole: weakref.ref(data), common.ItemTabRole: common.AssetTab, # common.EntryRole: [], diff --git a/bookmarks/items/bookmark_items.py b/bookmarks/items/bookmark_items.py index 3ed27101..aeb71d50 100644 --- a/bookmarks/items/bookmark_items.py +++ b/bookmarks/items/bookmark_items.py @@ -54,6 +54,7 @@ """ +import weakref from PySide2 import QtCore, QtWidgets @@ -236,6 +237,7 @@ def init_data(self): # common.QueueRole: self.queues, common.DataTypeRole: t, + common.DataDictRole: weakref.ref(data), common.ItemTabRole: common.BookmarkTab, # common.FlagsRole: flags, diff --git a/bookmarks/items/favourite_items.py b/bookmarks/items/favourite_items.py index bd74714a..4164fd04 100644 --- a/bookmarks/items/favourite_items.py +++ b/bookmarks/items/favourite_items.py @@ -2,6 +2,7 @@ """ import os +import weakref from PySide2 import QtCore, QtWidgets @@ -131,6 +132,7 @@ def init_data(self): # common.QueueRole: self.queues, common.DataTypeRole: t, + common.DataDictRole: weakref.ref(data), common.ItemTabRole: common.FavouriteTab, # common.EntryRole: [entry, ], diff --git a/bookmarks/items/file_items.py b/bookmarks/items/file_items.py index b4bc02f8..7c894a7b 100644 --- a/bookmarks/items/file_items.py +++ b/bookmarks/items/file_items.py @@ -40,6 +40,7 @@ """ import functools import os +import weakref from PySide2 import QtWidgets, QtCore @@ -399,6 +400,7 @@ def init_data(self): # common.QueueRole: self.queues, common.DataTypeRole: common.FileItem, + common.DataDictRole: weakref.ref(data), common.ItemTabRole: common.FileTab, # common.EntryRole: [entry, ], diff --git a/bookmarks/items/models.py b/bookmarks/items/models.py index cfb61539..1a638089 100644 --- a/bookmarks/items/models.py +++ b/bookmarks/items/models.py @@ -161,8 +161,6 @@ class ItemModel(QtCore.QAbstractTableModel): missing data. coreDataReset (QtCore.Signal): Signals that the underlying model data has been reset. Used by the thread workers to empty their queues. - dataTypeSorted (QtCore.Signal -> int): Signals that the underlying model - data was sorted. sortingChanged (QtCore.Signal -> int, bool): Emitted when the sorting order or sorting role was changed by the user. activeChanged (QtCore.Signal): Signals :meth:`.ItemModel.active_index` @@ -177,7 +175,6 @@ class ItemModel(QtCore.QAbstractTableModel): coreDataLoaded = QtCore.Signal(weakref.ref, weakref.ref) coreDataReset = QtCore.Signal() - dataTypeSorted = QtCore.Signal(int) sortingChanged = QtCore.Signal(int, bool) # (SortRole, SortOrder) activeChanged = QtCore.Signal(QtCore.QModelIndex) @@ -207,7 +204,6 @@ def __init__(self, parent=None): self._datatype = {} # used by the files model only self.sortingChanged.connect(self.set_sorting) - self.dataTypeSorted.connect(self.emit_reset_model) self.modelAboutToBeReset.connect(common.signals.updateTopBarButtons) self.modelReset.connect(common.signals.updateTopBarButtons) @@ -485,14 +481,19 @@ def reset_data(self, *args, force=False, emit_active=True): self.set_active(index) @QtCore.Slot(int) - def emit_reset_model(self, data_type): + def internal_data_about_to_be_sorted(self, data_type): + if self.data_type() != data_type: + return + self.layoutAboutToBeChanged.emit() + + @QtCore.Slot(int) + def internal_data_sorted(self, data_type): """Slot used to emit the reset model signals. """ if self.data_type() != data_type: return - self.beginResetModel() - self.endResetModel() + self.layoutChanged.emit() @QtCore.Slot(bool) @QtCore.Slot(int) @@ -848,6 +849,9 @@ def __init__(self, parent=None): self.queued_invalidate_timer.timeout.connect(self.delayed_invalidate) + # Notify the outside world that the filter text has changed + self.filterTextChanged.connect(common.signals.filterTextChanged) + def verify(self): """Verify the filter model contents to make sure archived items remain hidden when they're not meant to be visible. diff --git a/bookmarks/items/task_items.py b/bookmarks/items/task_items.py index 825f71ba..6a50d92c 100644 --- a/bookmarks/items/task_items.py +++ b/bookmarks/items/task_items.py @@ -9,6 +9,7 @@ """ import functools import os +import weakref from PySide2 import QtWidgets, QtGui, QtCore @@ -308,6 +309,7 @@ def init_data(self): # common.QueueRole: self.queues, common.DataTypeRole: common.FileItem, + common.DataDictRole: weakref.ref(data), common.ItemTabRole: common.TaskTab, # common.EntryRole: [entry, ], diff --git a/bookmarks/maya/main.py b/bookmarks/maya/main.py index 984a45c4..73b87774 100644 --- a/bookmarks/maya/main.py +++ b/bookmarks/maya/main.py @@ -464,7 +464,7 @@ def __init__(self, parent=None): self._workspacecontrol = None self._callbacks = [] # Maya api callbacks - self.setWindowTitle(common.product) + self.setWindowTitle(common.product.title()) common.set_stylesheet(self) # Rename object diff --git a/bookmarks/shortcuts.py b/bookmarks/shortcuts.py index fc27e5d6..e7a3b0df 100644 --- a/bookmarks/shortcuts.py +++ b/bookmarks/shortcuts.py @@ -78,7 +78,7 @@ 'value': 'Ctrl+Shift+N', 'default': 'Ctrl+Shift+N', 'repeat': False, - 'description': f'Open a new {common.product} instance...', + 'description': f'Open a new {common.product.title()} instance...', 'shortcut': None, }, RowIncrease: { @@ -281,28 +281,28 @@ 'value': 'Ctrl+.', 'default': 'Ctrl+.', 'repeat': False, - 'description': 'Show {} Preferences'.format(common.product), + 'description': f'Show {common.product.title()} Preferences', 'shortcut': None, }, OpenTodo: { 'value': 'Alt+N', 'default': 'Alt+N', 'repeat': False, - 'description': 'Show {} Preferences'.format(common.product), + 'description': f'Show {common.product.title()} Preferences', 'shortcut': None, }, ToggleItemArchived: { 'value': 'Ctrl+A', 'default': 'Ctrl+A', 'repeat': False, - 'description': 'Show {} Preferences'.format(common.product), + 'description': f'Show {common.product.title()} Preferences', 'shortcut': None, }, ToggleItemFavourite: { 'value': 'Ctrl+S', 'default': 'Ctrl+S', 'repeat': False, - 'description': 'Show {} Preferences'.format(common.product), + 'description': f'Show {common.product.title()} Preferences', 'shortcut': None, }, PushToRV: { diff --git a/bookmarks/standalone.py b/bookmarks/standalone.py index d9a846fa..8a435858 100644 --- a/bookmarks/standalone.py +++ b/bookmarks/standalone.py @@ -141,7 +141,7 @@ def __init__(self, parent=None): w = TrayMenu(parent=self.window()) self.setContextMenu(w) - self.setToolTip(common.product) + self.setToolTip(common.product.title()) self.activated.connect(self.tray_activated) @@ -405,7 +405,7 @@ def __init__(self, args): super().__init__([__file__, '-platform', 'windows:dpiawareness=2']) _set_application_properties(app=self) self.setApplicationVersion(__version__) - self.setApplicationName(common.product) + self.setApplicationName(common.product.title()) self.setAttribute(QtCore.Qt.AA_UseHighDpiPixmaps, bool=True) self._set_model_id() diff --git a/bookmarks/statusbar.py b/bookmarks/statusbar.py index 62a89631..47e054ca 100644 --- a/bookmarks/statusbar.py +++ b/bookmarks/statusbar.py @@ -73,7 +73,7 @@ def __init__(self, parent=None): 'check', (common.color(common.color_green), common.color(common.color_red)), common.size(common.size_margin), - description=f'Click to toggle {common.product}.', + description=f'Click to toggle {common.product.title()}.', parent=parent ) self.setMouseTracking(True) diff --git a/bookmarks/threads/workers.py b/bookmarks/threads/workers.py index a4ea0686..d330fc97 100644 --- a/bookmarks/threads/workers.py +++ b/bookmarks/threads/workers.py @@ -86,14 +86,19 @@ def func_wrapper(self, *args, **kwargs): if not ref() or self.interrupt: return - # Let the source model know that we loaded a data segment fully if ref().data_type in (common.FileItem, common.SequenceItem): - # Mark the model loaded + # Mark the internal model loaded ref().loaded = True if not ref() or self.interrupt: return - self.sort_data_type(ref) + + # Sort the data + data = self.sort_internal_data(ref) + + # Signal the world + common.signals.internalDataReady.emit(weakref.ref(data)) + return if not ref() or self.interrupt: @@ -136,6 +141,7 @@ class BaseWorker(QtCore.QObject): coreDataLoaded = QtCore.Signal(weakref.ref, weakref.ref) coreDataReset = QtCore.Signal() + dataTypeAboutToBeSorted = QtCore.Signal(int) dataTypeSorted = QtCore.Signal(int) queueItems = QtCore.Signal(list) @@ -170,7 +176,7 @@ def init_worker(self): from . import threads self.queue_timer = common.Timer(parent=self) - self.queue_timer.setObjectName('{}Timer_{}'.format(self.queue, uuid.uuid1().hex)) + self.queue_timer.setObjectName(f'{self.queue}Timer_{uuid.uuid1().hex}') self.queue_timer.setInterval(1) # Local direct worker signal connections @@ -205,7 +211,10 @@ def init_worker(self): if threads.THREADS[q]['preload'] and model and widget: model.coreDataLoaded.connect(self.coreDataLoaded, cnx) - self.dataTypeSorted.connect(model.dataTypeSorted, cnx) + + self.dataTypeAboutToBeSorted.connect(model.internal_data_about_to_be_sorted, cnx) + self.dataTypeSorted.connect(model.internal_data_sorted, cnx) + common.signals.databaseValueUpdated.connect(self.databaseValueUpdated, cnx) self.sgEntityDataReady.connect(common.signals.sgEntityDataReady, cnx) @@ -214,10 +223,10 @@ def update_changed_database_value(self, table, source, key, value): """Process changes when any bookmark database value changes. Args: - tab_type (idx): A tab type used to match the slot with the model type. + table (str): The database table. source (str): A file path. - role (int): An item role. - v (object): The value to set. + key (str): The database value key (column). + value (object): The value to set. Returns: type: Description of returned object. @@ -320,7 +329,7 @@ def queue_model(self, data_type_ref1, data_type_ref2): self.queue_timer.start() @common.error - def sort_data_type(self, ref): + def sort_internal_data(self, ref): """Sorts the data of the given data type. Args: @@ -331,7 +340,7 @@ def sort_data_type(self, ref): model = _model(self.queue) if not model: - return + return None sort_by = model.sort_by() sort_order = model.sort_order() @@ -341,15 +350,23 @@ def sort_data_type(self, ref): t = ref().data_type if not ref(): - return + return None + + if model.data_type() == t: + self.dataTypeAboutToBeSorted.emit(t) + d = common.sort_data(ref, sort_by, sort_order) + if not ref(): - return - common.set_data(p, k, t, d) + return None + + data = common.set_data(p, k, t, d) if model.data_type() == t: self.dataTypeSorted.emit(t) + return data + @QtCore.Slot() def clear_queue(self): """Slot called by the `resetQueue` signal and is responsible for @@ -371,6 +388,12 @@ def clear_queue(self): @common.error @QtCore.Slot(weakref.ref) def process_data(self, ref): + """Processes the given data item. + + Args: + ref (weakref.ref): A data item. + + """ # Do nothing by default if not ref() or self.interrupt: return False @@ -511,7 +534,7 @@ def process_data(self, ref): """Populates the item with the missing file information. Args: - ref (weakref): An internal model data item's weakref. + ref (weakref.ref): A data item as created by the :meth:`bookmarks.items.models.ItemModel.init_data` method. Returns: bool: `True` on success, `False` otherwise. @@ -537,6 +560,8 @@ def process_data(self, ref): def _process_data(self, ref): """Utility method for :meth:`process_data. + ref (weakref): A data item as created by the :meth:`bookmarks.items.models.ItemModel.init_data` method. + """ pp = ref()[common.ParentPathRole] st = ref()[common.PathRole] @@ -579,6 +604,24 @@ def _process_data(self, ref): force_exists=True ) ) + + # Asset ShotGrid task to list + if len(pp) == 4 and asset_row_data['sg_task_name'] and ref()[common.DataDictRole]: + if ref(): + _ref = ref()[common.DataDictRole] + + # TODO: This does not seem to be thread safe (?) but since we only have one asset worker + # it should be fine for now. + if _ref(): + if asset_row_data['sg_task_name'] and asset_row_data['sg_task_name'].lower() not in _ref().sg_task_names: + if _ref(): + _ref().sg_task_names.append(asset_row_data['sg_task_name'].lower()) + + if _ref(): + if asset_row_data['shotgun_name'] and asset_row_data['shotgun_name'].lower() not in _ref().shotgun_names: + if _ref(): + _ref().shotgun_names.append(asset_row_data['shotgun_name'].lower()) + # ShotGrid status if len(pp) <= 4: update_shotgun_configured(pp, bookmark_row_data, asset_row_data, ref) @@ -730,7 +773,7 @@ def process_data(self, ref): for details. Args: - ref (weakref.ref): A weakref to a data segment. + ref (weakref.ref): A data item as created by the :meth:`bookmarks.items.models.ItemModel.init_data` method. Returns: ref or None: `ref` if loaded successfully, else `None`. @@ -824,7 +867,7 @@ class TransactionsWorker(BaseWorker): """ @common.error - def process_data(self): + def process_data(self, *args, **kwargs): verify_thread_affinity() if self.interrupt: @@ -843,7 +886,7 @@ class SGWorker(BaseWorker): """This worker is used to retrieve data from ShotGrid.""" @common.error - def process_data(self): + def process_data(self, *args, **kwargs): verify_thread_affinity() if self.interrupt: diff --git a/bookmarks/topbar/sgfilter.py b/bookmarks/topbar/sgfilter.py new file mode 100644 index 00000000..57a71726 --- /dev/null +++ b/bookmarks/topbar/sgfilter.py @@ -0,0 +1,195 @@ +"""""" +import weakref + +from PySide2 import QtCore, QtGui, QtWidgets + + +from .. import common +from .. import ui + + +class BaseFilterModel(ui.AbstractListModel): + + def __init__(self, section_name_label, data_source, parent=None): + + self.show_all_label = ' - Show All -' + self.section_name_label = section_name_label + self.data_source = data_source + + super().__init__(parent=parent) + + common.signals.internalDataReady.connect(self.internal_data_ready) + common.signals.bookmarksChanged.connect(self.reset_data) + + @QtCore.Slot(weakref.ref) + def internal_data_ready(self, ref): + if not ref(): + return + + source_model = common.source_model(common.AssetTab) + data = common.get_data( + source_model.source_path(), + source_model.task(), + source_model.data_type() + ) + + if ref() != data: + return + + self.reset_data() + + def reset_data(self): + self.beginResetModel() + self.init_data() + self.endResetModel() + + def init_data(self, *args, **kwargs): + """Initializes data. + + """ + self._data = common.DataDict() + + source_model = common.source_model(common.AssetTab) + + data = common.get_data( + source_model.source_path(), + source_model.task(), + source_model.data_type() + ) + if not getattr(data, self.data_source): + return + + self._data[len(self._data)] = { + QtCore.Qt.DisplayRole: self.section_name_label, + QtCore.Qt.SizeHintRole: QtCore.QSize(1, common.size(common.size_row_height) * 0.66), + common.FlagsRole: QtCore.Qt.NoItemFlags, + QtCore.Qt.TextAlignmentRole: QtCore.Qt.AlignCenter, + } + + self._data[len(self._data)] = { + QtCore.Qt.DisplayRole: self.show_all_label, + QtCore.Qt.SizeHintRole: self.row_size, + QtCore.Qt.TextAlignmentRole: QtCore.Qt.AlignCenter, + } + + icon = ui.get_icon('sg') + + for task in sorted(getattr(data, self.data_source)): + self._data[len(self._data)] = { + QtCore.Qt.DisplayRole: task, + QtCore.Qt.SizeHintRole: self.row_size, + QtCore.Qt.DecorationRole: icon, + QtCore.Qt.StatusTipRole: task, + QtCore.Qt.AccessibleDescriptionRole: task, + QtCore.Qt.WhatsThisRole: task, + QtCore.Qt.ToolTipRole: task, + QtCore.Qt.TextAlignmentRole: QtCore.Qt.AlignCenter, + } + + +class BaseFilterButton(QtWidgets.QComboBox): + """The combo box used to set a text filter based on the available ShotGrid task names. + + """ + def __init__(self, Model, parent=None): + super().__init__(parent=parent) + self.setView(QtWidgets.QListView(parent=self)) + self.view().setMinimumWidth(int(common.size(common.size_width) * 0.66)) + self.setModel(Model()) + + self.setFixedHeight(common.size(common.size_margin)) + self.setMinimumWidth(common.size(common.size_margin) * 6) + + common.signals.updateTopBarButtons.connect(lambda: self.setHidden(not common.current_tab() == common.AssetTab)) + common.model(common.AssetTab).filterTextChanged.connect(self.select_text) + common.signals.internalDataReady.connect(self.select_text) + + self.textActivated.connect(self.update_filter_text) + + @QtCore.Slot(str) + def update_filter_text(self, text): + """Update the filter text. + + Args: + text (str): The text to set as the filter text. + + """ + if text == self.model().show_all_label: + text = '' + else: + text = f'"{text.lower().strip()}"' + + common.model(common.AssetTab).set_filter_text(text) + + @QtCore.Slot() + def select_text(self, *args, **kwargs): + """Update the filter text. + + """ + self.setCurrentIndex(-1) + + _text = common.model(common.AssetTab).filter_text() + _text = _text.lower().strip() if _text else '' + + if not _text: + return + + for i in range(self.count()): + text = self.itemText(i) + if not text: + continue + if text == self.model().show_all_label: + continue + if text == self.model().section_name_label: + continue + + text = text.lower().strip() + if _text == text: + self.setCurrentIndex(i) + return + + if _text == f'"{text}"': + self.setCurrentIndex(i) + return + + +class TaskFilterModel(BaseFilterModel): + + def __init__(self, parent=None): + super().__init__( + 'ShotGrid Tasks', + 'sg_task_names', + parent=parent + ) + + +class TaskFilterButton(BaseFilterButton): + """The combo box used to set a text filter based on the available ShotGrid task names. + + """ + def __init__(self, parent=None): + super().__init__( + TaskFilterModel, + parent=parent + ) + + +class EntityFilterModel(BaseFilterModel): + + def __init__(self, parent=None): + super().__init__( + 'ShotGrid Entities', + 'shotgun_names', + parent=parent + ) + + +class EntityFilterButton(BaseFilterButton): + """The combo box used to set a text filter based on the available ShotGrid task names. + + """ + def __init__(self, parent=None): + super().__init__( + EntityFilterModel, + parent=parent + ) \ No newline at end of file diff --git a/bookmarks/topbar/topbar.py b/bookmarks/topbar/topbar.py index 94dd42b5..d8943b75 100644 --- a/bookmarks/topbar/topbar.py +++ b/bookmarks/topbar/topbar.py @@ -5,6 +5,7 @@ from . import buttons from . import tabs +from . import sgfilter from .. import common from .. import images @@ -28,13 +29,21 @@ 'hidden': False, }, next(n): { - 'widget': buttons.RefreshButton, + 'widget': sgfilter.EntityFilterButton, + 'hidden': False, + }, + next(n): { + 'widget': sgfilter.TaskFilterButton, 'hidden': False, }, next(n): { 'widget': buttons.FilterButton, 'hidden': False, }, + next(n): { + 'widget': buttons.RefreshButton, + 'hidden': False, + }, next(n): { 'widget': buttons.ToggleSequenceButton, 'hidden': False, diff --git a/bookmarks/ui.py b/bookmarks/ui.py index 8fae55e8..71040c67 100644 --- a/bookmarks/ui.py +++ b/bookmarks/ui.py @@ -1372,6 +1372,7 @@ def __init__( ): super().__init__(parent=parent) + self.anim = None self.scroll_area = None self.columns = columns self._label = label @@ -1524,7 +1525,7 @@ def done(self, r): self.anim.start() self.anim.finished.connect(lambda: super(GalleryWidget, self).done(r)) else: - super(GalleryWidget, self).done(r) + super().done(r) class AbstractListModel(QtCore.QAbstractListModel): @@ -1538,10 +1539,7 @@ def __init__(self, parent=None): super().__init__(parent=parent) self._data = {} - - self.beginResetModel() - self.init_data() - self.endResetModel() + self.reset_data() def rowCount(self, parent=QtCore.QModelIndex()): return len(self._data) @@ -1549,6 +1547,12 @@ def rowCount(self, parent=QtCore.QModelIndex()): def display_name(self, v): return v.replace('/', ' | ') + def reset_data(self): + """Resets the model's data.""" + self.beginResetModel() + self.init_data() + self.endResetModel() + def init_data(self, *args, **kwargs): raise NotImplementedError('Abstract method must be implemented by subclass.') From 6cb35f02446d9c445cc85eac6419a252553813b4 Mon Sep 17 00:00:00 2001 From: Gergely Wootsch Date: Sat, 21 Oct 2023 18:48:08 +0200 Subject: [PATCH 13/32] WIP Commit for adding addittional filters for the files tab Based on the asset tab filters, I realised file items could benefit from pre-populated filters to set file types and subfolders. To be continued... --- bookmarks/common/core.py | 10 + bookmarks/images.py | 2 +- bookmarks/items/delegate.py | 1 + bookmarks/items/file_items.py | 4 +- bookmarks/items/models.py | 2 +- bookmarks/maya/actions.py | 9 - bookmarks/maya/contextmenu.py | 12 - bookmarks/maya/shadertool.py | 805 ------------------- bookmarks/rsc/config.json | 2 + bookmarks/standalone.py | 11 +- bookmarks/threads/workers.py | 17 +- bookmarks/topbar/{sgfilter.py => filters.py} | 83 +- bookmarks/topbar/topbar.py | 6 +- 13 files changed, 112 insertions(+), 852 deletions(-) delete mode 100644 bookmarks/maya/shadertool.py rename bookmarks/topbar/{sgfilter.py => filters.py} (66%) diff --git a/bookmarks/common/core.py b/bookmarks/common/core.py index a97b2aa4..aeaffbfd 100644 --- a/bookmarks/common/core.py +++ b/bookmarks/common/core.py @@ -654,6 +654,7 @@ def __init__(self, *args, **kwargs): self._data_type = None self._shotgun_names = [] self._sg_task_names = [] + self._file_types = [] @property def loaded(self): @@ -709,6 +710,15 @@ def sg_task_names(self): def sg_task_names(self, v): self._sg_task_names = v + @property + def file_types(self): + """Returns a list of file types stored in the data dictionary.""" + return self._file_types + + @file_types.setter + def file_types(self, v): + self._file_types = v + class Timer(QtCore.QTimer): """A custom QTimer class used across the app. diff --git a/bookmarks/images.py b/bookmarks/images.py index 0a55c0fa..641983ed 100644 --- a/bookmarks/images.py +++ b/bookmarks/images.py @@ -546,7 +546,7 @@ def rsc_pixmap( file_info = QtCore.QFileInfo(source) return file_info.absoluteFilePath() - size = size if isinstance(size, (float, int)) else -1 + size = size * common.pixel_ratio if isinstance(size, (float, int)) else -1 _color = color.name() if isinstance(color, QtGui.QColor) else 'null' k = 'rsc:' + name + ':' + str(int(size)) + ':' + _color diff --git a/bookmarks/items/delegate.py b/bookmarks/items/delegate.py index b07d4396..2fa99351 100644 --- a/bookmarks/items/delegate.py +++ b/bookmarks/items/delegate.py @@ -739,6 +739,7 @@ def _create_gradient_pixmap(self): # 3. Create a QPixmap and fill it with transparent color pixmap = QtGui.QPixmap(width, 1) + pixmap.setDevicePixelRatio(common.pixel_ratio) pixmap.fill(QtCore.Qt.transparent) # 4. Render the gradient onto the QPixmap diff --git a/bookmarks/items/file_items.py b/bookmarks/items/file_items.py index 7c894a7b..7de95c8a 100644 --- a/bookmarks/items/file_items.py +++ b/bookmarks/items/file_items.py @@ -457,6 +457,7 @@ def init_data(self): # common.QueueRole: self.queues, common.DataTypeRole: common.SequenceItem, + common.DataDictRole: weakref.ref(sequence_data), common.ItemTabRole: common.FileTab, # common.EntryRole: [], @@ -487,7 +488,8 @@ def init_data(self): sequence_data[sequence_path][common.FramesRole].append(seq.group(2)) sequence_data[sequence_path][common.EntryRole].append(entry) else: - # Copy the existing file item + # The sequence dictionary should contain not only sequence items but single files also, + # so we'll add them here sequence_data[filepath] = common.DataDict(data[idx]) sequence_data[filepath][common.IdRole] = -1 diff --git a/bookmarks/items/models.py b/bookmarks/items/models.py index 1a638089..cc8ee6af 100644 --- a/bookmarks/items/models.py +++ b/bookmarks/items/models.py @@ -287,7 +287,7 @@ def supportedDropActions(self): return QtCore.Qt.MoveAction | QtCore.Qt.CopyAction def supportedDragActions(self): - return QtCore.Qt.MoveAction + return QtCore.Qt.CopyAction def mimeData(self, indexes): """Returns the drag mime data for the given indexes. diff --git a/bookmarks/maya/actions.py b/bookmarks/maya/actions.py index 059bd866..ab7bc782 100644 --- a/bookmarks/maya/actions.py +++ b/bookmarks/maya/actions.py @@ -700,15 +700,6 @@ def import_camera_preset(): cmds.file(path, i=True, defaultNamespace=True, type="mayaAscii") -@QtCore.Slot() -def show_shader_tool(): - """Shows the bundled shader utility tool. - - """ - from . import shadertool - shadertool.show() - - @QtCore.Slot() @common.error @common.debug diff --git a/bookmarks/maya/contextmenu.py b/bookmarks/maya/contextmenu.py index c8b2a7ca..b22a289e 100644 --- a/bookmarks/maya/contextmenu.py +++ b/bookmarks/maya/contextmenu.py @@ -42,7 +42,6 @@ def setup(self): self.export_menu() self.separator() self.import_camera_menu() - self.shader_tool_menu() self.separator() self.viewport_presets_menu() self.capture_menu() @@ -144,16 +143,6 @@ def export_menu(self): 'action': export.show } - def shader_tool_menu(self): - """Shader tool action. - - """ - k = contextmenu.key() - self.menu[k] = { - 'text': 'Show Shader Tool', - 'action': actions.show_shader_tool - } - def import_camera_menu(self): """Import camera template action. @@ -306,7 +295,6 @@ def setup(self): self.export_menu() self.separator() self.import_camera_menu() - self.shader_tool_menu() self.separator() self.viewport_presets_menu() self.capture_menu() diff --git a/bookmarks/maya/shadertool.py b/bookmarks/maya/shadertool.py deleted file mode 100644 index 72dfe08b..00000000 --- a/bookmarks/maya/shadertool.py +++ /dev/null @@ -1,805 +0,0 @@ -"""A Maya utility tool used to work with shaders. - -""" -import functools -import re - -import shiboken2 -from PySide2 import QtWidgets, QtCore, QtGui - -try: - from maya import OpenMayaUI - from maya import cmds - from maya import mel - from maya.api import OpenMaya - from maya.app.general import mayaMixin -except ImportError: - raise ImportError('Could not find the Maya modules.') - -from .. import common - - -def show(): - _ = mel - common.shaders_widget = ShadersWidget() - common.shaders_widget.show(dockable=True) - - -def add_row(parent): - widget = QtWidgets.QWidget(parent=parent) - widget.setSizePolicy( - QtWidgets.QSizePolicy.MinimumExpanding, - QtWidgets.QSizePolicy.Maximum, - ) - widget.setAttribute(QtCore.Qt.WA_TranslucentBackground) - widget.setAttribute(QtCore.Qt.WA_NoSystemBackground) - QtWidgets.QHBoxLayout(widget) - widget.layout().setContentsMargins(0, 0, 0, 0) - parent.layout().addWidget(widget, 0) - widget.layout().setAlignment(QtCore.Qt.AlignCenter) - return widget - - -def icon(name): - return QtGui.QIcon(QtGui.QPixmap(f':/{name}')) - - -@functools.lru_cache(maxsize=1048576) -def elided_text(v): - font = bold_font() - metrics = QtGui.QFontMetrics(font) - text = metrics.elidedText(v, QtCore.Qt.ElideLeft, 250) - return text - - -@functools.lru_cache(maxsize=1024) -def bold_font(): - font = QtGui.QFont() - font.setBold(True) - return font - - -@functools.lru_cache(maxsize=1048576) -def get_attrs(shader): - attrs = {} - for attr in cmds.listAttr(shader): - if '.' in attr: - continue - if cmds.attributeQuery(attr, node=shader, internal=True): - continue - if not cmds.attributeQuery(attr, node=shader, writable=True): - continue - if cmds.attributeQuery(attr, node=shader, listParent=True): - continue - _type = cmds.getAttr(f'{shader}.{attr}', type=True) - if _type not in ('float3', 'float'): - continue - if any(f in attr for f in ('Camera', 'Id', 'Matte', 'Direction')): - continue - attrs[attr] = _type - return attrs - - -class RenameDialog(QtWidgets.QDialog): - renameRequested = QtCore.Signal(str) - - def __init__(self, parent=None): - super().__init__(parent=parent) - QtWidgets.QVBoxLayout(self) - widget = add_row(self) - - self.setAttribute(QtCore.Qt.WA_DeleteOnClose) - self.setWindowTitle('Rename Shader') - self.prefix_editor = QtWidgets.QLineEdit(parent=self) - self.prefix_editor.setValidator( - QtGui.QRegExpValidator(QtCore.QRegExp(r'[a-zA-Z][:a-zA-Z0-9]*')) - ) - self.prefix_editor.setPlaceholderText('Shader prefix...') - self.suffix_editor = QtWidgets.QLineEdit(parent=self) - self.suffix_editor.setValidator( - QtGui.QRegExpValidator(QtCore.QRegExp(r'[a-zA-Z][:a-zA-Z0-9]*')) - ) - self.suffix_editor.setPlaceholderText('Shader suffix...') - widget.layout().addWidget(self.prefix_editor, 0) - widget.layout().addWidget(self.suffix_editor, 0) - - widget = add_row(self) - self.ok_button = QtWidgets.QPushButton('Finish') - self.cancel_button = QtWidgets.QPushButton('Cancel') - widget.layout().addWidget(self.ok_button, 0) - widget.layout().addWidget(self.cancel_button, 0) - - self.ok_button.clicked.connect(self.action) - self.cancel_button.clicked.connect(self.close) - - def action(self): - prefix = self.prefix_editor.text() - suffix = self.suffix_editor.text() - if not all((prefix, suffix)): - OpenMaya.MGlobal.displayWarning(f'Prefix or suffix not set') - return - - shader = f'{prefix}_{suffix}_shader' - if cmds.objExists(shader): - OpenMaya.MGlobal.displayWarning(f'"{shader}" already exist.') - return - - self.renameRequested.emit(f'{prefix}_{suffix}') - self.done(QtWidgets.QDialog.Accepted) - - def sizeHint(self): - """Returns a size hint. - - """ - """Returns a size hint. - - """ - return QtCore.QSize(480, 60) - - -class ShaderModel(QtCore.QAbstractTableModel): - def __init__(self): - super().__init__() - self.internal_data = {} - - self.update_timer = QtCore.QTimer(parent=self) - self.update_timer.setInterval(333) - self.update_timer.setTimerType(QtCore.Qt.CoarseTimer) - self.update_timer.setSingleShot(True) - self.update_timer.timeout.connect(self.init_data) - - def rowCount(self, parent=QtCore.QModelIndex()): - return len(self.internal_data) - - def columnCount(self, parent=QtCore.QModelIndex()): - return 4 - - def flags(self, parent=QtCore.QModelIndex()): - return ( - QtCore.Qt.ItemIsEnabled | - QtCore.Qt.ItemIsSelectable - ) - - def index(self, row, column, parent=QtCore.QModelIndex()): - return self.createIndex(row, column, parent=parent) - - def data(self, index, role=QtCore.Qt.DisplayRole, parent=QtCore.QModelIndex()): - if role == QtCore.Qt.SizeHintRole: - if index.column() == 0: - metrics = QtGui.QFontMetrics(bold_font()) - width = metrics.horizontalAdvance( - self.data(index, role=QtCore.Qt.DisplayRole) - ) + 12 - return QtCore.QSize(width, 18) - else: - metrics = QtGui.QFontMetrics(QtGui.QFont()) - width = metrics.horizontalAdvance( - self.data(index, role=QtCore.Qt.DisplayRole) - ) + 12 - return QtCore.QSize(width, 18) - if role == QtCore.Qt.ToolTipRole: - v = self.internal_data[index.row()]['shapes'] - return '\n'.join(v) - if role == QtCore.Qt.TextAlignmentRole and index.column() != 0: - return QtCore.Qt.AlignCenter - if role == QtCore.Qt.FontRole and index.column() == 0: - return bold_font() - if not index.isValid(): - return None - if index.column() == 0 and role == QtCore.Qt.DisplayRole: - v = str(self.internal_data[index.row()][QtCore.Qt.DisplayRole]) - return elided_text(v) - if index.column() == 1 and role == QtCore.Qt.DisplayRole: - return str(self.internal_data[index.row()]['type']) - if index.column() == 2 and role == QtCore.Qt.DisplayRole: - v = self.internal_data[index.row()]['shapes'] - return f'{len(v)} shapes' if v else '-' - if index.column() == 3 and role == QtCore.Qt.DisplayRole: - v = self.internal_data[index.row()]['connections'] - return f'{len(v)} cnxs' if v else '-' - return None - - def headerData(self, column, orientation, role=QtCore.Qt.DisplayRole): - if role == QtCore.Qt.DisplayRole: - if orientation == QtCore.Qt.Horizontal: - if column == 0: - return 'Shader' - if column == 1: - return 'Type' - if column == 2: - return 'Meshes' - if column == 3: - return 'Connections' - return None - - def init_data(self): - """Initializes data. - - """ - self.beginResetModel() - - arnold_shaders = cmds.listNodeTypes( - 'rendernode/arnold/shader/surface' - ) - surface_shaders = ['standardSurface', ] - if arnold_shaders: - surface_shaders += arnold_shaders - - self.internal_data = {} - for node in cmds.ls(type='shadingEngine'): - for attr in cmds.listAttr(node): - if '.' in attr: - continue - if cmds.attributeQuery(attr, node=node, internal=True): - continue - if not cmds.attributeQuery(attr, node=node, writable=True): - continue - if cmds.attributeQuery(attr, node=node, listParent=True): - continue - _type = cmds.getAttr(f'{node}.{attr}', type=True) - if _type not in ('float3', 'float', 'Tdata'): - continue - if any(f in attr for f in ('Camera', 'Id', 'Matte', 'Direction')): - continue - - if cmds.connectionInfo(f'{node}.{attr}', isDestination=True): - source = cmds.connectionInfo( - f'{node}.{attr}', sourceFromDestination=True - ) - shader = source.split('.')[0] - - if cmds.objectType(shader) not in surface_shaders: - continue - - if not re.match(r'.*[a-zA-Z0-9]+_[a-zA-Z0-9]+_shader$', shader): - continue - - attrs = get_attrs(shader) - cnxs = [ - a for a in attrs if - cmds.connectionInfo(f'{shader}.{a}', id=True) - ] - - item_data = { - QtCore.Qt.DisplayRole: shader, - 'shader': shader, - 'type': cmds.objectType(shader), - 'shadingEngine': node, - 'attributes': attrs, - 'connections': cnxs, - 'shapes': [] - } - - nodes = cmds.sets(node, query=True) - nodes = nodes if nodes else [] - - for _node in nodes: - for n in cmds.ls(_node, long=True): - item_data['shapes'].append(n) - item_data['shapes'] = sorted(item_data['shapes']) - self.internal_data[len(self.internal_data)] = item_data - - self.endResetModel() - - -class ShaderView(QtWidgets.QTableView): - - def __init__(self, parent=None): - super().__init__(parent=parent) - - self.verticalHeader().setHidden(True) - self.verticalHeader().setSectionResizeMode(QtWidgets.QHeaderView.Fixed) - self.verticalHeader().setDefaultSectionSize(18) - self.horizontalHeader().setSectionsMovable(False) - self.setSortingEnabled(False) - self.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection) - self.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows) - self.setAutoFillBackground(True) - self.setModel(ShaderModel()) - - self.callbacks = [] - - self.activated.connect(self.item_activated) - self.selectionModel().selectionChanged.connect(self.selection_changed) - - self.model().modelReset.connect(self.resizeColumnsToContents) - self.model().modelAboutToBeReset.connect( - functools.partial(common.save_selection, self) - ) - self.model().modelReset.connect( - functools.partial(common.restore_selection, self) - ) - - @QtCore.Slot() - def select_shapes(self, *args, **kwargs): - if not self.selectionModel().hasSelection(): - return - index = next(f for f in self.selectionModel().selectedIndexes()) - if not index.isValid(): - return - shader = index.model().internal_data[index.row()]['shader'] - - cmds.hyperShade(objects=shader) - - @QtCore.Slot(QtCore.QModelIndex) - def selection_changed(self, index): - if not self.selectionModel().hasSelection(): - return - index = next(f for f in self.selectionModel().selectedIndexes()) - if not index.isValid(): - return - shader = self.model().internal_data[index.row()]['shader'] - cmds.select(shader, replace=True) - - @QtCore.Slot(QtCore.QModelIndex) - def item_activated(self, index): - data = index.model().internal_data - node = data[index.row()]['shadingEngine'] - shader = data[index.row()]['shader'] - cmds.select(node, replace=True) - cmds.select(shader, add=True) - cmds.HypershadeWindow() - melcmd = \ - 'hyperShadePanelGraphCommand("hyperShadePanel1", "showUpAndDownstream");' - 'evalDeferred -lp "hyperShadeRefreshActiveNode";' - cmd = f"from maya import mel;mel.eval('{melcmd}')" - cmds.evalDeferred(cmd) - - def init_callbacks(self): - if self.callbacks: - return - - cb = OpenMaya.MDGMessage.addNodeAddedCallback( - self.callback, - 'dependNode', - clientData='addNodeAddedCallback' - ) - self.callbacks.append(cb) - cb = OpenMaya.MDGMessage.addNodeRemovedCallback( - self.callback, - 'dependNode', - clientData='addNodeRemovedCallback' - ) - self.callbacks.append(cb) - cb = OpenMaya.MDGMessage.addConnectionCallback( - self.callback, - clientData='addNodeAddedCallback' - ) - self.callbacks.append(cb) - cb = OpenMaya.MDagMessage.addAllDagChangesCallback( - self.callback, - clientData='addAllDagChangesCallback' - ) - # self.callbacks.append(cb) - - def remove_callbacks(self): - for cb in self.callbacks: - OpenMaya.MMessage.removeCallback(cb) - self.callbacks = [] - - def callback(self, *args, clientData=None): - self.model().update_timer.start(self.model().update_timer.interval()) - - -class ShadersWidget(mayaMixin.MayaQWidgetDockableMixin, QtWidgets.QWidget): - def __init__(self): - - ptr = OpenMayaUI.MQtUtil.mainWindow() - if ptr: - parent = shiboken2.wrapInstance(int(ptr), QtWidgets.QMainWindow) - else: - parent = None - super().__init__(parent=parent) - common.set_stylesheet(self) - - self.setWindowTitle('Shader Setup Utility') - self.setWindowFlags( - QtCore.Qt.Dialog - ) - self.add_layer_button = None - self.active_layer_editor = None - self.show_layers_button = None - self.visible_layer_editor = None - self.search_editor = None - self.property_override_row = None - self.shader_override_row = None - self.prefix_editor = None - self.suffix_editor = None - self.type_picker = None - - self._create_ui() - self._connect_signals() - self.init_data() - - def _create_ui(self): - QtWidgets.QVBoxLayout(self) - o = 6 - self.layout().setContentsMargins(o, o, o, o) - - # ===== - group = QtWidgets.QGroupBox(parent=self) - QtWidgets.QVBoxLayout(group) - self.layout().addWidget(group, 0) - - # ===== - widget = add_row(group) - self.prefix_editor = QtWidgets.QLineEdit(parent=self) - self.prefix_editor.setValidator( - QtGui.QRegExpValidator(QtCore.QRegExp(r'[a-zA-Z][:a-zA-Z0-9]*')) - ) - self.prefix_editor.setPlaceholderText('Shader prefix...') - self.suffix_editor = QtWidgets.QLineEdit(parent=self) - self.suffix_editor.setValidator( - QtGui.QRegExpValidator(QtCore.QRegExp(r'[a-zA-Z][:a-zA-Z0-9]*')) - ) - self.suffix_editor.setPlaceholderText('Shader suffix...') - self.type_picker = QtWidgets.QComboBox(parent=self) - self.type_picker.setView(QtWidgets.QListView()) - widget.layout().addWidget(self.prefix_editor, 0) - widget.layout().addWidget(self.suffix_editor, 0) - widget.layout().addWidget(self.type_picker, 0) - - # Row9 - widget = add_row(group) - self.create_shader_button = QtWidgets.QPushButton( - '&Create Shader', parent=self - ) - self.rename_assigned_button = QtWidgets.QPushButton( - 'Rename Assigned', parent=self - ) - self.assign_shader_button = QtWidgets.QPushButton('Assign', parent=self) - widget.layout().addWidget(self.create_shader_button, 1) - widget.layout().addWidget(self.rename_assigned_button, 0) - widget.layout().addWidget(self.assign_shader_button, 0) - - group = QtWidgets.QGroupBox(parent=self) - QtWidgets.QVBoxLayout(group) - self.layout().addWidget(group, 1) - - # ===== - widget = add_row(group) - self.select_button = QtWidgets.QPushButton( - icon('edit'), '&Select Meshes' - ) - self.rename_button = QtWidgets.QPushButton( - icon('textBeam'), 'Re&name' - ) - self.duplicate_button = QtWidgets.QPushButton( - icon('shaderList'), '&Duplicate' - ) - self.refresh_button = QtWidgets.QPushButton( - icon('refresh'), 'Refresh' - ) - - widget.layout().addWidget(self.select_button, 0) - widget.layout().addWidget(self.rename_button, 0) - widget.layout().addWidget(self.duplicate_button, 0) - widget.layout().addWidget(self.refresh_button, 0) - widget.layout().addStretch(1) - - # ===== - self.shader_view = ShaderView(parent=self) - group.layout().addWidget(self.shader_view, 1) - - def _connect_signals(self): - self.refresh_button.clicked.connect( - self.shader_view.model().init_data - ) - self.refresh_button.clicked.connect( - self.init_data - ) - self.select_button.clicked.connect( - self.shader_view.select_shapes - ) - self.create_shader_button.clicked.connect( - self.create_shader_button_clicked - ) - self.duplicate_button.clicked.connect( - self.duplicate_button_clicked - ) - self.shader_view.selectionModel().selectionChanged.connect( - self.selection_changed - ) - self.rename_button.clicked.connect(self.rename_button_clicked) - self.assign_shader_button.clicked.connect(self.assign_shader_button_clicked) - self.rename_assigned_button.clicked.connect( - self.rename_assigned_button_clicked - ) - # Make sure the callbacks are removed - self.destroyed.connect(self.shader_view.remove_callbacks) - - @QtCore.Slot() - def rename_assigned_button_clicked(self): - v = self._selected_prefix_suffix() - if not v: - return - sel = cmds.ls(selection=True) - if not sel: - OpenMaya.MGlobal.displayWarning('Nothing is selected') - self.rename_assigned_nodes(f'{v[0]}_{v[1]}', sel) - - @QtCore.Slot() - def assign_shader_button_clicked(self): - if not self.shader_view.selectionModel().hasSelection(): - OpenMaya.MGlobal.displayWarning('No shader selection') - return - if not cmds.ls(selection=True): - OpenMaya.MGlobal.displayWarning('Nothing is selected') - return - - index = next( - f for f in self.shader_view.selectionModel().selectedIndexes() - ) - if not index.isValid(): - return - shader = index.model().internal_data[index.row()]['shader'] - cmds.hyperShade(assign=shader) - - @QtCore.Slot() - def rename_button_clicked(self): - v = self._selected_prefix_suffix() - if not v: - return - w = RenameDialog(parent=self) - w.prefix_editor.setText(v[0]) - w.suffix_editor.setText(v[1]) - old_name = f'{v[0]}_{v[1]}' - w.renameRequested.connect(functools.partial(self.rename, old_name)) - w.open() - - @QtCore.Slot(str) - @QtCore.Slot(str) - def rename(self, old_name, new_name): - if old_name == new_name: - OpenMaya.MGlobal.displayWarning(f'Old and new names are the same.') - return - - for node in cmds.ls(long=True): - if old_name not in node: - continue - - try: - _node = node.replace(old_name, new_name) - cmds.rename(node, _node) - OpenMaya.MGlobal.displayInfo(f'Renamed {node} > {_node}') - except: - OpenMaya.MGlobal.displayWarning(f'Could not rename {node}') - - self.shader_view.model().init_data() - - @QtCore.Slot(QtCore.QItemSelection) - def selection_changed(self, selection): - v = self._selected_prefix_suffix() - if not v: - return - index = next(f for f in self.shader_view.selectionModel().selectedIndexes()) - _type = index.model().internal_data[index.row()]['type'] - self.type_picker.setCurrentText(_type) - self.prefix_editor.setText(v[0]) - self.suffix_editor.setText(v[1]) - - def _selected_prefix_suffix(self): - if not self.shader_view.selectionModel().hasSelection(): - return None - index = next(f for f in self.shader_view.selectionModel().selectedIndexes()) - if not index.isValid(): - return None - name = index.data(role=QtCore.Qt.DisplayRole) - match = re.match( - r'([a-zA-Z0-9:]+?)_([a-zA-Z0-9]+?)_.*', - name - ) - if not match: - return None - return match.group(1), match.group(2) - - @QtCore.Slot() - def duplicate_button_clicked(self): - v = self._selected_prefix_suffix() - if not v: - return - w = RenameDialog(parent=self) - w.prefix_editor.setText(v[0]) - w.suffix_editor.setText(v[1]) - old_name = f'{v[0]}_{v[1]}' - w.renameRequested.connect(functools.partial(self.duplicate, old_name)) - w.open() - - @QtCore.Slot(str) - @QtCore.Slot(str) - def duplicate(self, old_name, new_name): - index = next(f for f in self.shader_view.selectionModel().selectedIndexes()) - if not index.isValid(): - return - sg = index.model().internal_data[index.row()]['shadingEngine'] - nodes = cmds.duplicate(sg, renameChildren=True, upstreamNodes=True) - - for node in nodes: - node = cmds.ls(node, long=True)[0] - if old_name not in node: - continue - try: - _node = node.replace(old_name, new_name) - _node = re.sub(r'[0-9]+$', '', _node) - if cmds.objExists(_node): - _node += 'Meshes' - cmds.rename(node, _node) - OpenMaya.MGlobal.displayInfo(f'Renamed {node} > {_node}') - except: - OpenMaya.MGlobal.displayWarning(f'Could not rename {node}') - - self.shader_view.model().init_data() - - @QtCore.Slot() - def init_data(self): - """Initializes data. - - """ - arnold_shaders = cmds.listNodeTypes( - 'rendernode/arnold/shader/surface' - ) - surface_shaders = ['standardSurface', ] - if arnold_shaders: - surface_shaders += arnold_shaders - self.type_picker.clear() - for item in surface_shaders: - self.type_picker.addItem(item) - - _types = cmds.listNodeTypes('rendernode/arnold/shader/volume') - _types = _types if _types else [] - for item in _types: - self.type_picker.addItem(item) - - @QtCore.Slot() - def create_shader_button_clicked(self): - sel = cmds.ls(selection=True) - sel = sel if sel else [] - rel = cmds.listRelatives( - sel, allDescendents=True, type='mesh', - path=True - ) - rel = rel if rel else [] - - prefix = self.prefix_editor.text() - suffix = self.suffix_editor.text() - - if not all((prefix, suffix)): - OpenMaya.MGlobal.displayWarning(f'Prefix or suffix not set') - return - - _type = self.type_picker.currentText() - if not _type: - return - - shader = f'{prefix}_{suffix}_shader' - if cmds.objExists(shader): - OpenMaya.MGlobal.displayWarning(f'"{shader}" already exist.') - return - cmds.shadingNode( - _type, - asShader=True, name=shader - ) - - material = f'{prefix}_{suffix}_material' - if cmds.objExists(material): - OpenMaya.MGlobal.displayWarning(f'"{material}" already exist.') - cmds.delete(shader) - return - cmds.sets( - name=material, - renderable=True, - noSurfaceShader=True, - empty=True - ) - cmds.connectAttr( - f'{shader}.outColor', - f'{material}.surfaceShader', - force=True - ) - - displacement = f'{prefix}_{suffix}_displacement' - if cmds.objExists(displacement): - OpenMaya.MGlobal.displayWarning(f'"{displacement}" already exist.') - else: - cmds.shadingNode( - 'displacementShader', - asShader=True, - name=displacement - ) - cmds.connectAttr( - f'{displacement}.displacement', - f'{material}.displacementShader', - force=True - ) - - # Rename selected elements - cmds.select(rel + sel, replace=True) - if rel + sel: - cmds.hyperShade(assign=shader) - if sel: - self.rename_assigned_nodes(f'{prefix}_{suffix}', sel) - - def rename_assigned_nodes(self, name, sel): - SUFFIXES = { - 'transform': '_t', - 'mesh': 'Shape', - 'nurbsSurface': 'Shape', - 'nurbsCurve': 'Crv', - 'bezierCurve': 'Crv', - 'locator': 'Loc', - } - - def _filter(lst): - """ Returns a filtered list accepting only the specified object types. - The resulting list is reverse sorted, to avoid missing object names when - renaming.""" - - lst = set(lst) - if not lst: - return [] - arr = [] - for i in lst: - for typ in SUFFIXES: - if cmds.objectType(i) == typ: - arr.append(i) - - arr.sort(key=lambda x: x.count('|')) - return arr[::-1] # reverse list - - _shapes = ['mesh', 'nurbsSurface'] - _f = [f for f in sel if cmds.objectType(f) == 'transform'] - f_ = [cmds.listRelatives(f, parent=True)[0] for f in sel if - cmds.objectType(f) in _shapes] - sel = _filter(_f + f_) - sel = sorted(sel) if sel else [] - - if not sel: - return [] - - if not name: - return [] - - for node in sel: - if name in node: - continue - - suffix = [SUFFIXES[f] for f in SUFFIXES if f == cmds.objectType(node)] - if not suffix: - continue - - suffix = suffix[0] - cmds.rename( - node, - f'{name}{suffix}#', - ignoreShape=False - ) - - def paintEvent(self, event): - """Event handler. - - """ - painter = QtGui.QPainter() - painter.begin(self) - painter.setPen(QtCore.Qt.NoPen) - painter.setBrush(common.color(common.color_background)) - painter.drawRect(self.rect()) - painter.end() - - def showEvent(self, event): - """Event handler. - - """ - super().showEvent(event) - self.shader_view.model().init_data() - self.shader_view.init_callbacks() - - def hideEvent(self, event): - """Event handler. - - """ - super().hideEvent(event) - self.shader_view.remove_callbacks() - - def closeEvent(self, event): - super().closeEvent(event) - self.shader_view.remove_callbacks() diff --git a/bookmarks/rsc/config.json b/bookmarks/rsc/config.json index 50d0e480..eb012955 100644 --- a/bookmarks/rsc/config.json +++ b/bookmarks/rsc/config.json @@ -3,6 +3,8 @@ "github_url": "https://github.com/wgergely/bookmarks", "env_key": "BOOKMARKS_ROOT", "product": "bookmarks", + "organization": "bookmarks", + "organization_domain": "bookmarks-vfx.com", "link_file": ".links", "bookmark_cache_dir": ".bookmark", "bookmark_database": "bookmark.db", diff --git a/bookmarks/standalone.py b/bookmarks/standalone.py index 8a435858..10dac871 100644 --- a/bookmarks/standalone.py +++ b/bookmarks/standalone.py @@ -82,7 +82,7 @@ def _set_application_properties(app=None): app.setAttribute(QtCore.Qt.AA_UseHighDpiPixmaps, True) return - QtWidgets.QApplication.setAttribute(QtCore.Qt.AA_UseOpenGLES, True) + QtWidgets.QApplication.setAttribute(QtCore.Qt.AA_UseOpenGLES, False) QtWidgets.QApplication.setAttribute( QtCore.Qt.AA_EnableHighDpiScaling, True ) @@ -402,11 +402,16 @@ class BookmarksApp(QtWidgets.QApplication): def __init__(self, args): _set_application_properties() - super().__init__([__file__, '-platform', 'windows:dpiawareness=2']) + + super().__init__([__file__,]) _set_application_properties(app=self) self.setApplicationVersion(__version__) + self.setApplicationName(common.product.title()) - self.setAttribute(QtCore.Qt.AA_UseHighDpiPixmaps, bool=True) + self.setOrganizationName(common.organization) + self.setOrganizationDomain(common.organization_domain) + + self.setAttribute(QtCore.Qt.AA_UseHighDpiPixmaps, True) self._set_model_id() self._set_window_icon() diff --git a/bookmarks/threads/workers.py b/bookmarks/threads/workers.py index d330fc97..9fa41c04 100644 --- a/bookmarks/threads/workers.py +++ b/bookmarks/threads/workers.py @@ -613,14 +613,23 @@ def _process_data(self, ref): # TODO: This does not seem to be thread safe (?) but since we only have one asset worker # it should be fine for now. if _ref(): - if asset_row_data['sg_task_name'] and asset_row_data['sg_task_name'].lower() not in _ref().sg_task_names: + if asset_row_data['sg_task_name'] and asset_row_data['sg_task_name'] not in _ref().sg_task_names: if _ref(): - _ref().sg_task_names.append(asset_row_data['sg_task_name'].lower()) + _ref().sg_task_names.append(asset_row_data['sg_task_name']) if _ref(): - if asset_row_data['shotgun_name'] and asset_row_data['shotgun_name'].lower() not in _ref().shotgun_names: + if asset_row_data['shotgun_name'] and asset_row_data['shotgun_name'] not in _ref().shotgun_names: if _ref(): - _ref().shotgun_names.append(asset_row_data['shotgun_name'].lower()) + _ref().shotgun_names.append(asset_row_data['shotgun_name']) + + # File type filters + if len(pp) == 4: + if ref(): + file_type = ref()[common.PathRole].split('.')[-1] + _ref = ref()[common.DataDictRole] + if _ref() and file_type.lower() not in _ref().file_types: + _ref().file_types.append(file_type.lower()) + # ShotGrid status if len(pp) <= 4: diff --git a/bookmarks/topbar/sgfilter.py b/bookmarks/topbar/filters.py similarity index 66% rename from bookmarks/topbar/sgfilter.py rename to bookmarks/topbar/filters.py index 57a71726..b502c12f 100644 --- a/bookmarks/topbar/sgfilter.py +++ b/bookmarks/topbar/filters.py @@ -10,7 +10,8 @@ class BaseFilterModel(ui.AbstractListModel): - def __init__(self, section_name_label, data_source, parent=None): + def __init__(self, section_name_label, data_source, tab_index, parent=None): + self.tab_index = tab_index self.show_all_label = ' - Show All -' self.section_name_label = section_name_label @@ -21,12 +22,37 @@ def __init__(self, section_name_label, data_source, parent=None): common.signals.internalDataReady.connect(self.internal_data_ready) common.signals.bookmarksChanged.connect(self.reset_data) + def data(self, index, role=QtCore.Qt.DisplayRole): + if role == QtCore.Qt.DecorationRole: + _text = common.model(self.tab_index).filter_text() + _text = _text.lower().strip() if _text else '' + + if not _text: + return super().data(index, role) + + text = super().data(index, QtCore.Qt.DisplayRole) + if not text: + return super().data(index, role) + if text == self.show_all_label: + return super().data(index, role) + if text == self.section_name_label: + return super().data(index, role) + + text = text.lower().strip() + if _text == text: + return ui.get_icon('check', color=common.color(common.color_green)) + + if _text == f'"{text}"': + return ui.get_icon('check', color=common.color(common.color_green)) + + return super().data(index, role) + @QtCore.Slot(weakref.ref) def internal_data_ready(self, ref): if not ref(): return - source_model = common.source_model(common.AssetTab) + source_model = common.source_model(self.tab_index) data = common.get_data( source_model.source_path(), source_model.task(), @@ -49,7 +75,7 @@ def init_data(self, *args, **kwargs): """ self._data = common.DataDict() - source_model = common.source_model(common.AssetTab) + source_model = common.source_model(self.tab_index) data = common.get_data( source_model.source_path(), @@ -68,6 +94,7 @@ def init_data(self, *args, **kwargs): self._data[len(self._data)] = { QtCore.Qt.DisplayRole: self.show_all_label, + QtCore.Qt.DecorationRole: ui.get_icon('archivedVisible', color=common.color(common.color_green)), QtCore.Qt.SizeHintRole: self.row_size, QtCore.Qt.TextAlignmentRole: QtCore.Qt.AlignCenter, } @@ -91,17 +118,19 @@ class BaseFilterButton(QtWidgets.QComboBox): """The combo box used to set a text filter based on the available ShotGrid task names. """ - def __init__(self, Model, parent=None): + def __init__(self, Model, tab_index, parent=None): super().__init__(parent=parent) + + self.tab_index = tab_index self.setView(QtWidgets.QListView(parent=self)) - self.view().setMinimumWidth(int(common.size(common.size_width) * 0.66)) self.setModel(Model()) + self.setSizeAdjustPolicy(QtWidgets.QComboBox.AdjustToContents) self.setFixedHeight(common.size(common.size_margin)) - self.setMinimumWidth(common.size(common.size_margin) * 6) + self.setMinimumWidth(common.size(common.size_margin)) - common.signals.updateTopBarButtons.connect(lambda: self.setHidden(not common.current_tab() == common.AssetTab)) - common.model(common.AssetTab).filterTextChanged.connect(self.select_text) + common.signals.updateTopBarButtons.connect(lambda: self.setHidden(not common.current_tab() == self.tab_index)) + common.model(self.tab_index).filterTextChanged.connect(self.select_text) common.signals.internalDataReady.connect(self.select_text) self.textActivated.connect(self.update_filter_text) @@ -119,16 +148,16 @@ def update_filter_text(self, text): else: text = f'"{text.lower().strip()}"' - common.model(common.AssetTab).set_filter_text(text) + common.model(self.tab_index).set_filter_text(text) @QtCore.Slot() def select_text(self, *args, **kwargs): """Update the filter text. """ - self.setCurrentIndex(-1) + self.setCurrentIndex(0) - _text = common.model(common.AssetTab).filter_text() + _text = common.model(self.tab_index).filter_text() _text = _text.lower().strip() if _text else '' if not _text: @@ -157,8 +186,9 @@ class TaskFilterModel(BaseFilterModel): def __init__(self, parent=None): super().__init__( - 'ShotGrid Tasks', + 'Tasks', 'sg_task_names', + common.AssetTab, parent=parent ) @@ -170,6 +200,7 @@ class TaskFilterButton(BaseFilterButton): def __init__(self, parent=None): super().__init__( TaskFilterModel, + common.AssetTab, parent=parent ) @@ -178,8 +209,9 @@ class EntityFilterModel(BaseFilterModel): def __init__(self, parent=None): super().__init__( - 'ShotGrid Entities', + 'Assets', 'shotgun_names', + common.AssetTab, parent=parent ) @@ -191,5 +223,30 @@ class EntityFilterButton(BaseFilterButton): def __init__(self, parent=None): super().__init__( EntityFilterModel, + common.AssetTab, + parent=parent + ) + + + +class TypeFilterModel(BaseFilterModel): + + def __init__(self, parent=None): + super().__init__( + 'File Types', + 'file_types', + common.FileTab, + parent=parent + ) + + +class TypeFilterButton(BaseFilterButton): + """The combo box used to set a text filter based on the available file types + + """ + def __init__(self, parent=None): + super().__init__( + TypeFilterModel, + common.FileTab, parent=parent ) \ No newline at end of file diff --git a/bookmarks/topbar/topbar.py b/bookmarks/topbar/topbar.py index d8943b75..92074fda 100644 --- a/bookmarks/topbar/topbar.py +++ b/bookmarks/topbar/topbar.py @@ -5,7 +5,7 @@ from . import buttons from . import tabs -from . import sgfilter +from . import filters from .. import common from .. import images @@ -29,11 +29,11 @@ 'hidden': False, }, next(n): { - 'widget': sgfilter.EntityFilterButton, + 'widget': filters.EntityFilterButton, 'hidden': False, }, next(n): { - 'widget': sgfilter.TaskFilterButton, + 'widget': filters.TaskFilterButton, 'hidden': False, }, next(n): { From b5824233ef45449f3a90e6d9d08191d09596d6b3 Mon Sep 17 00:00:00 2001 From: Gergely Wootsch Date: Sun, 22 Oct 2023 14:17:27 +0200 Subject: [PATCH 14/32] Add file type filter button to the file tab --- bookmarks/common/data.py | 1 + bookmarks/items/favourite_items.py | 2 + bookmarks/items/file_items.py | 15 +++++-- bookmarks/items/models.py | 64 ++++++++++++++---------------- bookmarks/standalone.py | 2 +- bookmarks/threads/workers.py | 16 +++----- bookmarks/topbar/filters.py | 10 ++++- bookmarks/topbar/topbar.py | 4 ++ 8 files changed, 65 insertions(+), 49 deletions(-) diff --git a/bookmarks/common/data.py b/bookmarks/common/data.py index be3b6419..03127ebf 100644 --- a/bookmarks/common/data.py +++ b/bookmarks/common/data.py @@ -48,6 +48,7 @@ def sort_key(_idx): d.refresh_needed = ref().refresh_needed d.shotgun_names = ref().shotgun_names d.sg_task_names = ref().sg_task_names + d.file_types = ref().file_types for n, idx in enumerate(sorted_idxs): if not ref(): diff --git a/bookmarks/items/favourite_items.py b/bookmarks/items/favourite_items.py index 4164fd04..aad9b6d5 100644 --- a/bookmarks/items/favourite_items.py +++ b/bookmarks/items/favourite_items.py @@ -184,6 +184,7 @@ def init_data(self): # common.QueueRole: self.queues, common.DataTypeRole: common.SequenceItem, + common.DataDictRole: None, common.ItemTabRole: common.FavouriteTab, # common.EntryRole: [], @@ -253,6 +254,7 @@ def init_data(self): v[common.DataTypeRole] = common.FileItem data[idx] = v + data[idx][common.DataDictRole] = weakref.ref(data) data[idx][common.IdRole] = idx def source_path(self): diff --git a/bookmarks/items/file_items.py b/bookmarks/items/file_items.py index 7de95c8a..10c606e3 100644 --- a/bookmarks/items/file_items.py +++ b/bookmarks/items/file_items.py @@ -152,7 +152,7 @@ def get_sequence_elements(filepath): sequence_path = None if seq: - sequence_path = f'{seq.group(1)}{common.SEQPROXY}{seq.group(3)}.{seq.group(4)}' + sequence_path = seq.group(1) + common.SEQPROXY + seq.group(3) + '.' + seq.group(4) return seq, sequence_path @@ -301,6 +301,8 @@ def init_data(self): return _dirs = [] + _extensions = [] + data = common.get_data(p, k, t) sequence_data = common.DataDict() # temporary dict for temp data @@ -356,6 +358,7 @@ def init_data(self): _source_path ) _dirs.append(_dir) + _extensions.append(ext) # We'll check against the current file extension against the allowed # extensions. If the task folder is not defined in the token config, @@ -457,7 +460,7 @@ def init_data(self): # common.QueueRole: self.queues, common.DataTypeRole: common.SequenceItem, - common.DataDictRole: weakref.ref(sequence_data), + common.DataDictRole: None, common.ItemTabRole: common.FileTab, # common.EntryRole: [], @@ -490,7 +493,6 @@ def init_data(self): else: # The sequence dictionary should contain not only sequence items but single files also, # so we'll add them here - sequence_data[filepath] = common.DataDict(data[idx]) sequence_data[filepath][common.IdRole] = -1 # Cast the sequence data back onto the model @@ -528,11 +530,18 @@ def init_data(self): v[common.DataTypeRole] = common.FileItem data[idx] = v + data[idx][common.DataDictRole] = weakref.ref(data) data[idx][common.IdRole] = idx watcher = common.get_watcher(common.FileTab) watcher.reset() watcher.add_directories(list(set(_dirs))) + + # Add the list of file extensions to the model's data + _extensions = sorted(set([('.' + f) for f in _extensions])) + common.get_data(p, k, common.FileItem).file_types = _extensions + common.get_data(p, k, common.SequenceItem).file_types = _extensions + self.set_refresh_needed(False) def disable_filter(self): diff --git a/bookmarks/items/models.py b/bookmarks/items/models.py index cc8ee6af..1bd95d9a 100644 --- a/bookmarks/items/models.py +++ b/bookmarks/items/models.py @@ -38,7 +38,8 @@ from .. import importexport from .. import log -MAX_HISTORY = 20 +MAX_HISTORY = 15 + DEFAULT_ITEM_FLAGS = ( QtCore.Qt.ItemNeverHasChildren | QtCore.Qt.ItemIsEnabled | @@ -52,8 +53,15 @@ DEFAULT_SORT_BY_NAME_ROLE = [str()] * 8 +# Filter expressions used by the filter proxy model +re_negative = re.compile(r'--([^\"\'\[\]\*\s]+)', flags=re.IGNORECASE | re.MULTILINE) +re_quoted_negative = re.compile(r'--"(.*?)"', flags=re.IGNORECASE | re.MULTILINE) +re_positive = re.compile(r'(--[^\"\'\[\]*\s]+)', flags=re.IGNORECASE | re.MULTILINE) +re_quoted_positive = re.compile(r'(--".*?")', flags=re.IGNORECASE | re.MULTILINE) + + def initdata(func): - """The decorator is responsible validating the current active paths, and emitting + """Decorator used by init_data to validate the active items, and emitting the ``beginResetModel``, ``endResetModel`` and :attr:`.ItemModel.coreDataLoaded` signals. @@ -66,11 +74,11 @@ def func_wrapper(self, *args, **kwargs): """ common.settings.load_active_values() - self.beginResetModel() self._interrupt_requested = False self._load_in_progress = True try: + self.beginResetModel() func(self, *args, **kwargs) except: raise @@ -79,7 +87,6 @@ def func_wrapper(self, *args, **kwargs): self._load_in_progress = False self.endResetModel() - # Emit references to the just loaded core data p = self.source_path() k = self.task() t1 = self.data_type() @@ -103,20 +110,10 @@ def filter_includes_row(filter_text, searchable): """ _filter_text = filter_text - it = re.finditer( - r'(--[^\"\'\[\]\*\s]+)', - filter_text, - flags=re.IGNORECASE | re.MULTILINE - ) - it_quoted = re.finditer( - r'(--".*?")', - filter_text, - flags=re.IGNORECASE | re.MULTILINE - ) - - for match in it: + + for match in re_positive.finditer(filter_text): _filter_text = re.sub(match.group(1), '', _filter_text) - for match in it_quoted: + for match in re_quoted_positive.finditer(filter_text): _filter_text = re.sub(match.group(1), '', _filter_text) for text in _filter_text.split(): @@ -127,23 +124,22 @@ def filter_includes_row(filter_text, searchable): @functools.lru_cache(maxsize=4194304) -def _filter_excludes_row(filter_text, searchable): - it = re.finditer( - r'--([^\"\'\[\]\*\s]+)', - filter_text, - flags=re.IGNORECASE | re.MULTILINE - ) - it_quoted = re.finditer( - r'--"(.*?)"', - filter_text, - flags=re.IGNORECASE | re.MULTILINE - ) - - for match in it: - if match.group(1).lower() in searchable: +def filter_excludes_row(filter_text, searchable_text): + """Checks whether the given filter text matches the given searchable text. + + Args: + filter_text (str): The filter text. + searchable_text (str): The searchable text. + + Returns: + bool: True if the filter text matches the searchable text, False otherwise. + + """ + for match in re_negative.finditer(filter_text): + if match.group(1).lower() in searchable_text: return True - for match in it_quoted: - if match.group(1).lower() in searchable: + for match in re_quoted_negative.finditer(filter_text): + if match.group(1).lower() in searchable_text: return True return False @@ -1053,7 +1049,7 @@ def filterAcceptsRow(self, idx, parent=None): ) if not filter_includes_row(filter_text, searchable): return False - if _filter_excludes_row(filter_text, searchable): + if filter_excludes_row(filter_text, searchable): return False if self.filter_flag(common.MarkedAsActive) and active: diff --git a/bookmarks/standalone.py b/bookmarks/standalone.py index 10dac871..1418c8ff 100644 --- a/bookmarks/standalone.py +++ b/bookmarks/standalone.py @@ -82,7 +82,7 @@ def _set_application_properties(app=None): app.setAttribute(QtCore.Qt.AA_UseHighDpiPixmaps, True) return - QtWidgets.QApplication.setAttribute(QtCore.Qt.AA_UseOpenGLES, False) + QtWidgets.QApplication.setAttribute(QtCore.Qt.AA_UseOpenGLES, True) QtWidgets.QApplication.setAttribute( QtCore.Qt.AA_EnableHighDpiScaling, True ) diff --git a/bookmarks/threads/workers.py b/bookmarks/threads/workers.py index 9fa41c04..de2583f1 100644 --- a/bookmarks/threads/workers.py +++ b/bookmarks/threads/workers.py @@ -606,7 +606,12 @@ def _process_data(self, ref): ) # Asset ShotGrid task to list - if len(pp) == 4 and asset_row_data['sg_task_name'] and ref()[common.DataDictRole]: + if ( + len(pp) == 4 and + asset_row_data['sg_task_name'] and + ref()[common.DataDictRole] and + not asset_row_data['flags'] & common.MarkedAsArchived + ): if ref(): _ref = ref()[common.DataDictRole] @@ -622,15 +627,6 @@ def _process_data(self, ref): if _ref(): _ref().shotgun_names.append(asset_row_data['shotgun_name']) - # File type filters - if len(pp) == 4: - if ref(): - file_type = ref()[common.PathRole].split('.')[-1] - _ref = ref()[common.DataDictRole] - if _ref() and file_type.lower() not in _ref().file_types: - _ref().file_types.append(file_type.lower()) - - # ShotGrid status if len(pp) <= 4: update_shotgun_configured(pp, bookmark_row_data, asset_row_data, ref) diff --git a/bookmarks/topbar/filters.py b/bookmarks/topbar/filters.py index b502c12f..d1d359f5 100644 --- a/bookmarks/topbar/filters.py +++ b/bookmarks/topbar/filters.py @@ -3,8 +3,8 @@ from PySide2 import QtCore, QtGui, QtWidgets - from .. import common +from .. import log from .. import ui @@ -21,6 +21,7 @@ def __init__(self, section_name_label, data_source, tab_index, parent=None): common.signals.internalDataReady.connect(self.internal_data_ready) common.signals.bookmarksChanged.connect(self.reset_data) + common.signals.bookmarkActivated.connect(self.reset_data) def data(self, index, role=QtCore.Qt.DisplayRole): if role == QtCore.Qt.DecorationRole: @@ -82,6 +83,10 @@ def init_data(self, *args, **kwargs): source_model.task(), source_model.data_type() ) + + if not hasattr(data, self.data_source): + log.error(f'No {self.data_source} found in data!') + return if not getattr(data, self.data_source): return @@ -239,6 +244,9 @@ def __init__(self, parent=None): parent=parent ) + common.signals.assetActivated.connect(self.reset_data) + common.signals.taskFolderChanged.connect(self.reset_data) + class TypeFilterButton(BaseFilterButton): """The combo box used to set a text filter based on the available file types diff --git a/bookmarks/topbar/topbar.py b/bookmarks/topbar/topbar.py index 92074fda..c5fbfcb9 100644 --- a/bookmarks/topbar/topbar.py +++ b/bookmarks/topbar/topbar.py @@ -36,6 +36,10 @@ 'widget': filters.TaskFilterButton, 'hidden': False, }, + next(n): { + 'widget': filters.TypeFilterButton, + 'hidden': False, + }, next(n): { 'widget': buttons.FilterButton, 'hidden': False, From f60f3c47822a0e73843bc9de7e79be9da3e212a0 Mon Sep 17 00:00:00 2001 From: Gergely Wootsch Date: Sun, 22 Oct 2023 21:26:59 +0200 Subject: [PATCH 15/32] Fix filter related clunks and bugs This commit issues some regressions and bugs and general functionality tweaks related to item filtering. Notably - I re-enabled the TaskModel's thread worker and added item filtering for empty items. --- bookmarks/actions.py | 2 +- bookmarks/common/core.py | 40 +++++ bookmarks/common/data.py | 4 + bookmarks/database.py | 2 +- bookmarks/items/bookmark_items.py | 12 ++ bookmarks/items/delegate.py | 13 +- bookmarks/items/favourite_items.py | 6 +- bookmarks/items/file_items.py | 23 ++- bookmarks/items/models.py | 4 + bookmarks/items/task_items.py | 85 +++++------ bookmarks/items/views.py | 71 +++++---- bookmarks/scripts/clips.py | 6 +- bookmarks/threads/threads.py | 13 +- bookmarks/threads/workers.py | 63 +++++++- bookmarks/topbar/filters.py | 237 ++++++++++++++++++++++++----- bookmarks/topbar/topbar.py | 16 ++ 16 files changed, 449 insertions(+), 148 deletions(-) diff --git a/bookmarks/actions.py b/bookmarks/actions.py index f8d6d5ec..07a123e1 100644 --- a/bookmarks/actions.py +++ b/bookmarks/actions.py @@ -889,7 +889,7 @@ def refresh(idx=None): if common.current_tab() == common.AssetTab: cache = f'{common.active("root", path=True)}/{common.bookmark_cache_dir}/assets.cache' if os.path.exists(cache): - print('Removing asset cache:', cache) + log.debug('Removing asset cache:', cache) os.remove(cache) model.reset_data(force=True) diff --git a/bookmarks/common/core.py b/bookmarks/common/core.py index aeaffbfd..ba50837a 100644 --- a/bookmarks/common/core.py +++ b/bookmarks/common/core.py @@ -655,6 +655,10 @@ def __init__(self, *args, **kwargs): self._shotgun_names = [] self._sg_task_names = [] self._file_types = [] + self._subdirectories = [] + self._servers = [] + self._jobs = [] + self._roots = [] @property def loaded(self): @@ -719,6 +723,42 @@ def file_types(self): def file_types(self, v): self._file_types = v + @property + def subdirectories(self): + """Returns a list of file types stored in the data dictionary.""" + return self._subdirectories + + @subdirectories.setter + def subdirectories(self, v): + self._subdirectories = v + + @property + def servers(self): + """Returns a list of file types stored in the data dictionary.""" + return self._servers + + @servers.setter + def servers(self, v): + self._servers = v + + @property + def jobs(self): + """Returns a list of file types stored in the data dictionary.""" + return self._jobs + + @jobs.setter + def jobs(self, v): + self._jobs = v + + @property + def roots(self): + """Returns a list of file types stored in the data dictionary.""" + return self._roots + + @roots.setter + def roots(self, v): + self._roots = v + class Timer(QtCore.QTimer): """A custom QTimer class used across the app. diff --git a/bookmarks/common/data.py b/bookmarks/common/data.py index 03127ebf..669a43f6 100644 --- a/bookmarks/common/data.py +++ b/bookmarks/common/data.py @@ -49,6 +49,10 @@ def sort_key(_idx): d.shotgun_names = ref().shotgun_names d.sg_task_names = ref().sg_task_names d.file_types = ref().file_types + d.subdirectories = ref().subdirectories + d.servers = ref().servers + d.jobs = ref().jobs + d.roots = ref().roots for n, idx in enumerate(sorted_idxs): if not ref(): diff --git a/bookmarks/database.py b/bookmarks/database.py index c2f35ac7..64db227d 100644 --- a/bookmarks/database.py +++ b/bookmarks/database.py @@ -749,7 +749,7 @@ def value(self, source, key, table): table (str, optional): Optional table parameter, defaults to `AssetTable`. Returns: - The value stored in the database, or None. + object: The value stored in the database, or None. """ if not self.is_valid(): diff --git a/bookmarks/items/bookmark_items.py b/bookmarks/items/bookmark_items.py index aeb71d50..e6255743 100644 --- a/bookmarks/items/bookmark_items.py +++ b/bookmarks/items/bookmark_items.py @@ -149,6 +149,10 @@ def init_data(self): data = common.get_data(p, _k, t) database.remove_all_connections() + _servers = [] + _jobs = [] + _roots = [] + for k, v in self.item_generator(): common.check_type(v, dict) @@ -161,6 +165,10 @@ def init_data(self): job = v['job'] root = v['root'] + _servers.append(server) + _jobs.append(job) + _roots.append(root) + # Get the display name based on the value set in the database db = database.get(server, job, root) @@ -268,6 +276,10 @@ def init_data(self): if not exists: continue + data.servers = sorted(set(_servers)) + data.jobs = sorted(set(_jobs)) + data.roots = sorted(set(_roots)) + self.activeChanged.emit(self.active_index()) def item_generator(self): diff --git a/bookmarks/items/delegate.py b/bookmarks/items/delegate.py index 2fa99351..459c9868 100644 --- a/bookmarks/items/delegate.py +++ b/bookmarks/items/delegate.py @@ -1013,6 +1013,7 @@ def draw_subdir_rectangles(self, bg_rect, *args): ) filter_text = self.parent().model().filter_text() + filter_text = filter_text.lower().strip('\\/-_\'" ') if filter_text else '' # Paint the background rectangle of the sub-folder modifiers = QtWidgets.QApplication.instance().keyboardModifiers() @@ -1045,8 +1046,7 @@ def draw_subdir_rectangles(self, bg_rect, *args): color = common.color(common.color_dark_background) # Green the sub-folder is set as a text filter - ftext = f'"{text}"' - if filter_text and ftext.lower() in filter_text.lower(): + if text.lower() in filter_text.lower(): color = common.color(common.color_green) if rect.contains(cursor_position): @@ -1198,9 +1198,10 @@ def draw_file_description( label_text_color = common.color(common.color_text) filter_text = self.parent().model().filter_text() - filter_text = filter_text.lower() if filter_text else '' - filter_texts = re.split(f'\s', filter_text, flags=re.IGNORECASE) - filter_texts = {f.lower().strip('"').strip('-').strip() for f in filter_texts} + filter_text = filter_text.lower().strip('\\/-_\'" ') if filter_text else '' + + filter_texts = re.split(r'\s', filter_text, flags=re.IGNORECASE) + filter_texts = {f.lower().strip('\\/-_\'" ') for f in filter_texts} for s in reversed(it): width = metrics.horizontalAdvance(s) @@ -2458,6 +2459,8 @@ def paint_asset_name(self, *args, offset=0): painter.setPen(QtCore.Qt.NoPen) filter_text = self.parent().model().filter_text() + filter_text = filter_text.lower().strip('\\/-_\'" ') if filter_text else '' + overlay_rect_left_edge = None for segment in text_segments.values(): diff --git a/bookmarks/items/favourite_items.py b/bookmarks/items/favourite_items.py index aad9b6d5..3328819f 100644 --- a/bookmarks/items/favourite_items.py +++ b/bookmarks/items/favourite_items.py @@ -86,10 +86,6 @@ def init_data(self): if self._interrupt_requested: break - # Skipping directories - if entry.is_dir(): - continue - filename = entry.name _source_path = '/'.join(source_paths) @@ -257,6 +253,8 @@ def init_data(self): data[idx][common.DataDictRole] = weakref.ref(data) data[idx][common.IdRole] = idx + self.set_refresh_needed(False) + def source_path(self): """The path of the source file. diff --git a/bookmarks/items/file_items.py b/bookmarks/items/file_items.py index 10c606e3..d652bb9f 100644 --- a/bookmarks/items/file_items.py +++ b/bookmarks/items/file_items.py @@ -300,7 +300,8 @@ def init_data(self): if not p or not all(p) or not k or t is None: return - _dirs = [] + _subdirectories = [] + _watch_paths = [] _extensions = [] data = common.get_data(p, k, t) @@ -311,7 +312,7 @@ def init_data(self): _source_path = '/'.join(p + (k,)) if not QtCore.QFileInfo(_source_path).exists(): return - _dirs.append(_source_path) + _watch_paths.append(_source_path) # Let's get the token config instance to check what extensions are # currently allowed to be displayed in the task folder @@ -357,8 +358,6 @@ def init_data(self): entry.path, _source_path ) - _dirs.append(_dir) - _extensions.append(ext) # We'll check against the current file extension against the allowed # extensions. If the task folder is not defined in the token config, @@ -366,6 +365,12 @@ def init_data(self): if not disable_filter and ext not in valid_extensions: continue + if _dir: + _watch_paths.append(_dir) + _d = _dir[len(_source_path) + 1:] + _subdirectories += [('/' + f) for f in _d.split('/')] + _extensions.append(ext) + # Progress bar c += 1 if not c % nth: @@ -493,6 +498,7 @@ def init_data(self): else: # The sequence dictionary should contain not only sequence items but single files also, # so we'll add them here + sequence_data[filepath] = common.DataDict(data[idx]) sequence_data[filepath][common.IdRole] = -1 # Cast the sequence data back onto the model @@ -535,13 +541,18 @@ def init_data(self): watcher = common.get_watcher(common.FileTab) watcher.reset() - watcher.add_directories(list(set(_dirs))) + watcher.add_directories(sorted(set([f for f in _watch_paths if f]))) # Add the list of file extensions to the model's data - _extensions = sorted(set([('.' + f) for f in _extensions])) + _extensions = sorted(set([('.' + f) for f in _extensions if f])) common.get_data(p, k, common.FileItem).file_types = _extensions common.get_data(p, k, common.SequenceItem).file_types = _extensions + # Add the list of subdirectories to the model's data + _subdirectories = sorted(set([f for f in _subdirectories if f])) + common.get_data(p, k, common.FileItem).subdirectories = _subdirectories + common.get_data(p, k, common.SequenceItem).subdirectories = _subdirectories + self.set_refresh_needed(False) def disable_filter(self): diff --git a/bookmarks/items/models.py b/bookmarks/items/models.py index 1bd95d9a..02d3aecf 100644 --- a/bookmarks/items/models.py +++ b/bookmarks/items/models.py @@ -1024,6 +1024,10 @@ def filterAcceptsRow(self, idx, parent=None): if not ref(): return False + # Task item specific filter + if '#empty#' in ref()[idx][common.DescriptionRole]: + return False + filter_text = self.filter_text() if filter_text: filter_text = filter_text.strip().lower() diff --git a/bookmarks/items/task_items.py b/bookmarks/items/task_items.py index 6a50d92c..39694f29 100644 --- a/bookmarks/items/task_items.py +++ b/bookmarks/items/task_items.py @@ -20,6 +20,7 @@ from .. import contextmenu from .. import images from .. import log +from ..threads import threads from ..tokens import tokens @@ -127,15 +128,23 @@ def paint_name(self, *args): return active = index.data(QtCore.Qt.DisplayRole) == common.active('task') + o = common.size(common.size_margin) + rect = option.rect if index.data(common.NoteCountRole): color = common.color( common.color_selected_text ) if hover else common.color(common.color_text) + pixmap = images.rsc_pixmap( + 'folder', common.color(common.color_separator), o + ) else: color = common.color(common.color_text) if hover else common.color( common.color_light_background ) + pixmap = images.rsc_pixmap( + 'folder', common.color(common.color_dark_background), o + ) color = common.color(common.color_selected_text) if active else color color = common.color(common.color_selected_text) if selected else color @@ -143,18 +152,6 @@ def paint_name(self, *args): common.size(common.size_font_medium) )[0] - o = common.size(common.size_margin) - rect = QtCore.QRect(option.rect) - - if index.data(common.NoteCountRole): - pixmap = images.rsc_pixmap( - 'folder', common.color(common.color_separator), o - ) - else: - pixmap = images.rsc_pixmap( - 'folder', common.color(common.color_dark_background), o - ) - _rect = QtCore.QRect(0, 0, o, o) _rect.moveCenter(option.rect.center()) _rect.moveLeft( @@ -173,13 +170,14 @@ def paint_name(self, *args): items = [] - if index.data(QtCore.Qt.ToolTipRole): + if index.data(common.DescriptionRole): + color = common.color( - common.color_selected_text - ) if active else common.color(common.color_text) - color = common.color(common.color_selected_text) if hover else color - color = common.color(common.color_selected_text) if selected else color - items.append((index.data(QtCore.Qt.ToolTipRole), color)) + common.color_green + ) if active else common.color(common.color_secondary_text) + color = common.color(common.color_text) if hover else color + color = common.color(common.color_text) if selected else color + items.append((index.data(common.DescriptionRole), color)) for idx, val in enumerate(items): text, color = val @@ -212,6 +210,11 @@ def sizeHint(self, option, index): """Size hint. """ + if index.data(common.NoteCountRole) == 0: + return QtCore.QSize( + self.parent().model().sourceModel().row_size.width(), + self.parent().model().sourceModel().row_size.height() * 0.5 + ) return self.parent().model().sourceModel().row_size @@ -282,8 +285,6 @@ def init_data(self): return _source_path = '/'.join(source_path) - config = tokens.get(*source_path[0:3]) - for entry in self.item_generator(source): if entry.name.startswith('.'): continue @@ -293,7 +294,6 @@ def init_data(self): path = entry.path.replace('\\', '/') idx = len(data) - description = config.get_description(entry.name) data[idx] = common.DataDict( { @@ -302,10 +302,10 @@ def init_data(self): common.PathRole: path, QtCore.Qt.SizeHintRole: self.row_size, # - QtCore.Qt.StatusTipRole: description, - QtCore.Qt.AccessibleDescriptionRole: description, - QtCore.Qt.WhatsThisRole: description, - QtCore.Qt.ToolTipRole: description, + QtCore.Qt.StatusTipRole: path, + QtCore.Qt.AccessibleDescriptionRole: path, + QtCore.Qt.WhatsThisRole: path, + QtCore.Qt.ToolTipRole: path, # common.QueueRole: self.queues, common.DataTypeRole: common.FileItem, @@ -314,9 +314,9 @@ def init_data(self): # common.EntryRole: [entry, ], common.FlagsRole: flags, - common.ParentPathRole: source_path, - common.DescriptionRole: description, - common.NoteCountRole: self.file_count(path), + common.ParentPathRole: list(source_path) + [entry.name,], + common.DescriptionRole: '', + common.NoteCountRole: 0, common.FileDetailsRole: '', common.SequenceRole: None, common.FramesRole: [], @@ -326,10 +326,10 @@ def init_data(self): common.FileInfoLoaded: False, common.ThumbnailLoaded: True, # - common.SortByNameRole: entry.name.lower(), - common.SortByLastModifiedRole: entry.name.lower(), - common.SortBySizeRole: entry.name.lower(), - common.SortByTypeRole: entry.name.lower(), + common.SortByNameRole: 'z' + entry.name.lower(), + common.SortByLastModifiedRole: 'z' + entry.name.lower(), + common.SortBySizeRole: 'z' + entry.name.lower(), + common.SortByTypeRole: 'z' + entry.name.lower(), # common.IdRole: idx, # @@ -359,17 +359,6 @@ def filter_setting_dict_key(self): """ return 'task' - def file_count(self, source): - """Counts the number of file items the current task folder has. - - """ - count = 0 - for _ in self.item_generator(source): - count += 1 - if count > 9: - break - return count - class TaskItemView(views.ThreadedItemView): """The view responsible for displaying the available data-keys. @@ -378,13 +367,15 @@ class TaskItemView(views.ThreadedItemView): Delegate = TaskItemViewDelegate ContextMenu = TaskItemContextMenu - queues = () + queues = (threads.FileInfo2, ) def __init__(self, parent=None): super().__init__( icon='folder', parent=parent ) + self.filter_indicator_widget.setHidden(True) + self._context_menu_active = False self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) self.setContextMenuPolicy(QtCore.Qt.DefaultContextMenu) @@ -454,6 +445,12 @@ def inline_icons_count(self): """ return 0 + def paint_hint(self, widget, event): + return + + def paint_status_message(self, widget, event): + return + def hideEvent(self, event): """Event handler. diff --git a/bookmarks/items/views.py b/bookmarks/items/views.py index c59f20b0..b3e2f34d 100644 --- a/bookmarks/items/views.py +++ b/bookmarks/items/views.py @@ -555,10 +555,10 @@ def __init__(self, icon='icon_bw', parent=None): self.filter_editor = filter_editor.TextFilterEditor(parent=self.parent()) self.filter_editor.setHidden(True) - self.setAttribute(QtCore.Qt.WA_NoSystemBackground) - self.setAttribute(QtCore.Qt.WA_TranslucentBackground) - self.viewport().setAttribute(QtCore.Qt.WA_NoSystemBackground) - self.viewport().setAttribute(QtCore.Qt.WA_TranslucentBackground) + self.setAttribute(QtCore.Qt.WA_NoSystemBackground, True) + self.setAttribute(QtCore.Qt.WA_TranslucentBackground, True) + self.viewport().setAttribute(QtCore.Qt.WA_NoSystemBackground, True) + self.viewport().setAttribute(QtCore.Qt.WA_TranslucentBackground, True) self.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection) self.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows) @@ -1854,41 +1854,58 @@ def clickable_rectangle_event(self, event): if not text: continue - text = text.lower() + _text = text.lower().strip('\\/-_\'" ') if not rect.contains(cursor_position): continue filter_text = self.model().filter_text() - filter_text = filter_text.lower() if filter_text else '' + filter_text = filter_text if filter_text else '' - if shift_modifier: - # Shift modifier will add a "positive" filter and hide all items - # that does not contain the given text. - folder_filter = f'"{text}"' + filter_texts = filter_text.split(' ') if filter_text else [] + _filter_texts = [ + ('--' + f.lower().strip('\\/-_\'" ') if f.startswith('--') else f.lower().strip('\\/-_\'" ')) + for f in filter_texts + ] + filter_texts_ = [f.lower().strip('\\/-_\'" ') for f in filter_texts] - if folder_filter in filter_text: - filter_text = filter_text.replace(folder_filter, '') - else: - filter_text = f'{filter_text} {folder_filter}' + # Shift modifier toggles a text filter + if shift_modifier: + # If the filter is empty we'll add a positive filter + if not filter_texts: + self.model().set_filter_text(f'"/{_text}"') + self.repaint(self.rect()) + return - self.model().set_filter_text(filter_text) + # If the clicked item is already in the filter, we'll remove it + for idx, filter_text_element in enumerate(_filter_texts): + if _text in filter_text_element: + del filter_texts[idx] + self.model().set_filter_text(' '.join(filter_texts)) + self.repaint(self.rect()) + return + # If the filter has items we'll append + self.model().set_filter_text(f'{filter_text} "/{_text}"') self.repaint(self.rect()) return + # Alt or control modifiers toggle a negative filter if alt_modifier or control_modifier: - # The alt or control modifiers will add a "negative filter" - # and hide the selected sub-folder from the view - folder_filter = f'--"{text}"' - _folder_filter = f'"{text}"' - - if filter_text: - if _folder_filter in filter_text: - filter_text = filter_text.replace(_folder_filter, '') - if folder_filter not in filter_text: - folder_filter = f'{filter_text} {folder_filter}' - - self.model().set_filter_text(folder_filter) + # If the filter is empty we'll add a negative filter + if not filter_texts: + self.model().set_filter_text(f'--"/{_text}"') + self.repaint(self.rect()) + return + + # If the clicked item is already in the filter, we'll remove it + for idx, filter_text_element in enumerate(filter_texts_): + if _text in filter_text_element: + del filter_texts[idx] + + filter_texts.append(f'--"/{_text}"') + + # add negative filter + self.model().set_filter_text(' '.join(filter_texts)) self.repaint(self.rect()) return diff --git a/bookmarks/scripts/clips.py b/bookmarks/scripts/clips.py index f5f21ec4..e42c6ca3 100644 --- a/bookmarks/scripts/clips.py +++ b/bookmarks/scripts/clips.py @@ -2,7 +2,6 @@ """ import functools import os -import re import opentimelineio as otio from PySide2 import QtWidgets, QtCore @@ -10,6 +9,7 @@ from .. import actions from .. import common from .. import database +from .. import log from .. import ui from ..editor import base from ..external import rv @@ -716,7 +716,7 @@ def _get_sources(path, ext): for k in DEFAULT_SOURCES: if not hasattr(self, f'{k.replace("/", "_")}_editor'): - print(f'No editor found for {k}, skipping.') + log.debug(f'No editor found for {k}, skipping.') continue editor = getattr(self, f'{k.replace("/", "_")}_editor') @@ -730,7 +730,7 @@ def _get_sources(path, ext): source_ext = source_info.suffix() if not QtCore.QFileInfo(source_dir).exists(): - print(f'{source_dir} does not exist, skipping.') + log.debug(f'{source_dir} does not exist, skipping.') continue common.message_widget.body_label.setText(f'Found source dir:\n{source_dir}') diff --git a/bookmarks/threads/threads.py b/bookmarks/threads/threads.py index d84f5757..8a8e47c2 100644 --- a/bookmarks/threads/threads.py +++ b/bookmarks/threads/threads.py @@ -101,18 +101,7 @@ def __init__(self, q, t): }, 'worker': workers.InfoWorker, 'role': common.FileInfoLoaded, - 'tab': common.FileTab, - }, - FileInfo3: { - 'queue': collections.deque([], common.max_list_items), - 'preload': True, - 'data_types': { - common.FileItem: DataType(FileInfo, common.FileItem), - common.SequenceItem: DataType(FileInfo, common.SequenceItem), - }, - 'worker': workers.InfoWorker, - 'role': common.FileInfoLoaded, - 'tab': common.FileTab, + 'tab': common.TaskTab, }, FileThumbnail: { 'queue': collections.deque([], 99), diff --git a/bookmarks/threads/workers.py b/bookmarks/threads/workers.py index de2583f1..217b841f 100644 --- a/bookmarks/threads/workers.py +++ b/bookmarks/threads/workers.py @@ -14,6 +14,7 @@ from .. import database from .. import images from .. import log +from ..tokens import tokens from ..shotgun import shotgun @@ -644,7 +645,64 @@ def _process_data(self, ref): flags |= _proxy_flags if _proxy_flags else 0 ref()[common.FlagsRole] = QtCore.Qt.ItemFlags(flags) - self.count_items(ref, st) + if ref() and ref()[common.ItemTabRole] == common.TaskTab: + # Let's get the token config instance to check what extensions are + # currently allowed to be displayed in the task folder + config = tokens.get(*pp[0:3]) + + description = config.get_description(pp[-1]) + ref()[common.DescriptionRole] = description + + is_valid_task = config.check_task(pp[-1]) + if is_valid_task: + valid_extensions = config.get_task_extensions(pp[-1]) + else: + valid_extensions = config.get_extensions(tokens.AllFormat) + + def _file_it(path): + for entry in os.scandir(path): + if entry.is_symlink(): + continue + if entry.name.startswith('.'): + continue + if entry.name == 'thumbs.db': + continue + + if entry.is_dir(): + yield from _file_it(entry.path) + + if not entry.is_file(): + continue + + if QtCore.QFileInfo(entry.path).suffix().lower() not in valid_extensions: + continue + + yield entry.path + + _idx = 0 + _max = 199 + for _idx, _ in enumerate(_file_it(st)): + if not ref(): + break + + ref()[common.NoteCountRole] = _idx + 1 + + if _idx == 1: + _s = 'a' + pp[-1].lower() + ref()[common.SortByNameRole] = _s + ref()[common.SortByLastModifiedRole] = _s + ref()[common.SortBySizeRole] = _s + ref()[common.SortByTypeRole] = _s + + if _idx > _max: + break + + _suffix = f'{_idx + 1} items' if _idx < _max else f'{_max}+ items' + _suffix = _suffix if _idx > 0 else '#empty#' + _suffix = f' ({_suffix})' if description else _suffix + + ref()[common.DescriptionRole] += _suffix + self._process_bookmark_item(ref, db.source(), bookmark_row_data, pp) self._process_file_item(ref, item_type) @@ -750,9 +808,6 @@ def _process_file_item(self, ref, item_type): ref()[common.FileDetailsRole] = info_string ref()[common.SortBySizeRole] = size - def count_items(self, ref, source): - pass - class ThumbnailWorker(BaseWorker): """Thread worker responsible for creating and loading thumbnails. diff --git a/bookmarks/topbar/filters.py b/bookmarks/topbar/filters.py index d1d359f5..c1ab3498 100644 --- a/bookmarks/topbar/filters.py +++ b/bookmarks/topbar/filters.py @@ -1,7 +1,7 @@ """""" import weakref -from PySide2 import QtCore, QtGui, QtWidgets +from PySide2 import QtCore, QtWidgets from .. import common from .. import log @@ -10,9 +10,10 @@ class BaseFilterModel(ui.AbstractListModel): - def __init__(self, section_name_label, data_source, tab_index, parent=None): + def __init__(self, section_name_label, data_source, tab_index, icon, parent=None): self.tab_index = tab_index + self.icon = icon self.show_all_label = ' - Show All -' self.section_name_label = section_name_label self.data_source = data_source @@ -55,9 +56,7 @@ def internal_data_ready(self, ref): source_model = common.source_model(self.tab_index) data = common.get_data( - source_model.source_path(), - source_model.task(), - source_model.data_type() + source_model.source_path(), source_model.task(), source_model.data_type() ) if ref() != data: @@ -79,9 +78,7 @@ def init_data(self, *args, **kwargs): source_model = common.source_model(self.tab_index) data = common.get_data( - source_model.source_path(), - source_model.task(), - source_model.data_type() + source_model.source_path(), source_model.task(), source_model.data_type() ) if not hasattr(data, self.data_source): @@ -94,7 +91,18 @@ def init_data(self, *args, **kwargs): QtCore.Qt.DisplayRole: self.section_name_label, QtCore.Qt.SizeHintRole: QtCore.QSize(1, common.size(common.size_row_height) * 0.66), common.FlagsRole: QtCore.Qt.NoItemFlags, - QtCore.Qt.TextAlignmentRole: QtCore.Qt.AlignCenter, + QtCore.Qt.TextAlignmentRole: QtCore.Qt.AlignLeft | QtCore.Qt.AlignBottom, + } + + self._data[len(self._data)] = { + QtCore.Qt.DisplayRole: '', + common.FlagsRole: QtCore.Qt.NoItemFlags, + QtCore.Qt.SizeHintRole:QtCore.QSize(1, common.size(common.size_separator)), + } + self._data[len(self._data)] = { + QtCore.Qt.DisplayRole: '', + common.FlagsRole: QtCore.Qt.NoItemFlags, + QtCore.Qt.SizeHintRole: QtCore.QSize(1, common.size(common.size_separator)), } self._data[len(self._data)] = { @@ -104,17 +112,28 @@ def init_data(self, *args, **kwargs): QtCore.Qt.TextAlignmentRole: QtCore.Qt.AlignCenter, } - icon = ui.get_icon('sg') + self._data[len(self._data)] = { + QtCore.Qt.DisplayRole: '', + common.FlagsRole: QtCore.Qt.NoItemFlags, + QtCore.Qt.SizeHintRole:QtCore.QSize(1, common.size(common.size_separator)), + } + self._data[len(self._data)] = { + QtCore.Qt.DisplayRole: '', + common.FlagsRole: QtCore.Qt.NoItemFlags, + QtCore.Qt.SizeHintRole:QtCore.QSize(1, common.size(common.size_separator)), + } - for task in sorted(getattr(data, self.data_source)): + icon = ui.get_icon(self.icon) + + for v in sorted(getattr(data, self.data_source)): self._data[len(self._data)] = { - QtCore.Qt.DisplayRole: task, + QtCore.Qt.DisplayRole: v, QtCore.Qt.SizeHintRole: self.row_size, QtCore.Qt.DecorationRole: icon, - QtCore.Qt.StatusTipRole: task, - QtCore.Qt.AccessibleDescriptionRole: task, - QtCore.Qt.WhatsThisRole: task, - QtCore.Qt.ToolTipRole: task, + QtCore.Qt.StatusTipRole: v, + QtCore.Qt.AccessibleDescriptionRole: v, + QtCore.Qt.WhatsThisRole: v, + QtCore.Qt.ToolTipRole: v, QtCore.Qt.TextAlignmentRole: QtCore.Qt.AlignCenter, } @@ -123,16 +142,24 @@ class BaseFilterButton(QtWidgets.QComboBox): """The combo box used to set a text filter based on the available ShotGrid task names. """ + def __init__(self, Model, tab_index, parent=None): super().__init__(parent=parent) self.tab_index = tab_index - self.setView(QtWidgets.QListView(parent=self)) + view = QtWidgets.QListView(parent=self) + view.setSizeAdjustPolicy( + QtWidgets.QAbstractScrollArea.AdjustToContents + ) + self.setView(view) self.setModel(Model()) self.setSizeAdjustPolicy(QtWidgets.QComboBox.AdjustToContents) self.setFixedHeight(common.size(common.size_margin)) - self.setMinimumWidth(common.size(common.size_margin)) + self.setMinimumWidth(common.size(common.size_margin) * 3) + + min_width = self.minimumSizeHint().width() + self.view().setMinimumWidth(min_width * 3) common.signals.updateTopBarButtons.connect(lambda: self.setHidden(not common.current_tab() == self.tab_index)) common.model(self.tab_index).filterTextChanged.connect(self.select_text) @@ -191,10 +218,7 @@ class TaskFilterModel(BaseFilterModel): def __init__(self, parent=None): super().__init__( - 'Tasks', - 'sg_task_names', - common.AssetTab, - parent=parent + 'Tasks', 'sg_task_names', common.AssetTab, 'sg', parent=parent ) @@ -202,11 +226,10 @@ class TaskFilterButton(BaseFilterButton): """The combo box used to set a text filter based on the available ShotGrid task names. """ + def __init__(self, parent=None): super().__init__( - TaskFilterModel, - common.AssetTab, - parent=parent + TaskFilterModel, common.AssetTab, parent=parent ) @@ -214,10 +237,7 @@ class EntityFilterModel(BaseFilterModel): def __init__(self, parent=None): super().__init__( - 'Assets', - 'shotgun_names', - common.AssetTab, - parent=parent + 'Assets', 'shotgun_names', common.AssetTab, 'sg', parent=parent ) @@ -225,23 +245,18 @@ class EntityFilterButton(BaseFilterButton): """The combo box used to set a text filter based on the available ShotGrid task names. """ + def __init__(self, parent=None): super().__init__( - EntityFilterModel, - common.AssetTab, - parent=parent + EntityFilterModel, common.AssetTab, parent=parent ) - class TypeFilterModel(BaseFilterModel): def __init__(self, parent=None): super().__init__( - 'File Types', - 'file_types', - common.FileTab, - parent=parent + 'File Types', 'file_types', common.FileTab, 'file', parent=parent ) common.signals.assetActivated.connect(self.reset_data) @@ -252,9 +267,149 @@ class TypeFilterButton(BaseFilterButton): """The combo box used to set a text filter based on the available file types """ + + def __init__(self, parent=None): + super().__init__( + TypeFilterModel, common.FileTab, parent=parent + ) + + +class SubdirFilterModel(BaseFilterModel): + + def __init__(self, parent=None): + super().__init__( + 'Folders', 'subdirectories', common.FileTab, 'folder', parent=parent + ) + + common.signals.assetActivated.connect(self.reset_data) + common.signals.taskFolderChanged.connect(self.reset_data) + + def init_data(self, *args, **kwargs): + super().init_data(*args, **kwargs) + + data = {} + + insert_idx = 3 + + for idx, v in self._data.items(): + if idx == 1: + data[idx] = v + + k = '- Hide Folders -' + data[idx + insert_idx] = { + QtCore.Qt.DisplayRole: k, + QtCore.Qt.SizeHintRole: self.row_size, + QtCore.Qt.DecorationRole: ui.get_icon('archivedHidden', color=common.color(common.color_red)), + QtCore.Qt.StatusTipRole: k, + QtCore.Qt.AccessibleDescriptionRole: k, + QtCore.Qt.WhatsThisRole: k, + QtCore.Qt.ToolTipRole: k, + QtCore.Qt.TextAlignmentRole: QtCore.Qt.AlignCenter, + } + continue + + if idx > insert_idx: + idx += 1 + data[idx] = v + + self._data = data + +class SubdirFilterButton(BaseFilterButton): + """The combo box used to set a text filter based on the available file types + + """ + + def __init__(self, parent=None): + super().__init__( + SubdirFilterModel, common.FileTab, parent=parent + ) + + + @QtCore.Slot(str) + def update_filter_text(self, text): + """Update the filter text. + + Args: + text (str): The text to set as the filter text. + + """ + if text == '- Hide Folders -': + filter_texts = [] + for i in range(self.count()): + text = self.itemText(i) + if not text: + continue + if text == self.model().show_all_label: + continue + if text == self.model().section_name_label: + continue + if text == '- Hide Folders -': + continue + filter_texts.append(f'--"{text}"') + common.model(self.tab_index).set_filter_text(' '.join(filter_texts)) + else: + super().update_filter_text(text) +class ServersFilterModel(BaseFilterModel): + + def __init__(self, parent=None): + super().__init__( + 'Servers', 'servers', common.BookmarkTab, 'icon', parent=parent + ) + + common.signals.assetActivated.connect(self.reset_data) + common.signals.taskFolderChanged.connect(self.reset_data) + + +class ServersFilterButton(BaseFilterButton): + """The combo box used to set a text filter based on the available file types + + """ + + def __init__(self, parent=None): + super().__init__( + ServersFilterModel, common.BookmarkTab, parent=parent + ) + + +class JobsFilterModel(BaseFilterModel): + + def __init__(self, parent=None): + super().__init__( + 'Jobs', 'jobs', common.BookmarkTab, 'icon', parent=parent + ) + + common.signals.assetActivated.connect(self.reset_data) + common.signals.taskFolderChanged.connect(self.reset_data) + + +class JobsFilterButton(BaseFilterButton): + """The combo box used to set a text filter based on the available file types + + """ + + def __init__(self, parent=None): + super().__init__( + JobsFilterModel, common.BookmarkTab, parent=parent + ) + + +class RootsFilterModel(BaseFilterModel): + def __init__(self, parent=None): super().__init__( - TypeFilterModel, - common.FileTab, - parent=parent - ) \ No newline at end of file + 'Bookmarks', 'roots', common.BookmarkTab, 'icon', parent=parent + ) + + common.signals.assetActivated.connect(self.reset_data) + common.signals.taskFolderChanged.connect(self.reset_data) + + +class RootsFilterButton(BaseFilterButton): + """The combo box used to set a text filter based on the available file types + + """ + + def __init__(self, parent=None): + super().__init__( + RootsFilterModel, common.BookmarkTab, parent=parent + ) diff --git a/bookmarks/topbar/topbar.py b/bookmarks/topbar/topbar.py index c5fbfcb9..0ccf25c8 100644 --- a/bookmarks/topbar/topbar.py +++ b/bookmarks/topbar/topbar.py @@ -28,6 +28,18 @@ 'widget': tabs.FavouritesTabButton, 'hidden': False, }, + # next(n): { + # 'widget': filters.ServersFilterButton, + # 'hidden': False, + # }, + next(n): { + 'widget': filters.JobsFilterButton, + 'hidden': False, + }, + # next(n): { + # 'widget': filters.RootsFilterButton, + # 'hidden': False, + # }, next(n): { 'widget': filters.EntityFilterButton, 'hidden': False, @@ -36,6 +48,10 @@ 'widget': filters.TaskFilterButton, 'hidden': False, }, + next(n): { + 'widget': filters.SubdirFilterButton, + 'hidden': False, + }, next(n): { 'widget': filters.TypeFilterButton, 'hidden': False, From 7e289a0225d9277a79bd6e0a720e75807f576b9e Mon Sep 17 00:00:00 2001 From: Gergely Wootsch Date: Mon, 23 Oct 2023 10:12:25 +0200 Subject: [PATCH 16/32] update docs --- docs/src/generate_modules_skeleton.py | 121 +++++++++++++----- docs/src/modules/bookmarks.rst | 8 +- docs/src/modules/bookmarks/actions.rst | 2 +- .../bookmarks/bookmarker/bookmark_editor.rst | 2 +- .../bookmarks/bookmarker/job_editor.rst | 2 +- .../src/modules/bookmarks/bookmarker/main.rst | 2 +- .../bookmarks/bookmarker/server_editor.rst | 2 +- docs/src/modules/bookmarks/common/core.rst | 2 +- docs/src/modules/bookmarks/common/data.rst | 2 +- docs/src/modules/bookmarks/common/env.rst | 2 +- docs/src/modules/bookmarks/common/font.rst | 2 +- docs/src/modules/bookmarks/common/monitor.rst | 2 +- .../src/modules/bookmarks/common/sequence.rst | 2 +- .../modules/bookmarks/common/session_lock.rst | 2 +- .../src/modules/bookmarks/common/settings.rst | 2 +- docs/src/modules/bookmarks/common/setup.rst | 2 +- docs/src/modules/bookmarks/common/signals.rst | 2 +- docs/src/modules/bookmarks/common/ui.rst | 2 +- docs/src/modules/bookmarks/contextmenu.rst | 2 +- docs/src/modules/bookmarks/database.rst | 2 +- .../bookmarks/editor/asset_properties.rst | 2 +- docs/src/modules/bookmarks/editor/base.rst | 2 +- .../modules/bookmarks/editor/base_widgets.rst | 2 +- .../bookmarks/editor/bookmark_properties.rst | 2 +- .../modules/bookmarks/editor/preferences.rst | 2 +- .../src/modules/bookmarks/external/ffmpeg.rst | 2 +- .../bookmarks/external/ffmpeg_widget.rst | 2 +- docs/src/modules/bookmarks/external/rv.rst | 2 +- .../src/modules/bookmarks/file_saver/main.rst | 2 +- .../modules/bookmarks/file_saver/widgets.rst | 2 +- docs/src/modules/bookmarks/images.rst | 2 +- docs/src/modules/bookmarks/importexport.rst | 2 +- .../modules/bookmarks/items/asset_items.rst | 2 +- .../bookmarks/items/bookmark_items.rst | 2 +- docs/src/modules/bookmarks/items/delegate.rst | 2 +- .../bookmarks/items/favourite_items.rst | 2 +- .../modules/bookmarks/items/file_items.rst | 2 +- docs/src/modules/bookmarks/items/models.rst | 2 +- .../modules/bookmarks/items/task_items.rst | 2 +- docs/src/modules/bookmarks/items/views.rst | 2 +- .../bookmarks/items/widgets/filter_editor.rst | 2 +- .../bookmarks/items/widgets/image_viewer.rst | 2 +- .../bookmarks/items/widgets/thumb_capture.rst | 2 +- .../bookmarks/items/widgets/thumb_library.rst | 2 +- .../bookmarks/items/widgets/thumb_picker.rst | 2 +- .../modules/bookmarks/launcher/gallery.rst | 2 +- docs/src/modules/bookmarks/launcher/main.rst | 2 +- docs/src/modules/bookmarks/log.rst | 2 +- docs/src/modules/bookmarks/main.rst | 2 +- docs/src/modules/bookmarks/maya/actions.rst | 2 +- docs/src/modules/bookmarks/maya/base.rst | 2 +- docs/src/modules/bookmarks/maya/capture.rst | 2 +- .../modules/bookmarks/maya/contextmenu.rst | 2 +- docs/src/modules/bookmarks/maya/export.rst | 2 +- docs/src/modules/bookmarks/maya/hud.rst | 2 +- docs/src/modules/bookmarks/maya/main.rst | 2 +- docs/src/modules/bookmarks/maya/plugin.rst | 2 +- .../maya/scripts/aka_cloth_cache.rst | 10 ++ .../maya/scripts/aka_cloth_cache_animated.rst | 10 ++ .../maya/scripts/aka_make_export_sets.rst | 10 ++ .../maya/scripts/aka_odyssey_shaders.rst | 10 -- .../maya/scripts/aka_shader_templates.rst | 10 ++ .../maya/scripts/reset_joint_orientations.rst | 10 ++ .../src/modules/bookmarks/maya/shadertool.rst | 10 -- docs/src/modules/bookmarks/maya/viewport.rst | 2 +- docs/src/modules/bookmarks/notes.rst | 2 +- docs/src/modules/bookmarks/progress.rst | 2 +- docs/src/modules/bookmarks/publish.rst | 2 +- .../bookmarks/scripts/aka_odyssey_sync.rst | 10 -- docs/src/modules/bookmarks/scripts/clips.rst | 10 ++ .../scripts/send_images_to_after_effects.rst | 10 ++ .../bookmarks/scripts/sync_asset_data.rst | 10 ++ docs/src/modules/bookmarks/shortcuts.rst | 2 +- .../src/modules/bookmarks/shotgun/actions.rst | 2 +- docs/src/modules/bookmarks/shotgun/link.rst | 2 +- .../modules/bookmarks/shotgun/link_asset.rst | 2 +- .../modules/bookmarks/shotgun/link_assets.rst | 2 +- .../bookmarks/shotgun/link_bookmark.rst | 2 +- .../src/modules/bookmarks/shotgun/publish.rst | 10 -- .../bookmarks/shotgun/publish_widgets.rst | 2 +- .../bookmarks/shotgun/sg_publish_clip.rst | 10 ++ .../src/modules/bookmarks/shotgun/shotgun.rst | 2 +- docs/src/modules/bookmarks/shotgun/tasks.rst | 2 +- docs/src/modules/bookmarks/standalone.rst | 2 +- docs/src/modules/bookmarks/statusbar.rst | 2 +- docs/src/modules/bookmarks/templates.rst | 2 +- .../src/modules/bookmarks/threads/threads.rst | 2 +- .../src/modules/bookmarks/threads/workers.rst | 2 +- docs/src/modules/bookmarks/tokens/tokens.rst | 2 +- .../bookmarks/tokens/tokens_editor.rst | 2 +- docs/src/modules/bookmarks/topbar/buttons.rst | 2 +- docs/src/modules/bookmarks/topbar/filters.rst | 10 ++ .../modules/bookmarks/topbar/quickswitch.rst | 2 +- .../src/modules/bookmarks/topbar/sgfilter.rst | 10 ++ docs/src/modules/bookmarks/topbar/tabs.rst | 2 +- docs/src/modules/bookmarks/topbar/topbar.rst | 2 +- docs/src/modules/bookmarks/ui.rst | 2 +- .../bookmarks/versioncontrol/version.rst | 2 +- .../versioncontrol/versioncontrol.rst | 2 +- 99 files changed, 283 insertions(+), 160 deletions(-) create mode 100644 docs/src/modules/bookmarks/maya/scripts/aka_cloth_cache.rst create mode 100644 docs/src/modules/bookmarks/maya/scripts/aka_cloth_cache_animated.rst create mode 100644 docs/src/modules/bookmarks/maya/scripts/aka_make_export_sets.rst delete mode 100644 docs/src/modules/bookmarks/maya/scripts/aka_odyssey_shaders.rst create mode 100644 docs/src/modules/bookmarks/maya/scripts/aka_shader_templates.rst create mode 100644 docs/src/modules/bookmarks/maya/scripts/reset_joint_orientations.rst delete mode 100644 docs/src/modules/bookmarks/maya/shadertool.rst delete mode 100644 docs/src/modules/bookmarks/scripts/aka_odyssey_sync.rst create mode 100644 docs/src/modules/bookmarks/scripts/clips.rst create mode 100644 docs/src/modules/bookmarks/scripts/send_images_to_after_effects.rst create mode 100644 docs/src/modules/bookmarks/scripts/sync_asset_data.rst delete mode 100644 docs/src/modules/bookmarks/shotgun/publish.rst create mode 100644 docs/src/modules/bookmarks/shotgun/sg_publish_clip.rst create mode 100644 docs/src/modules/bookmarks/topbar/filters.rst create mode 100644 docs/src/modules/bookmarks/topbar/sgfilter.rst diff --git a/docs/src/generate_modules_skeleton.py b/docs/src/generate_modules_skeleton.py index fb71421e..3b86607b 100644 --- a/docs/src/generate_modules_skeleton.py +++ b/docs/src/generate_modules_skeleton.py @@ -1,49 +1,102 @@ +"""This script generates the rst files for the modules in the bookmarks package. + +Use it in conjunction with make.bat to generate the documentation. + +""" import os -SOURCE = os.path.normpath(f'{__file__}/{os.pardir}/{os.pardir}/{os.pardir}/bookmarks').replace('\\', '/') -DOCS_DIR = os.path.normpath(f'{__file__}/{os.pardir}/modules').replace('\\', '/') +PYTHON_MODULE_DIR = os.path.normpath(f'{__file__}/{os.pardir}/{os.pardir}/{os.pardir}/bookmarks').replace('\\', '/') +PYTHON_MODULE_DOCS_DIR = os.path.normpath(f'{__file__}/{os.pardir}/modules').replace('\\', '/') -def recursive_search(path): - for entry in os.scandir(path): - if entry.is_dir(): - yield from recursive_search(entry.path) - else: - yield entry +if not os.path.isdir(PYTHON_MODULE_DOCS_DIR): + raise RuntimeError(f'Could not find the docs directory: {PYTHON_MODULE_DOCS_DIR}') +if not os.path.isdir(PYTHON_MODULE_DIR): + raise RuntimeError(f'Could not find the source directory: {PYTHON_MODULE_DIR}') -_source = '/'.join(SOURCE.split('/')[:-1]) -contents = [] -for entry in recursive_search(SOURCE): - if not entry.name.endswith('.py'): - continue +def recursive_search(_path): + if '__pycache__' in _path: + return + + print(f'>>> Searching for modules in: {_path}') + for _entry in os.scandir(_path): + if _entry.is_dir(): + yield from recursive_search(_entry.path.replace('\\', '/')) + else: + yield _entry - path = entry.path.replace('\\', '/') - parent = os.path.join(path, os.pardir) - basemod = os.path.normpath(parent).replace('\\', '/').replace(_source, '').strip('/') - basemod = basemod if basemod else 'bookmarks' - if entry.name == '__init__.py': - rst_dir = f'{DOCS_DIR}/{basemod}' - rst_file = f'{rst_dir}/{basemod}.rst' +if __name__ == '__main__': + print('>>> Generating the documentation for the bookmarks package:') + print(f'>>> Source: {PYTHON_MODULE_DIR}') - if not os.path.isdir(rst_dir): - os.makedirs(rst_dir) - continue + _source = '/'.join(PYTHON_MODULE_DIR.split('/')[:-1]) + contents = [] - basename = path.replace(_source, '').strip('/').replace('.py', '.rst') - basename = basename if basename else 'bookmarks' - rst_file = f'{DOCS_DIR}/{basename}' - with open(rst_file, 'w') as f: + # Generate the index bookmarks.rst file + with open(f'{PYTHON_MODULE_DOCS_DIR}/bookmarks.rst', 'w') as f: f.write(f'.. meta::\n') f.write(f' :description: Developer documentation page for the Bookmarks python modules\n') - f.write(f' :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO\n') + f.write( + f' :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,' + f'Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ' + f'ffmpeg, openimageio, publish, manage, digital content management\n' + ) f.write(f'\n') - title = '.'.join(basename.replace(".rst", '').split('/')[1:]) - f.write(f'{title}\n') - f.write('=' * (len(basemod) + 10) + '\n') + f.write(f'bookmarks\n') + f.write(f'=========\n') f.write(f'\n') - f.write(f'.. automodule:: {basename.replace("/", ".").replace(".rst", "")}\n') - f.write(' :members:\n') - f.write(' :show-inheritance:\n') \ No newline at end of file + f.write(f'.. automodule:: bookmarks\n') + f.write(f' :members:\n') + f.write(f' :show-inheritance:\n') + f.write(f'\n') + f.write(f'.. toctree::\n') + f.write(f' :glob:\n') + f.write(f'\n') + f.write(f' bookmarks/**\n') + f.write(f'\n') + + for entry in recursive_search(PYTHON_MODULE_DIR): + if not entry.name.endswith('.py'): + continue + + path = entry.path.replace('\\', '/') + + parent = os.path.join(path, os.pardir) + basemod = os.path.normpath(parent).replace('\\', '/').replace(_source, '').strip('/') + basemod = basemod if basemod else 'bookmarks' + + if entry.name == '__init__.py': + rst_dir = f'{PYTHON_MODULE_DOCS_DIR}/{basemod}' + rst_file = f'{rst_dir}/{basemod}.rst' + os.makedirs(rst_dir, exist_ok=True) + continue + + basename = path.replace(_source, '').strip('/').replace('.py', '.rst') + basename = basename if basename else 'bookmarks' + rst_file = f'{PYTHON_MODULE_DOCS_DIR}/{basename}' + + os.makedirs(os.path.dirname(rst_file), exist_ok=True) + + with open(rst_file, 'w') as f: + print(f'>>> Generating rst for: {path}') + + f.write(f'.. meta::\n') + f.write(f' :description: Developer documentation page for the Bookmarks python modules\n') + f.write( + f' :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,' + f'Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ' + f'ffmpeg, openimageio, publish, manage, digital content management\n' + ) + f.write(f'\n') + title = '.'.join(basename.replace(".rst", '').split('/')[1:]) + f.write(f'{title}\n') + f.write('=' * (len(basemod) + 10) + '\n') + f.write(f'\n') + f.write(f'.. automodule:: {basename.replace("/", ".").replace(".rst", "")}\n') + f.write(' :members:\n') + f.write(' :show-inheritance:\n') + + diff --git a/docs/src/modules/bookmarks.rst b/docs/src/modules/bookmarks.rst index d9def023..4a7a9dbc 100644 --- a/docs/src/modules/bookmarks.rst +++ b/docs/src/modules/bookmarks.rst @@ -1,6 +1,6 @@ .. meta:: :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management bookmarks ========= @@ -9,8 +9,8 @@ bookmarks :members: :show-inheritance: - .. toctree:: - :glob: + :glob: + + bookmarks/** - bookmarks/** \ No newline at end of file diff --git a/docs/src/modules/bookmarks/actions.rst b/docs/src/modules/bookmarks/actions.rst index 065f05e6..6df354cd 100644 --- a/docs/src/modules/bookmarks/actions.rst +++ b/docs/src/modules/bookmarks/actions.rst @@ -1,6 +1,6 @@ .. meta:: :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management actions =================== diff --git a/docs/src/modules/bookmarks/bookmarker/bookmark_editor.rst b/docs/src/modules/bookmarks/bookmarker/bookmark_editor.rst index 1b1599ad..c2f16c4b 100644 --- a/docs/src/modules/bookmarks/bookmarker/bookmark_editor.rst +++ b/docs/src/modules/bookmarks/bookmarker/bookmark_editor.rst @@ -1,6 +1,6 @@ .. meta:: :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management bookmarker.bookmark_editor ============================== diff --git a/docs/src/modules/bookmarks/bookmarker/job_editor.rst b/docs/src/modules/bookmarks/bookmarker/job_editor.rst index 2258aada..1e592ee6 100644 --- a/docs/src/modules/bookmarks/bookmarker/job_editor.rst +++ b/docs/src/modules/bookmarks/bookmarker/job_editor.rst @@ -1,6 +1,6 @@ .. meta:: :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management bookmarker.job_editor ============================== diff --git a/docs/src/modules/bookmarks/bookmarker/main.rst b/docs/src/modules/bookmarks/bookmarker/main.rst index f4a46a07..c9753b51 100644 --- a/docs/src/modules/bookmarks/bookmarker/main.rst +++ b/docs/src/modules/bookmarks/bookmarker/main.rst @@ -1,6 +1,6 @@ .. meta:: :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management bookmarker.main ============================== diff --git a/docs/src/modules/bookmarks/bookmarker/server_editor.rst b/docs/src/modules/bookmarks/bookmarker/server_editor.rst index 5e0bf821..79f542a4 100644 --- a/docs/src/modules/bookmarks/bookmarker/server_editor.rst +++ b/docs/src/modules/bookmarks/bookmarker/server_editor.rst @@ -1,6 +1,6 @@ .. meta:: :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management bookmarker.server_editor ============================== diff --git a/docs/src/modules/bookmarks/common/core.rst b/docs/src/modules/bookmarks/common/core.rst index 966112b7..98d53ae3 100644 --- a/docs/src/modules/bookmarks/common/core.rst +++ b/docs/src/modules/bookmarks/common/core.rst @@ -1,6 +1,6 @@ .. meta:: :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management common.core ========================== diff --git a/docs/src/modules/bookmarks/common/data.rst b/docs/src/modules/bookmarks/common/data.rst index 7bc0d435..2d05a31a 100644 --- a/docs/src/modules/bookmarks/common/data.rst +++ b/docs/src/modules/bookmarks/common/data.rst @@ -1,6 +1,6 @@ .. meta:: :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management common.data ========================== diff --git a/docs/src/modules/bookmarks/common/env.rst b/docs/src/modules/bookmarks/common/env.rst index 8cb6638a..e1db87fe 100644 --- a/docs/src/modules/bookmarks/common/env.rst +++ b/docs/src/modules/bookmarks/common/env.rst @@ -1,6 +1,6 @@ .. meta:: :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management common.env ========================== diff --git a/docs/src/modules/bookmarks/common/font.rst b/docs/src/modules/bookmarks/common/font.rst index 642754c2..27960c79 100644 --- a/docs/src/modules/bookmarks/common/font.rst +++ b/docs/src/modules/bookmarks/common/font.rst @@ -1,6 +1,6 @@ .. meta:: :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management common.font ========================== diff --git a/docs/src/modules/bookmarks/common/monitor.rst b/docs/src/modules/bookmarks/common/monitor.rst index a1d4634d..973c67b6 100644 --- a/docs/src/modules/bookmarks/common/monitor.rst +++ b/docs/src/modules/bookmarks/common/monitor.rst @@ -1,6 +1,6 @@ .. meta:: :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management common.monitor ========================== diff --git a/docs/src/modules/bookmarks/common/sequence.rst b/docs/src/modules/bookmarks/common/sequence.rst index 146c1dde..8dc40194 100644 --- a/docs/src/modules/bookmarks/common/sequence.rst +++ b/docs/src/modules/bookmarks/common/sequence.rst @@ -1,6 +1,6 @@ .. meta:: :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management common.sequence ========================== diff --git a/docs/src/modules/bookmarks/common/session_lock.rst b/docs/src/modules/bookmarks/common/session_lock.rst index db7459bf..37fec42d 100644 --- a/docs/src/modules/bookmarks/common/session_lock.rst +++ b/docs/src/modules/bookmarks/common/session_lock.rst @@ -1,6 +1,6 @@ .. meta:: :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management common.session_lock ========================== diff --git a/docs/src/modules/bookmarks/common/settings.rst b/docs/src/modules/bookmarks/common/settings.rst index 53074594..4d5655f1 100644 --- a/docs/src/modules/bookmarks/common/settings.rst +++ b/docs/src/modules/bookmarks/common/settings.rst @@ -1,6 +1,6 @@ .. meta:: :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management common.settings ========================== diff --git a/docs/src/modules/bookmarks/common/setup.rst b/docs/src/modules/bookmarks/common/setup.rst index a6c6728a..ce7bd7e4 100644 --- a/docs/src/modules/bookmarks/common/setup.rst +++ b/docs/src/modules/bookmarks/common/setup.rst @@ -1,6 +1,6 @@ .. meta:: :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management common.setup ========================== diff --git a/docs/src/modules/bookmarks/common/signals.rst b/docs/src/modules/bookmarks/common/signals.rst index c43c08c4..cb82318b 100644 --- a/docs/src/modules/bookmarks/common/signals.rst +++ b/docs/src/modules/bookmarks/common/signals.rst @@ -1,6 +1,6 @@ .. meta:: :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management common.signals ========================== diff --git a/docs/src/modules/bookmarks/common/ui.rst b/docs/src/modules/bookmarks/common/ui.rst index 21f318df..2ad208cc 100644 --- a/docs/src/modules/bookmarks/common/ui.rst +++ b/docs/src/modules/bookmarks/common/ui.rst @@ -1,6 +1,6 @@ .. meta:: :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management common.ui ========================== diff --git a/docs/src/modules/bookmarks/contextmenu.rst b/docs/src/modules/bookmarks/contextmenu.rst index f3bbf2be..3d6f5e25 100644 --- a/docs/src/modules/bookmarks/contextmenu.rst +++ b/docs/src/modules/bookmarks/contextmenu.rst @@ -1,6 +1,6 @@ .. meta:: :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management contextmenu =================== diff --git a/docs/src/modules/bookmarks/database.rst b/docs/src/modules/bookmarks/database.rst index 7f7c6841..4447dfce 100644 --- a/docs/src/modules/bookmarks/database.rst +++ b/docs/src/modules/bookmarks/database.rst @@ -1,6 +1,6 @@ .. meta:: :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management database =================== diff --git a/docs/src/modules/bookmarks/editor/asset_properties.rst b/docs/src/modules/bookmarks/editor/asset_properties.rst index d29cbac7..a61b17db 100644 --- a/docs/src/modules/bookmarks/editor/asset_properties.rst +++ b/docs/src/modules/bookmarks/editor/asset_properties.rst @@ -1,6 +1,6 @@ .. meta:: :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management editor.asset_properties ========================== diff --git a/docs/src/modules/bookmarks/editor/base.rst b/docs/src/modules/bookmarks/editor/base.rst index e9898a4e..b54b27e1 100644 --- a/docs/src/modules/bookmarks/editor/base.rst +++ b/docs/src/modules/bookmarks/editor/base.rst @@ -1,6 +1,6 @@ .. meta:: :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management editor.base ========================== diff --git a/docs/src/modules/bookmarks/editor/base_widgets.rst b/docs/src/modules/bookmarks/editor/base_widgets.rst index d49c1e41..092dde28 100644 --- a/docs/src/modules/bookmarks/editor/base_widgets.rst +++ b/docs/src/modules/bookmarks/editor/base_widgets.rst @@ -1,6 +1,6 @@ .. meta:: :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management editor.base_widgets ========================== diff --git a/docs/src/modules/bookmarks/editor/bookmark_properties.rst b/docs/src/modules/bookmarks/editor/bookmark_properties.rst index 86bd9dc3..dbeda631 100644 --- a/docs/src/modules/bookmarks/editor/bookmark_properties.rst +++ b/docs/src/modules/bookmarks/editor/bookmark_properties.rst @@ -1,6 +1,6 @@ .. meta:: :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management editor.bookmark_properties ========================== diff --git a/docs/src/modules/bookmarks/editor/preferences.rst b/docs/src/modules/bookmarks/editor/preferences.rst index 11870642..55673d9c 100644 --- a/docs/src/modules/bookmarks/editor/preferences.rst +++ b/docs/src/modules/bookmarks/editor/preferences.rst @@ -1,6 +1,6 @@ .. meta:: :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management editor.preferences ========================== diff --git a/docs/src/modules/bookmarks/external/ffmpeg.rst b/docs/src/modules/bookmarks/external/ffmpeg.rst index 5268435a..5f3837c3 100644 --- a/docs/src/modules/bookmarks/external/ffmpeg.rst +++ b/docs/src/modules/bookmarks/external/ffmpeg.rst @@ -1,6 +1,6 @@ .. meta:: :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management external.ffmpeg ============================ diff --git a/docs/src/modules/bookmarks/external/ffmpeg_widget.rst b/docs/src/modules/bookmarks/external/ffmpeg_widget.rst index 915cd59a..1b24a1ab 100644 --- a/docs/src/modules/bookmarks/external/ffmpeg_widget.rst +++ b/docs/src/modules/bookmarks/external/ffmpeg_widget.rst @@ -1,6 +1,6 @@ .. meta:: :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management external.ffmpeg_widget ============================ diff --git a/docs/src/modules/bookmarks/external/rv.rst b/docs/src/modules/bookmarks/external/rv.rst index 0c979738..7e5e1625 100644 --- a/docs/src/modules/bookmarks/external/rv.rst +++ b/docs/src/modules/bookmarks/external/rv.rst @@ -1,6 +1,6 @@ .. meta:: :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management external.rv ============================ diff --git a/docs/src/modules/bookmarks/file_saver/main.rst b/docs/src/modules/bookmarks/file_saver/main.rst index 4b809c4c..032a3aab 100644 --- a/docs/src/modules/bookmarks/file_saver/main.rst +++ b/docs/src/modules/bookmarks/file_saver/main.rst @@ -1,6 +1,6 @@ .. meta:: :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management file_saver.main ============================== diff --git a/docs/src/modules/bookmarks/file_saver/widgets.rst b/docs/src/modules/bookmarks/file_saver/widgets.rst index 2e14ad9f..95937423 100644 --- a/docs/src/modules/bookmarks/file_saver/widgets.rst +++ b/docs/src/modules/bookmarks/file_saver/widgets.rst @@ -1,6 +1,6 @@ .. meta:: :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management file_saver.widgets ============================== diff --git a/docs/src/modules/bookmarks/images.rst b/docs/src/modules/bookmarks/images.rst index 17a6eb53..b2f0beb4 100644 --- a/docs/src/modules/bookmarks/images.rst +++ b/docs/src/modules/bookmarks/images.rst @@ -1,6 +1,6 @@ .. meta:: :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management images =================== diff --git a/docs/src/modules/bookmarks/importexport.rst b/docs/src/modules/bookmarks/importexport.rst index a3251f89..bf5a1620 100644 --- a/docs/src/modules/bookmarks/importexport.rst +++ b/docs/src/modules/bookmarks/importexport.rst @@ -1,6 +1,6 @@ .. meta:: :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management importexport =================== diff --git a/docs/src/modules/bookmarks/items/asset_items.rst b/docs/src/modules/bookmarks/items/asset_items.rst index dd442638..0018c85b 100644 --- a/docs/src/modules/bookmarks/items/asset_items.rst +++ b/docs/src/modules/bookmarks/items/asset_items.rst @@ -1,6 +1,6 @@ .. meta:: :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management items.asset_items ========================= diff --git a/docs/src/modules/bookmarks/items/bookmark_items.rst b/docs/src/modules/bookmarks/items/bookmark_items.rst index 71c75cd3..2cde6bfa 100644 --- a/docs/src/modules/bookmarks/items/bookmark_items.rst +++ b/docs/src/modules/bookmarks/items/bookmark_items.rst @@ -1,6 +1,6 @@ .. meta:: :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management items.bookmark_items ========================= diff --git a/docs/src/modules/bookmarks/items/delegate.rst b/docs/src/modules/bookmarks/items/delegate.rst index 9a3c94ba..75a1339d 100644 --- a/docs/src/modules/bookmarks/items/delegate.rst +++ b/docs/src/modules/bookmarks/items/delegate.rst @@ -1,6 +1,6 @@ .. meta:: :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management items.delegate ========================= diff --git a/docs/src/modules/bookmarks/items/favourite_items.rst b/docs/src/modules/bookmarks/items/favourite_items.rst index 805031fe..86c3a959 100644 --- a/docs/src/modules/bookmarks/items/favourite_items.rst +++ b/docs/src/modules/bookmarks/items/favourite_items.rst @@ -1,6 +1,6 @@ .. meta:: :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management items.favourite_items ========================= diff --git a/docs/src/modules/bookmarks/items/file_items.rst b/docs/src/modules/bookmarks/items/file_items.rst index d3812e63..55099b36 100644 --- a/docs/src/modules/bookmarks/items/file_items.rst +++ b/docs/src/modules/bookmarks/items/file_items.rst @@ -1,6 +1,6 @@ .. meta:: :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management items.file_items ========================= diff --git a/docs/src/modules/bookmarks/items/models.rst b/docs/src/modules/bookmarks/items/models.rst index 07ba55fd..57dcb30d 100644 --- a/docs/src/modules/bookmarks/items/models.rst +++ b/docs/src/modules/bookmarks/items/models.rst @@ -1,6 +1,6 @@ .. meta:: :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management items.models ========================= diff --git a/docs/src/modules/bookmarks/items/task_items.rst b/docs/src/modules/bookmarks/items/task_items.rst index e785c04a..c5da150c 100644 --- a/docs/src/modules/bookmarks/items/task_items.rst +++ b/docs/src/modules/bookmarks/items/task_items.rst @@ -1,6 +1,6 @@ .. meta:: :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management items.task_items ========================= diff --git a/docs/src/modules/bookmarks/items/views.rst b/docs/src/modules/bookmarks/items/views.rst index 09602515..cb2ad249 100644 --- a/docs/src/modules/bookmarks/items/views.rst +++ b/docs/src/modules/bookmarks/items/views.rst @@ -1,6 +1,6 @@ .. meta:: :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management items.views ========================= diff --git a/docs/src/modules/bookmarks/items/widgets/filter_editor.rst b/docs/src/modules/bookmarks/items/widgets/filter_editor.rst index a6e9f799..dec84b25 100644 --- a/docs/src/modules/bookmarks/items/widgets/filter_editor.rst +++ b/docs/src/modules/bookmarks/items/widgets/filter_editor.rst @@ -1,6 +1,6 @@ .. meta:: :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management items.widgets.filter_editor ================================= diff --git a/docs/src/modules/bookmarks/items/widgets/image_viewer.rst b/docs/src/modules/bookmarks/items/widgets/image_viewer.rst index 1f90b62a..e394305e 100644 --- a/docs/src/modules/bookmarks/items/widgets/image_viewer.rst +++ b/docs/src/modules/bookmarks/items/widgets/image_viewer.rst @@ -1,6 +1,6 @@ .. meta:: :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management items.widgets.image_viewer ================================= diff --git a/docs/src/modules/bookmarks/items/widgets/thumb_capture.rst b/docs/src/modules/bookmarks/items/widgets/thumb_capture.rst index fd9fe5fb..68b97e7f 100644 --- a/docs/src/modules/bookmarks/items/widgets/thumb_capture.rst +++ b/docs/src/modules/bookmarks/items/widgets/thumb_capture.rst @@ -1,6 +1,6 @@ .. meta:: :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management items.widgets.thumb_capture ================================= diff --git a/docs/src/modules/bookmarks/items/widgets/thumb_library.rst b/docs/src/modules/bookmarks/items/widgets/thumb_library.rst index 93f7fbb5..71fcebef 100644 --- a/docs/src/modules/bookmarks/items/widgets/thumb_library.rst +++ b/docs/src/modules/bookmarks/items/widgets/thumb_library.rst @@ -1,6 +1,6 @@ .. meta:: :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management items.widgets.thumb_library ================================= diff --git a/docs/src/modules/bookmarks/items/widgets/thumb_picker.rst b/docs/src/modules/bookmarks/items/widgets/thumb_picker.rst index bd65ddea..73e6dcd7 100644 --- a/docs/src/modules/bookmarks/items/widgets/thumb_picker.rst +++ b/docs/src/modules/bookmarks/items/widgets/thumb_picker.rst @@ -1,6 +1,6 @@ .. meta:: :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management items.widgets.thumb_picker ================================= diff --git a/docs/src/modules/bookmarks/launcher/gallery.rst b/docs/src/modules/bookmarks/launcher/gallery.rst index a4ff231f..78da3020 100644 --- a/docs/src/modules/bookmarks/launcher/gallery.rst +++ b/docs/src/modules/bookmarks/launcher/gallery.rst @@ -1,6 +1,6 @@ .. meta:: :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management launcher.gallery ============================ diff --git a/docs/src/modules/bookmarks/launcher/main.rst b/docs/src/modules/bookmarks/launcher/main.rst index 15cd4287..5bd195c1 100644 --- a/docs/src/modules/bookmarks/launcher/main.rst +++ b/docs/src/modules/bookmarks/launcher/main.rst @@ -1,6 +1,6 @@ .. meta:: :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management launcher.main ============================ diff --git a/docs/src/modules/bookmarks/log.rst b/docs/src/modules/bookmarks/log.rst index d56da216..21d497d6 100644 --- a/docs/src/modules/bookmarks/log.rst +++ b/docs/src/modules/bookmarks/log.rst @@ -1,6 +1,6 @@ .. meta:: :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management log =================== diff --git a/docs/src/modules/bookmarks/main.rst b/docs/src/modules/bookmarks/main.rst index ec7797ce..e01800bd 100644 --- a/docs/src/modules/bookmarks/main.rst +++ b/docs/src/modules/bookmarks/main.rst @@ -1,6 +1,6 @@ .. meta:: :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management main =================== diff --git a/docs/src/modules/bookmarks/maya/actions.rst b/docs/src/modules/bookmarks/maya/actions.rst index 5425c251..72ae6726 100644 --- a/docs/src/modules/bookmarks/maya/actions.rst +++ b/docs/src/modules/bookmarks/maya/actions.rst @@ -1,6 +1,6 @@ .. meta:: :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management maya.actions ======================== diff --git a/docs/src/modules/bookmarks/maya/base.rst b/docs/src/modules/bookmarks/maya/base.rst index ed29480c..b9a3b4fe 100644 --- a/docs/src/modules/bookmarks/maya/base.rst +++ b/docs/src/modules/bookmarks/maya/base.rst @@ -1,6 +1,6 @@ .. meta:: :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management maya.base ======================== diff --git a/docs/src/modules/bookmarks/maya/capture.rst b/docs/src/modules/bookmarks/maya/capture.rst index 9817c3ee..edbde978 100644 --- a/docs/src/modules/bookmarks/maya/capture.rst +++ b/docs/src/modules/bookmarks/maya/capture.rst @@ -1,6 +1,6 @@ .. meta:: :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management maya.capture ======================== diff --git a/docs/src/modules/bookmarks/maya/contextmenu.rst b/docs/src/modules/bookmarks/maya/contextmenu.rst index fe8d8a58..e2e3ce0f 100644 --- a/docs/src/modules/bookmarks/maya/contextmenu.rst +++ b/docs/src/modules/bookmarks/maya/contextmenu.rst @@ -1,6 +1,6 @@ .. meta:: :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management maya.contextmenu ======================== diff --git a/docs/src/modules/bookmarks/maya/export.rst b/docs/src/modules/bookmarks/maya/export.rst index a69014c6..31af8d5b 100644 --- a/docs/src/modules/bookmarks/maya/export.rst +++ b/docs/src/modules/bookmarks/maya/export.rst @@ -1,6 +1,6 @@ .. meta:: :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management maya.export ======================== diff --git a/docs/src/modules/bookmarks/maya/hud.rst b/docs/src/modules/bookmarks/maya/hud.rst index 9532f7ef..d89775c0 100644 --- a/docs/src/modules/bookmarks/maya/hud.rst +++ b/docs/src/modules/bookmarks/maya/hud.rst @@ -1,6 +1,6 @@ .. meta:: :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management maya.hud ======================== diff --git a/docs/src/modules/bookmarks/maya/main.rst b/docs/src/modules/bookmarks/maya/main.rst index 99ee3846..18fc6d0a 100644 --- a/docs/src/modules/bookmarks/maya/main.rst +++ b/docs/src/modules/bookmarks/maya/main.rst @@ -1,6 +1,6 @@ .. meta:: :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management maya.main ======================== diff --git a/docs/src/modules/bookmarks/maya/plugin.rst b/docs/src/modules/bookmarks/maya/plugin.rst index f38f6b39..1ad70488 100644 --- a/docs/src/modules/bookmarks/maya/plugin.rst +++ b/docs/src/modules/bookmarks/maya/plugin.rst @@ -1,6 +1,6 @@ .. meta:: :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management maya.plugin ======================== diff --git a/docs/src/modules/bookmarks/maya/scripts/aka_cloth_cache.rst b/docs/src/modules/bookmarks/maya/scripts/aka_cloth_cache.rst new file mode 100644 index 00000000..807c156d --- /dev/null +++ b/docs/src/modules/bookmarks/maya/scripts/aka_cloth_cache.rst @@ -0,0 +1,10 @@ +.. meta:: + :description: Developer documentation page for the Bookmarks python modules + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management + +maya.scripts.aka_cloth_cache +================================ + +.. automodule:: bookmarks.maya.scripts.aka_cloth_cache + :members: + :show-inheritance: diff --git a/docs/src/modules/bookmarks/maya/scripts/aka_cloth_cache_animated.rst b/docs/src/modules/bookmarks/maya/scripts/aka_cloth_cache_animated.rst new file mode 100644 index 00000000..2ae3400d --- /dev/null +++ b/docs/src/modules/bookmarks/maya/scripts/aka_cloth_cache_animated.rst @@ -0,0 +1,10 @@ +.. meta:: + :description: Developer documentation page for the Bookmarks python modules + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management + +maya.scripts.aka_cloth_cache_animated +================================ + +.. automodule:: bookmarks.maya.scripts.aka_cloth_cache_animated + :members: + :show-inheritance: diff --git a/docs/src/modules/bookmarks/maya/scripts/aka_make_export_sets.rst b/docs/src/modules/bookmarks/maya/scripts/aka_make_export_sets.rst new file mode 100644 index 00000000..6ccb7799 --- /dev/null +++ b/docs/src/modules/bookmarks/maya/scripts/aka_make_export_sets.rst @@ -0,0 +1,10 @@ +.. meta:: + :description: Developer documentation page for the Bookmarks python modules + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management + +maya.scripts.aka_make_export_sets +================================ + +.. automodule:: bookmarks.maya.scripts.aka_make_export_sets + :members: + :show-inheritance: diff --git a/docs/src/modules/bookmarks/maya/scripts/aka_odyssey_shaders.rst b/docs/src/modules/bookmarks/maya/scripts/aka_odyssey_shaders.rst deleted file mode 100644 index 8dea41ec..00000000 --- a/docs/src/modules/bookmarks/maya/scripts/aka_odyssey_shaders.rst +++ /dev/null @@ -1,10 +0,0 @@ -.. meta:: - :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO - -maya.scripts.aka_odyssey_shaders -================================ - -.. automodule:: bookmarks.maya.scripts.aka_odyssey_shaders - :members: - :show-inheritance: diff --git a/docs/src/modules/bookmarks/maya/scripts/aka_shader_templates.rst b/docs/src/modules/bookmarks/maya/scripts/aka_shader_templates.rst new file mode 100644 index 00000000..11ead732 --- /dev/null +++ b/docs/src/modules/bookmarks/maya/scripts/aka_shader_templates.rst @@ -0,0 +1,10 @@ +.. meta:: + :description: Developer documentation page for the Bookmarks python modules + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management + +maya.scripts.aka_shader_templates +================================ + +.. automodule:: bookmarks.maya.scripts.aka_shader_templates + :members: + :show-inheritance: diff --git a/docs/src/modules/bookmarks/maya/scripts/reset_joint_orientations.rst b/docs/src/modules/bookmarks/maya/scripts/reset_joint_orientations.rst new file mode 100644 index 00000000..f721c883 --- /dev/null +++ b/docs/src/modules/bookmarks/maya/scripts/reset_joint_orientations.rst @@ -0,0 +1,10 @@ +.. meta:: + :description: Developer documentation page for the Bookmarks python modules + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management + +maya.scripts.reset_joint_orientations +================================ + +.. automodule:: bookmarks.maya.scripts.reset_joint_orientations + :members: + :show-inheritance: diff --git a/docs/src/modules/bookmarks/maya/shadertool.rst b/docs/src/modules/bookmarks/maya/shadertool.rst deleted file mode 100644 index 5fcc4531..00000000 --- a/docs/src/modules/bookmarks/maya/shadertool.rst +++ /dev/null @@ -1,10 +0,0 @@ -.. meta:: - :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO - -maya.shadertool -======================== - -.. automodule:: bookmarks.maya.shadertool - :members: - :show-inheritance: diff --git a/docs/src/modules/bookmarks/maya/viewport.rst b/docs/src/modules/bookmarks/maya/viewport.rst index 661d4baa..16d54eaf 100644 --- a/docs/src/modules/bookmarks/maya/viewport.rst +++ b/docs/src/modules/bookmarks/maya/viewport.rst @@ -1,6 +1,6 @@ .. meta:: :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management maya.viewport ======================== diff --git a/docs/src/modules/bookmarks/notes.rst b/docs/src/modules/bookmarks/notes.rst index faa53129..2841949a 100644 --- a/docs/src/modules/bookmarks/notes.rst +++ b/docs/src/modules/bookmarks/notes.rst @@ -1,6 +1,6 @@ .. meta:: :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management notes =================== diff --git a/docs/src/modules/bookmarks/progress.rst b/docs/src/modules/bookmarks/progress.rst index 6b9701e3..f0d7ded2 100644 --- a/docs/src/modules/bookmarks/progress.rst +++ b/docs/src/modules/bookmarks/progress.rst @@ -1,6 +1,6 @@ .. meta:: :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management progress =================== diff --git a/docs/src/modules/bookmarks/publish.rst b/docs/src/modules/bookmarks/publish.rst index 69f1333e..b2aea3fd 100644 --- a/docs/src/modules/bookmarks/publish.rst +++ b/docs/src/modules/bookmarks/publish.rst @@ -1,6 +1,6 @@ .. meta:: :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management publish =================== diff --git a/docs/src/modules/bookmarks/scripts/aka_odyssey_sync.rst b/docs/src/modules/bookmarks/scripts/aka_odyssey_sync.rst deleted file mode 100644 index e75f2b8a..00000000 --- a/docs/src/modules/bookmarks/scripts/aka_odyssey_sync.rst +++ /dev/null @@ -1,10 +0,0 @@ -.. meta:: - :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO - -scripts.aka_odyssey_sync -=========================== - -.. automodule:: bookmarks.scripts.aka_odyssey_sync - :members: - :show-inheritance: diff --git a/docs/src/modules/bookmarks/scripts/clips.rst b/docs/src/modules/bookmarks/scripts/clips.rst new file mode 100644 index 00000000..e6fb97bf --- /dev/null +++ b/docs/src/modules/bookmarks/scripts/clips.rst @@ -0,0 +1,10 @@ +.. meta:: + :description: Developer documentation page for the Bookmarks python modules + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management + +scripts.clips +=========================== + +.. automodule:: bookmarks.scripts.clips + :members: + :show-inheritance: diff --git a/docs/src/modules/bookmarks/scripts/send_images_to_after_effects.rst b/docs/src/modules/bookmarks/scripts/send_images_to_after_effects.rst new file mode 100644 index 00000000..929f60ae --- /dev/null +++ b/docs/src/modules/bookmarks/scripts/send_images_to_after_effects.rst @@ -0,0 +1,10 @@ +.. meta:: + :description: Developer documentation page for the Bookmarks python modules + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management + +scripts.send_images_to_after_effects +=========================== + +.. automodule:: bookmarks.scripts.send_images_to_after_effects + :members: + :show-inheritance: diff --git a/docs/src/modules/bookmarks/scripts/sync_asset_data.rst b/docs/src/modules/bookmarks/scripts/sync_asset_data.rst new file mode 100644 index 00000000..f20f8c80 --- /dev/null +++ b/docs/src/modules/bookmarks/scripts/sync_asset_data.rst @@ -0,0 +1,10 @@ +.. meta:: + :description: Developer documentation page for the Bookmarks python modules + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management + +scripts.sync_asset_data +=========================== + +.. automodule:: bookmarks.scripts.sync_asset_data + :members: + :show-inheritance: diff --git a/docs/src/modules/bookmarks/shortcuts.rst b/docs/src/modules/bookmarks/shortcuts.rst index 174e9981..fd17c4cb 100644 --- a/docs/src/modules/bookmarks/shortcuts.rst +++ b/docs/src/modules/bookmarks/shortcuts.rst @@ -1,6 +1,6 @@ .. meta:: :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management shortcuts =================== diff --git a/docs/src/modules/bookmarks/shotgun/actions.rst b/docs/src/modules/bookmarks/shotgun/actions.rst index ef70d2b3..d74df640 100644 --- a/docs/src/modules/bookmarks/shotgun/actions.rst +++ b/docs/src/modules/bookmarks/shotgun/actions.rst @@ -1,6 +1,6 @@ .. meta:: :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management shotgun.actions =========================== diff --git a/docs/src/modules/bookmarks/shotgun/link.rst b/docs/src/modules/bookmarks/shotgun/link.rst index bddfb869..12223690 100644 --- a/docs/src/modules/bookmarks/shotgun/link.rst +++ b/docs/src/modules/bookmarks/shotgun/link.rst @@ -1,6 +1,6 @@ .. meta:: :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management shotgun.link =========================== diff --git a/docs/src/modules/bookmarks/shotgun/link_asset.rst b/docs/src/modules/bookmarks/shotgun/link_asset.rst index 08639030..b0df44e3 100644 --- a/docs/src/modules/bookmarks/shotgun/link_asset.rst +++ b/docs/src/modules/bookmarks/shotgun/link_asset.rst @@ -1,6 +1,6 @@ .. meta:: :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management shotgun.link_asset =========================== diff --git a/docs/src/modules/bookmarks/shotgun/link_assets.rst b/docs/src/modules/bookmarks/shotgun/link_assets.rst index c4566a91..9d52ab79 100644 --- a/docs/src/modules/bookmarks/shotgun/link_assets.rst +++ b/docs/src/modules/bookmarks/shotgun/link_assets.rst @@ -1,6 +1,6 @@ .. meta:: :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management shotgun.link_assets =========================== diff --git a/docs/src/modules/bookmarks/shotgun/link_bookmark.rst b/docs/src/modules/bookmarks/shotgun/link_bookmark.rst index 53f3dbf0..cf0888df 100644 --- a/docs/src/modules/bookmarks/shotgun/link_bookmark.rst +++ b/docs/src/modules/bookmarks/shotgun/link_bookmark.rst @@ -1,6 +1,6 @@ .. meta:: :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management shotgun.link_bookmark =========================== diff --git a/docs/src/modules/bookmarks/shotgun/publish.rst b/docs/src/modules/bookmarks/shotgun/publish.rst deleted file mode 100644 index a6687276..00000000 --- a/docs/src/modules/bookmarks/shotgun/publish.rst +++ /dev/null @@ -1,10 +0,0 @@ -.. meta:: - :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO - -shotgun.publish -=========================== - -.. automodule:: bookmarks.shotgun.publish - :members: - :show-inheritance: diff --git a/docs/src/modules/bookmarks/shotgun/publish_widgets.rst b/docs/src/modules/bookmarks/shotgun/publish_widgets.rst index 4000557f..d1340f76 100644 --- a/docs/src/modules/bookmarks/shotgun/publish_widgets.rst +++ b/docs/src/modules/bookmarks/shotgun/publish_widgets.rst @@ -1,6 +1,6 @@ .. meta:: :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management shotgun.publish_widgets =========================== diff --git a/docs/src/modules/bookmarks/shotgun/sg_publish_clip.rst b/docs/src/modules/bookmarks/shotgun/sg_publish_clip.rst new file mode 100644 index 00000000..1125bcce --- /dev/null +++ b/docs/src/modules/bookmarks/shotgun/sg_publish_clip.rst @@ -0,0 +1,10 @@ +.. meta:: + :description: Developer documentation page for the Bookmarks python modules + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management + +shotgun.sg_publish_clip +=========================== + +.. automodule:: bookmarks.shotgun.sg_publish_clip + :members: + :show-inheritance: diff --git a/docs/src/modules/bookmarks/shotgun/shotgun.rst b/docs/src/modules/bookmarks/shotgun/shotgun.rst index c8ceb107..ad0f6d9c 100644 --- a/docs/src/modules/bookmarks/shotgun/shotgun.rst +++ b/docs/src/modules/bookmarks/shotgun/shotgun.rst @@ -1,6 +1,6 @@ .. meta:: :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management shotgun.shotgun =========================== diff --git a/docs/src/modules/bookmarks/shotgun/tasks.rst b/docs/src/modules/bookmarks/shotgun/tasks.rst index 3a0068b2..40f9f030 100644 --- a/docs/src/modules/bookmarks/shotgun/tasks.rst +++ b/docs/src/modules/bookmarks/shotgun/tasks.rst @@ -1,6 +1,6 @@ .. meta:: :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management shotgun.tasks =========================== diff --git a/docs/src/modules/bookmarks/standalone.rst b/docs/src/modules/bookmarks/standalone.rst index 68ce76ea..8606f07e 100644 --- a/docs/src/modules/bookmarks/standalone.rst +++ b/docs/src/modules/bookmarks/standalone.rst @@ -1,6 +1,6 @@ .. meta:: :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management standalone =================== diff --git a/docs/src/modules/bookmarks/statusbar.rst b/docs/src/modules/bookmarks/statusbar.rst index 553db236..6e7b3a67 100644 --- a/docs/src/modules/bookmarks/statusbar.rst +++ b/docs/src/modules/bookmarks/statusbar.rst @@ -1,6 +1,6 @@ .. meta:: :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management statusbar =================== diff --git a/docs/src/modules/bookmarks/templates.rst b/docs/src/modules/bookmarks/templates.rst index 035ed09b..a41f7b82 100644 --- a/docs/src/modules/bookmarks/templates.rst +++ b/docs/src/modules/bookmarks/templates.rst @@ -1,6 +1,6 @@ .. meta:: :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management templates =================== diff --git a/docs/src/modules/bookmarks/threads/threads.rst b/docs/src/modules/bookmarks/threads/threads.rst index 27e34736..b808bec9 100644 --- a/docs/src/modules/bookmarks/threads/threads.rst +++ b/docs/src/modules/bookmarks/threads/threads.rst @@ -1,6 +1,6 @@ .. meta:: :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management threads.threads =========================== diff --git a/docs/src/modules/bookmarks/threads/workers.rst b/docs/src/modules/bookmarks/threads/workers.rst index 6def0df8..63f26598 100644 --- a/docs/src/modules/bookmarks/threads/workers.rst +++ b/docs/src/modules/bookmarks/threads/workers.rst @@ -1,6 +1,6 @@ .. meta:: :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management threads.workers =========================== diff --git a/docs/src/modules/bookmarks/tokens/tokens.rst b/docs/src/modules/bookmarks/tokens/tokens.rst index 568ac98b..9e71be15 100644 --- a/docs/src/modules/bookmarks/tokens/tokens.rst +++ b/docs/src/modules/bookmarks/tokens/tokens.rst @@ -1,6 +1,6 @@ .. meta:: :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management tokens.tokens ========================== diff --git a/docs/src/modules/bookmarks/tokens/tokens_editor.rst b/docs/src/modules/bookmarks/tokens/tokens_editor.rst index 47a086cf..89376292 100644 --- a/docs/src/modules/bookmarks/tokens/tokens_editor.rst +++ b/docs/src/modules/bookmarks/tokens/tokens_editor.rst @@ -1,6 +1,6 @@ .. meta:: :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management tokens.tokens_editor ========================== diff --git a/docs/src/modules/bookmarks/topbar/buttons.rst b/docs/src/modules/bookmarks/topbar/buttons.rst index 58376b85..0c123b9e 100644 --- a/docs/src/modules/bookmarks/topbar/buttons.rst +++ b/docs/src/modules/bookmarks/topbar/buttons.rst @@ -1,6 +1,6 @@ .. meta:: :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management topbar.buttons ========================== diff --git a/docs/src/modules/bookmarks/topbar/filters.rst b/docs/src/modules/bookmarks/topbar/filters.rst new file mode 100644 index 00000000..c6eee044 --- /dev/null +++ b/docs/src/modules/bookmarks/topbar/filters.rst @@ -0,0 +1,10 @@ +.. meta:: + :description: Developer documentation page for the Bookmarks python modules + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management + +topbar.filters +========================== + +.. automodule:: bookmarks.topbar.filters + :members: + :show-inheritance: diff --git a/docs/src/modules/bookmarks/topbar/quickswitch.rst b/docs/src/modules/bookmarks/topbar/quickswitch.rst index 2b0ebc8b..1dae3afe 100644 --- a/docs/src/modules/bookmarks/topbar/quickswitch.rst +++ b/docs/src/modules/bookmarks/topbar/quickswitch.rst @@ -1,6 +1,6 @@ .. meta:: :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management topbar.quickswitch ========================== diff --git a/docs/src/modules/bookmarks/topbar/sgfilter.rst b/docs/src/modules/bookmarks/topbar/sgfilter.rst new file mode 100644 index 00000000..dc89a4ae --- /dev/null +++ b/docs/src/modules/bookmarks/topbar/sgfilter.rst @@ -0,0 +1,10 @@ +.. meta:: + :description: Developer documentation page for the Bookmarks python modules + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management + +topbar.sgfilter +========================== + +.. automodule:: bookmarks.topbar.sgfilter + :members: + :show-inheritance: diff --git a/docs/src/modules/bookmarks/topbar/tabs.rst b/docs/src/modules/bookmarks/topbar/tabs.rst index c4571593..86fb935f 100644 --- a/docs/src/modules/bookmarks/topbar/tabs.rst +++ b/docs/src/modules/bookmarks/topbar/tabs.rst @@ -1,6 +1,6 @@ .. meta:: :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management topbar.tabs ========================== diff --git a/docs/src/modules/bookmarks/topbar/topbar.rst b/docs/src/modules/bookmarks/topbar/topbar.rst index 5a75d8e7..0f11ab37 100644 --- a/docs/src/modules/bookmarks/topbar/topbar.rst +++ b/docs/src/modules/bookmarks/topbar/topbar.rst @@ -1,6 +1,6 @@ .. meta:: :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management topbar.topbar ========================== diff --git a/docs/src/modules/bookmarks/ui.rst b/docs/src/modules/bookmarks/ui.rst index d662b758..9228a5f7 100644 --- a/docs/src/modules/bookmarks/ui.rst +++ b/docs/src/modules/bookmarks/ui.rst @@ -1,6 +1,6 @@ .. meta:: :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management ui =================== diff --git a/docs/src/modules/bookmarks/versioncontrol/version.rst b/docs/src/modules/bookmarks/versioncontrol/version.rst index d171e9cf..5c59acc8 100644 --- a/docs/src/modules/bookmarks/versioncontrol/version.rst +++ b/docs/src/modules/bookmarks/versioncontrol/version.rst @@ -1,6 +1,6 @@ .. meta:: :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management versioncontrol.version ================================== diff --git a/docs/src/modules/bookmarks/versioncontrol/versioncontrol.rst b/docs/src/modules/bookmarks/versioncontrol/versioncontrol.rst index 87c452d0..5bcbadcd 100644 --- a/docs/src/modules/bookmarks/versioncontrol/versioncontrol.rst +++ b/docs/src/modules/bookmarks/versioncontrol/versioncontrol.rst @@ -1,6 +1,6 @@ .. meta:: :description: Developer documentation page for the Bookmarks python modules - :keywords: Bookmarks, bookmarksvfx, asset manager, assets, PySide, Qt5, PySide2, Python, vfx, animation, film, productivity, free, open-source, opensource, lightweight, ShotGrid, RV, FFMpeg, ffmpeg, publish, manage, digital content management, production, OpenImageIO + :keywords: Bookmarks, bookmarksvfx, pipeline, pipe, asset manager, assets, PySide, Qt, PySide,Python, vfx, animation, film, production, open-source, opensource, ShotGun, ShotGrid, RV, ffmpeg, openimageio, publish, manage, digital content management versioncontrol.versioncontrol ================================== From 2b5abf85884a986d2443efdd216507b5f0859c9c Mon Sep 17 00:00:00 2001 From: Gergely Wootsch Date: Mon, 23 Oct 2023 10:22:45 +0200 Subject: [PATCH 17/32] update version --- docs/src/conf.py | 2 +- docs/src/guide.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/src/conf.py b/docs/src/conf.py index 3901e7c1..5de04469 100644 --- a/docs/src/conf.py +++ b/docs/src/conf.py @@ -33,7 +33,7 @@ author = 'Gergely Wootsch' # The full version, including alpha/beta/rc tags -release = '0.8.6' +release = '0.8.9' html_baseurl = 'https://bookmarks-vfx.com' html_extra_path = [ diff --git a/docs/src/guide.rst b/docs/src/guide.rst index 8d95811b..20b85f87 100644 --- a/docs/src/guide.rst +++ b/docs/src/guide.rst @@ -25,7 +25,7 @@ Get Bookmarks The project is hosted on `Github `_. -.. admonition:: Download the latest Windows release: `Bookmarks v0.8.6 `_ +.. admonition:: Download the latest Windows release: `Bookmarks v0.8.9 `_ ☹ Currently, Bookmarks only supports Windows. From a3100209a999d86a06eec8023acac1432c6f106e Mon Sep 17 00:00:00 2001 From: Gergely Wootsch Date: Wed, 25 Oct 2023 14:15:19 +0200 Subject: [PATCH 18/32] Update active mode to respect environment values I introduced recently environment values for subprocesses launched via Bookmarks. ``BOOKMARKS_ACTIVE_SERVER``, ``BOOKMARKS_ACTIVE_JOB``, ``BOOKMARKS_ACTIVE_ROOT``, ``BOOKMARKS_ACTIVE_ASSET`` and ``BOOKMARKS_ACTIVE_TASK`` will be used to set the default active paths in any subsequent Bookmarks session and Bookmarks will automatically be put in private active path mode when any of these environments are set. --- .../{session_lock.py => active_mode.py} | 39 +++- bookmarks/topbar/sgfilter.py | 195 ------------------ 2 files changed, 31 insertions(+), 203 deletions(-) rename bookmarks/common/{session_lock.py => active_mode.py} (69%) delete mode 100644 bookmarks/topbar/sgfilter.py diff --git a/bookmarks/common/session_lock.py b/bookmarks/common/active_mode.py similarity index 69% rename from bookmarks/common/session_lock.py rename to bookmarks/common/active_mode.py index d7ad91cd..1307dc23 100644 --- a/bookmarks/common/session_lock.py +++ b/bookmarks/common/active_mode.py @@ -72,23 +72,46 @@ def prune_lock(): raise RuntimeError('Failed to remove a lockfile.') -def init_lock(): - """Initialises the Bookmark's session lock. +def init_active_mode(): + """Initialises the Bookmark's active path reading mode. + + We define two modes, ``SynchronisedActivePaths`` (when Bookmarks is in sync with the user settings) and + ``PrivateActivePaths`` when the Bookmarks sessions set the active paths values internally without changing the user + settings. + + The session mode can be toggled via the ui (there's a button in the lower right hand corner) and will be initialised + to a default value based on the following conditions: + + If any of the `BOOKMARKS_ACTIVE_SERVER`, `BOOKMARKS_ACTIVE_JOB`, `BOOKMARKS_ACTIVE_ROOT`, `BOOKMARKS_ACTIVE_ASSET` + and `BOOKMARKS_ACTIVE_TASK` environment values have valid values, the session will automatically be marked + ``PrivateActivePaths``. + + If the environment has not been set but there's already an active ``SynchronisedActivePaths`` session running the + current session will be set to ``PrivateActivePaths``. + + Any sessions that doesn't have environment values set and does not find synchronized session lock files will be + marked ``SynchronisedActivePaths``. - We'll check all lock-files and to see if there's already a - ``SynchronisedActivePaths`` session. As we want only one session controlling - the active path settings we'll set all subsequent application sessions - to be ``PrivateActivePaths`` (when ``PrivateActivePaths`` is on, all active path - settings will be kept in memory, instead of writing them out to the - disk). """ + # Check if any of the environment variables are set + _env_active_server = os.environ.get('BOOKMARKS_ACTIVE_SERVER', None) + _env_active_job = os.environ.get('BOOKMARKS_ACTIVE_JOB', None) + _env_active_root = os.environ.get('BOOKMARKS_ACTIVE_ROOT', None) + _env_active_asset = os.environ.get('BOOKMARKS_ACTIVE_ASSET', None) + _env_active_task = os.environ.get('BOOKMARKS_ACTIVE_TASK', None) + + if any((_env_active_server, _env_active_job, _env_active_root, _env_active_asset, _env_active_task)): + common.active_mode = common.PrivateActivePaths + return write_current_mode_to_lock() + path = LOCK_DIR.format( root=QtCore.QStandardPaths.writableLocation( QtCore.QStandardPaths.GenericDataLocation ), product=common.product, ) + # Iterate over all lock files and check their contents for entry in os.scandir(path): if entry.is_dir(): diff --git a/bookmarks/topbar/sgfilter.py b/bookmarks/topbar/sgfilter.py deleted file mode 100644 index 57a71726..00000000 --- a/bookmarks/topbar/sgfilter.py +++ /dev/null @@ -1,195 +0,0 @@ -"""""" -import weakref - -from PySide2 import QtCore, QtGui, QtWidgets - - -from .. import common -from .. import ui - - -class BaseFilterModel(ui.AbstractListModel): - - def __init__(self, section_name_label, data_source, parent=None): - - self.show_all_label = ' - Show All -' - self.section_name_label = section_name_label - self.data_source = data_source - - super().__init__(parent=parent) - - common.signals.internalDataReady.connect(self.internal_data_ready) - common.signals.bookmarksChanged.connect(self.reset_data) - - @QtCore.Slot(weakref.ref) - def internal_data_ready(self, ref): - if not ref(): - return - - source_model = common.source_model(common.AssetTab) - data = common.get_data( - source_model.source_path(), - source_model.task(), - source_model.data_type() - ) - - if ref() != data: - return - - self.reset_data() - - def reset_data(self): - self.beginResetModel() - self.init_data() - self.endResetModel() - - def init_data(self, *args, **kwargs): - """Initializes data. - - """ - self._data = common.DataDict() - - source_model = common.source_model(common.AssetTab) - - data = common.get_data( - source_model.source_path(), - source_model.task(), - source_model.data_type() - ) - if not getattr(data, self.data_source): - return - - self._data[len(self._data)] = { - QtCore.Qt.DisplayRole: self.section_name_label, - QtCore.Qt.SizeHintRole: QtCore.QSize(1, common.size(common.size_row_height) * 0.66), - common.FlagsRole: QtCore.Qt.NoItemFlags, - QtCore.Qt.TextAlignmentRole: QtCore.Qt.AlignCenter, - } - - self._data[len(self._data)] = { - QtCore.Qt.DisplayRole: self.show_all_label, - QtCore.Qt.SizeHintRole: self.row_size, - QtCore.Qt.TextAlignmentRole: QtCore.Qt.AlignCenter, - } - - icon = ui.get_icon('sg') - - for task in sorted(getattr(data, self.data_source)): - self._data[len(self._data)] = { - QtCore.Qt.DisplayRole: task, - QtCore.Qt.SizeHintRole: self.row_size, - QtCore.Qt.DecorationRole: icon, - QtCore.Qt.StatusTipRole: task, - QtCore.Qt.AccessibleDescriptionRole: task, - QtCore.Qt.WhatsThisRole: task, - QtCore.Qt.ToolTipRole: task, - QtCore.Qt.TextAlignmentRole: QtCore.Qt.AlignCenter, - } - - -class BaseFilterButton(QtWidgets.QComboBox): - """The combo box used to set a text filter based on the available ShotGrid task names. - - """ - def __init__(self, Model, parent=None): - super().__init__(parent=parent) - self.setView(QtWidgets.QListView(parent=self)) - self.view().setMinimumWidth(int(common.size(common.size_width) * 0.66)) - self.setModel(Model()) - - self.setFixedHeight(common.size(common.size_margin)) - self.setMinimumWidth(common.size(common.size_margin) * 6) - - common.signals.updateTopBarButtons.connect(lambda: self.setHidden(not common.current_tab() == common.AssetTab)) - common.model(common.AssetTab).filterTextChanged.connect(self.select_text) - common.signals.internalDataReady.connect(self.select_text) - - self.textActivated.connect(self.update_filter_text) - - @QtCore.Slot(str) - def update_filter_text(self, text): - """Update the filter text. - - Args: - text (str): The text to set as the filter text. - - """ - if text == self.model().show_all_label: - text = '' - else: - text = f'"{text.lower().strip()}"' - - common.model(common.AssetTab).set_filter_text(text) - - @QtCore.Slot() - def select_text(self, *args, **kwargs): - """Update the filter text. - - """ - self.setCurrentIndex(-1) - - _text = common.model(common.AssetTab).filter_text() - _text = _text.lower().strip() if _text else '' - - if not _text: - return - - for i in range(self.count()): - text = self.itemText(i) - if not text: - continue - if text == self.model().show_all_label: - continue - if text == self.model().section_name_label: - continue - - text = text.lower().strip() - if _text == text: - self.setCurrentIndex(i) - return - - if _text == f'"{text}"': - self.setCurrentIndex(i) - return - - -class TaskFilterModel(BaseFilterModel): - - def __init__(self, parent=None): - super().__init__( - 'ShotGrid Tasks', - 'sg_task_names', - parent=parent - ) - - -class TaskFilterButton(BaseFilterButton): - """The combo box used to set a text filter based on the available ShotGrid task names. - - """ - def __init__(self, parent=None): - super().__init__( - TaskFilterModel, - parent=parent - ) - - -class EntityFilterModel(BaseFilterModel): - - def __init__(self, parent=None): - super().__init__( - 'ShotGrid Entities', - 'shotgun_names', - parent=parent - ) - - -class EntityFilterButton(BaseFilterButton): - """The combo box used to set a text filter based on the available ShotGrid task names. - - """ - def __init__(self, parent=None): - super().__init__( - EntityFilterModel, - parent=parent - ) \ No newline at end of file From 337daf2b54b822ed0990692124e9085d7f63fd98 Mon Sep 17 00:00:00 2001 From: Gergely Wootsch Date: Wed, 25 Oct 2023 14:27:33 +0200 Subject: [PATCH 19/32] Update active mode to respect environment values I introduced recently environment values for subprocesses launched via Bookmarks. ``BOOKMARKS_ACTIVE_SERVER``, ``BOOKMARKS_ACTIVE_JOB``, ``BOOKMARKS_ACTIVE_ROOT``, ``BOOKMARKS_ACTIVE_ASSET`` and ``BOOKMARKS_ACTIVE_TASK`` will be used to set the default active paths in any subsequent Bookmarks session and Bookmarks will automatically be put in private active path mode when any of these environments are set. --- README.md | 2 +- bookmarks/__init__.py | 4 +-- bookmarks/common/__init__.py | 2 +- bookmarks/common/active_mode.py | 42 ++++++++++++++--------------- bookmarks/common/settings.py | 48 ++++++++++++++++++++++----------- bookmarks/common/setup.py | 22 +++++++++++++-- bookmarks/common/signals.py | 9 ++++--- bookmarks/database.py | 2 +- bookmarks/items/models.py | 4 --- bookmarks/maya/main.py | 2 +- bookmarks/maya/plugin.py | 2 +- bookmarks/threads/workers.py | 9 ++----- bookmarks/topbar/filters.py | 1 + package/CMakeLists.txt | 2 +- 14 files changed, 89 insertions(+), 62 deletions(-) diff --git a/README.md b/README.md index b91664b0..fa2c76cb 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ - +

diff --git a/bookmarks/__init__.py b/bookmarks/__init__.py index fc469554..a4817da4 100644 --- a/bookmarks/__init__.py +++ b/bookmarks/__init__.py @@ -76,7 +76,7 @@ .. |label3| image:: https://img.shields.io/badge/Platform-Windows-lightgrey :height: 18 -.. |label4| image:: https://img.shields.io/badge/Version-v0.8.6-green +.. |label4| image:: https://img.shields.io/badge/Version-v0.8.9-green :height: 18 .. |image1| image:: ./images/active_bookmark.png @@ -100,7 +100,7 @@ __email__ = 'hello@gergely-wootsch.com' #: Project version -__version__ = '0.8.6' +__version__ = '0.8.9' #: Project version __version_info__ = __version__.split('.') diff --git a/bookmarks/common/__init__.py b/bookmarks/common/__init__.py index 10aff7ff..b0e8d2b4 100644 --- a/bookmarks/common/__init__.py +++ b/bookmarks/common/__init__.py @@ -123,7 +123,7 @@ from .font import * from .monitor import * from .sequence import * -from .session_lock import * +from .active_mode import * from .settings import * from .setup import * from .signals import * diff --git a/bookmarks/common/active_mode.py b/bookmarks/common/active_mode.py index 1307dc23..15ededab 100644 --- a/bookmarks/common/active_mode.py +++ b/bookmarks/common/active_mode.py @@ -1,5 +1,4 @@ -"""Module defines the classes and methods needed to set and edit session lock -files. +"""Defines active path reading mode. The app has two session modes. When `common.active_mode` is `common.SynchronisedActivePaths`, the app will save active paths in the user settings @@ -11,6 +10,11 @@ active path values are read from the user settings, but active paths changes won't be saved to the settings file. +The session mode will also be set to `common.PrivateActivePaths` if any of the +`BOOKMARKS_ACTIVE_SERVER`, `BOOKMARKS_ACTIVE_JOB`, `BOOKMARKS_ACTIVE_ROOT`, +`BOOKMARKS_ACTIVE_ASSET` and `BOOKMARKS_ACTIVE_TASK` environment variables are set +as these will take precedence over the user settings. + To toggle between the two modes use :func:`bookmarks.actions.toggle_active_mode`. Also see :class:`bookmarks.statusbar.ToggleSessionModeButton`. @@ -34,11 +38,7 @@ def get_lock_path(): return LOCK_PATH.format( root=QtCore.QStandardPaths.writableLocation( QtCore.QStandardPaths.GenericDataLocation - ), - product=common.product, - prefix=PREFIX, - pid=os.getpid(), - ext=FORMAT + ), product=common.product, prefix=PREFIX, pid=os.getpid(), ext=FORMAT ) @@ -49,9 +49,7 @@ def prune_lock(): path = LOCK_DIR.format( root=QtCore.QStandardPaths.writableLocation( QtCore.QStandardPaths.GenericDataLocation - ), - product=common.product, - ) + ), product=common.product, ) r = fr'{PREFIX}_([0-9]+)\.{FORMAT}' pids = psutil.pids() @@ -79,21 +77,23 @@ def init_active_mode(): ``PrivateActivePaths`` when the Bookmarks sessions set the active paths values internally without changing the user settings. - The session mode can be toggled via the ui (there's a button in the lower right hand corner) and will be initialised - to a default value based on the following conditions: + The session mode will be initialised to a default value based on the following conditions: - If any of the `BOOKMARKS_ACTIVE_SERVER`, `BOOKMARKS_ACTIVE_JOB`, `BOOKMARKS_ACTIVE_ROOT`, `BOOKMARKS_ACTIVE_ASSET` - and `BOOKMARKS_ACTIVE_TASK` environment values have valid values, the session will automatically be marked - ``PrivateActivePaths``. + If any of the `BOOKMARKS_ACTIVE_SERVER`, `BOOKMARKS_ACTIVE_JOB`, `BOOKMARKS_ACTIVE_ROOT`, + `BOOKMARKS_ACTIVE_ASSET` and `BOOKMARKS_ACTIVE_TASK` environment values have valid values, the session will + automatically be marked ``PrivateActivePaths``. - If the environment has not been set but there's already an active ``SynchronisedActivePaths`` session running the - current session will be set to ``PrivateActivePaths``. + If the environment has not been set but there's already an active ``SynchronisedActivePaths`` session + running, the current session will be set to ``PrivateActivePaths``. - Any sessions that doesn't have environment values set and does not find synchronized session lock files will be - marked ``SynchronisedActivePaths``. + Any sessions that doesn't have environment values set and does not find synchronized session lock files will + be marked ``SynchronisedActivePaths``. """ + # Remove stale lock files + prune_lock() + # Check if any of the environment variables are set _env_active_server = os.environ.get('BOOKMARKS_ACTIVE_SERVER', None) _env_active_job = os.environ.get('BOOKMARKS_ACTIVE_JOB', None) @@ -108,9 +108,7 @@ def init_active_mode(): path = LOCK_DIR.format( root=QtCore.QStandardPaths.writableLocation( QtCore.QStandardPaths.GenericDataLocation - ), - product=common.product, - ) + ), product=common.product, ) # Iterate over all lock files and check their contents for entry in os.scandir(path): diff --git a/bookmarks/common/settings.py b/bookmarks/common/settings.py index ee5cc03b..b5957298 100644 --- a/bookmarks/common/settings.py +++ b/bookmarks/common/settings.py @@ -140,9 +140,10 @@ def init_settings(): for key in SECTIONS['active']: common.active_paths[mode][key] = None - # Create the setting object, this will load the previously saved active - # paths from the ini file. + # Initialize the user settings instance common.settings = UserSettings() + + # Load values from the user settings file common.settings.load_active_values() common.update_private_values() @@ -155,6 +156,7 @@ def init_settings(): if not v or not isinstance(v, dict): v = {} common.favourites = v + common.signals.favouritesChanged.emit() _init_bookmarks() @@ -306,12 +308,26 @@ def bookmark_key(server, job, root): def update_private_values(): - """Copy the ``SynchronisedActivePaths`` values to ``PrivateActivePaths``. + """Copy the controlling values to ``PrivateActivePaths``. + + The source of the value is determined by the active mode and the current environment. """ + _env_active_server = os.environ.get('BOOKMARKS_ACTIVE_SERVER', None) + _env_active_job = os.environ.get('BOOKMARKS_ACTIVE_JOB', None) + _env_active_root = os.environ.get('BOOKMARKS_ACTIVE_ROOT', None) + _env_active_asset = os.environ.get('BOOKMARKS_ACTIVE_ASSET', None) + _env_active_task = os.environ.get('BOOKMARKS_ACTIVE_TASK', None) + + if any((_env_active_server, _env_active_job, _env_active_root, _env_active_asset, _env_active_task)): + for k in SECTIONS['active']: + common.active_paths[PrivateActivePaths][k] = os.environ.get(f'BOOKMARKS_ACTIVE_{k.upper()}', None) + for k in SECTIONS['active']: - common.active_paths[PrivateActivePaths][k] = \ - common.active_paths[SynchronisedActivePaths][k] + common.active_paths[PrivateActivePaths][k] = common.active_paths[SynchronisedActivePaths][k] + + # Verify the values + common.settings.verify_active(PrivateActivePaths) _true = {'True', 'true', '1', True} @@ -338,10 +354,11 @@ def __init__(self, parent=None): self.verify_timer.timeout.connect(self.load_active_values) def load_active_values(self): - """Load previously saved active path elements from the settings file. + """Load active path elements from the settings file. - If the resulting path is invalid, we'll progressively unset the invalid - path segments until we find a valid path. + Whilst the function will load the current values from the settings file, + it doesn't guarantee the values will actually be used. App will only use + these values if the active mode is set to `SynchronisedActivePaths`. """ self.sync() @@ -351,22 +368,21 @@ def load_active_values(self): v = None common.active_paths[SynchronisedActivePaths][k] = v self.verify_active(SynchronisedActivePaths) - self.verify_active(PrivateActivePaths) - def verify_active(self, m): + def verify_active(self, active_mode): """Verify the active path values and unset any item, that refers to an invalid path. Args: - m (int): The active mode. + active_mode (int): One of ``SynchronisedActivePaths`` or ``Pr. """ p = str() for k in SECTIONS['active']: - if common.active_paths[m][k]: - p += common.active_paths[m][k] + if common.active_paths[active_mode][k]: + p += common.active_paths[active_mode][k] if not os.path.exists(p): - common.active_paths[m][k] = None - if m == SynchronisedActivePaths: + common.active_paths[active_mode][k] = None + if active_mode == SynchronisedActivePaths: self.setValue(f'active/{k}', None) p += '/' @@ -452,7 +468,7 @@ def setValue(self, key, v): v (object): The value to save. """ - # Skip saving active values when PrivateActivePaths is on + # We don't want to save private active values to the user settings if common.active_mode == PrivateActivePaths and key in SECTIONS['active']: return diff --git a/bookmarks/common/setup.py b/bookmarks/common/setup.py index 2a9b1dfe..4fbac042 100644 --- a/bookmarks/common/setup.py +++ b/bookmarks/common/setup.py @@ -75,8 +75,7 @@ def initialize(mode): os.makedirs(os.path.normpath(common.temp_path())) common.init_signals() - common.prune_lock() - common.init_lock() # Sets the current active mode + common.init_active_mode() common.init_settings() _init_ui_scale() @@ -120,6 +119,25 @@ def initialize(mode): break +def initialize_core(): + """""" + if common.init_mode is not None: + raise RuntimeError(f'Already initialized as "{common.init_mode}"!') + + common.init_mode = common.EmbeddedMode + + _init_config() + + common.item_data = common.DataDict() + + if not os.path.isdir(common.temp_path()): + os.makedirs(os.path.normpath(common.temp_path())) + + common.init_signals(connect_signals=False) + common.init_active_mode() + common.init_settings() + + def uninitialize(): """Un-initializes all app components. diff --git a/bookmarks/common/signals.py b/bookmarks/common/signals.py index fea575c4..a6773850 100644 --- a/bookmarks/common/signals.py +++ b/bookmarks/common/signals.py @@ -9,9 +9,9 @@ from .. import common -def init_signals(): +def init_signals(connect_signals=True): """Initialize signals.""" - common.signals = CoreSignals() + common.signals = CoreSignals(connect_signals=connect_signals) class CoreSignals(QtCore.QObject): @@ -132,9 +132,12 @@ class CoreSignals(QtCore.QObject): #: Signals an item is ready to be processed by a thread threadItemsQueued = QtCore.Signal() - def __init__(self, parent=None): + def __init__(self, connect_signals=True, parent=None): super().__init__(parent=parent) + if not connect_signals: + return + from .. import actions self.toggleFilterButton.connect(actions.toggle_filter_editor) diff --git a/bookmarks/database.py b/bookmarks/database.py index 64db227d..4b4fc6ea 100644 --- a/bookmarks/database.py +++ b/bookmarks/database.py @@ -700,7 +700,7 @@ def get_row(self, source, table): Args: source (str): A source file path. - table (str): A database 01table name. + table (str): A database table name. Returns: dict: A dictionary of column/value pairs. diff --git a/bookmarks/items/models.py b/bookmarks/items/models.py index 02d3aecf..1bd95d9a 100644 --- a/bookmarks/items/models.py +++ b/bookmarks/items/models.py @@ -1024,10 +1024,6 @@ def filterAcceptsRow(self, idx, parent=None): if not ref(): return False - # Task item specific filter - if '#empty#' in ref()[idx][common.DescriptionRole]: - return False - filter_text = self.filter_text() if filter_text: filter_text = filter_text.strip().lower() diff --git a/bookmarks/maya/main.py b/bookmarks/maya/main.py index 73b87774..d95218ca 100644 --- a/bookmarks/maya/main.py +++ b/bookmarks/maya/main.py @@ -127,7 +127,7 @@ def show(): def init_maya_widget(): """Initializes the maya widget. - Usually the Maya plugin will call this method. + Usually the Maya plugin will call this function. """ if isinstance(common.maya_widget, MayaWidget): diff --git a/bookmarks/maya/plugin.py b/bookmarks/maya/plugin.py index aa3a003a..3d68bfcf 100644 --- a/bookmarks/maya/plugin.py +++ b/bookmarks/maya/plugin.py @@ -17,7 +17,7 @@ product = 'bookmarks' __author__ = 'Gergely Wootsch' -__version__ = '0.8.6' +__version__ = '0.8.9' maya_useNewAPI = True diff --git a/bookmarks/threads/workers.py b/bookmarks/threads/workers.py index 217b841f..a49b159f 100644 --- a/bookmarks/threads/workers.py +++ b/bookmarks/threads/workers.py @@ -308,10 +308,6 @@ def queue_model(self, data_type_ref1, data_type_ref2): if ref().loaded: continue # Skip if the model is loaded already - # Skip if the model has already been queued - if ref in threads.THREADS[q]['queue']: - continue - idxs = ref().keys() for idx in idxs: if not ref() or self.interrupt: @@ -698,12 +694,11 @@ def _file_it(path): break _suffix = f'{_idx + 1} items' if _idx < _max else f'{_max}+ items' - _suffix = _suffix if _idx > 0 else '#empty#' - _suffix = f' ({_suffix})' if description else _suffix + _suffix = _suffix if _idx > 0 else '' + _suffix = f' ({_suffix})' if description and _idx > 0 else _suffix ref()[common.DescriptionRole] += _suffix - self._process_bookmark_item(ref, db.source(), bookmark_row_data, pp) self._process_file_item(ref, item_type) self._process_sequence_item(ref, item_type) diff --git a/bookmarks/topbar/filters.py b/bookmarks/topbar/filters.py index c1ab3498..d0430041 100644 --- a/bookmarks/topbar/filters.py +++ b/bookmarks/topbar/filters.py @@ -157,6 +157,7 @@ def __init__(self, Model, tab_index, parent=None): self.setSizeAdjustPolicy(QtWidgets.QComboBox.AdjustToContents) self.setFixedHeight(common.size(common.size_margin)) self.setMinimumWidth(common.size(common.size_margin) * 3) + self.setMaxVisibleItems(48) min_width = self.minimumSizeHint().width() self.view().setMinimumWidth(min_width * 3) diff --git a/package/CMakeLists.txt b/package/CMakeLists.txt index 83365d2a..f993a181 100644 --- a/package/CMakeLists.txt +++ b/package/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.20) project( Bookmarks - VERSION 0.8.6 + VERSION 0.8.9 DESCRIPTION "Bookmarks is a lightweight asset manager for digital artists working in the animation, motion graphics and VFX industries." HOMEPAGE_URL "https://bookmarks-vfx.com" ) From 122be03dac55db05e1eb8c7657094cbbc1f102b0 Mon Sep 17 00:00:00 2001 From: Gergely Wootsch Date: Wed, 25 Oct 2023 17:49:42 +0200 Subject: [PATCH 20/32] Add Application Launcher button to the top bar This mean adding a new shortcut Alt+L for the launcher and modifying the look and ui behaviour to make the window more legible --- bookmarks/common/ui.py | 20 +++++++++------- bookmarks/contextmenu.py | 4 ++++ bookmarks/launcher/gallery.py | 16 ++++--------- bookmarks/main.py | 2 ++ bookmarks/rsc/stylesheet.qss | 15 ++++++++++-- bookmarks/shortcuts.py | 9 +++++++ bookmarks/topbar/buttons.py | 44 ++++++++++++++++++++++++++++++++++ bookmarks/topbar/topbar.py | 18 +++++++------- bookmarks/ui.py | 45 +++++++++++++---------------------- 9 files changed, 114 insertions(+), 59 deletions(-) diff --git a/bookmarks/common/ui.py b/bookmarks/common/ui.py index 25c915a7..dfab7cd3 100644 --- a/bookmarks/common/ui.py +++ b/bookmarks/common/ui.py @@ -114,21 +114,23 @@ def center_window(w): return -def center_to_parent(w): - """Move the given widget to the available screen geometry's middle. +def center_to_parent(widget, parent=None): + """Move the given widget to the widget's parent's middle. Args: - w (QWidget): The widget to center. - p (QWidget): The widget to center to. + widget (QWidget): The widget to center. + parent (QWidget): Optional. The widget to center to. """ - if not w.parent(): + if not widget.parent() and not parent: return + if widget.parent() and not parent: + parent = widget.parent() - w.adjustSize() - g = w.parent().geometry() - r = w.rect() - w.move(g.center() + (r.topLeft() - r.center())) + widget.adjustSize() + g = parent.geometry() + r = widget.rect() + widget.move(g.center() + (r.topLeft() - r.center())) return diff --git a/bookmarks/contextmenu.py b/bookmarks/contextmenu.py index 1a4d3dfc..b71087a6 100644 --- a/bookmarks/contextmenu.py +++ b/bookmarks/contextmenu.py @@ -1295,6 +1295,10 @@ def launcher_menu(self): 'icon': ui.get_icon('icon'), 'text': 'Application Launcher', 'action': actions.pick_launcher_item, + 'shortcut': shortcuts.get( + shortcuts.MainWidgetShortcuts, + shortcuts.ApplicationLauncher + ).key(), } def sg_thumbnail_menu(self): diff --git a/bookmarks/launcher/gallery.py b/bookmarks/launcher/gallery.py index 88696867..7b240b88 100644 --- a/bookmarks/launcher/gallery.py +++ b/bookmarks/launcher/gallery.py @@ -10,7 +10,7 @@ def close(): - """Opens the :class:`LauncherGallery` editor. + """Opens the :class:`ApplicationLauncherWidget` editor. """ if common.launcher_widget is None: @@ -24,23 +24,23 @@ def close(): def show(): - """Shows the :class:`LauncherGallery` editor. + """Shows the :class:`ApplicationLauncherWidget` editor. """ close() - common.launcher_widget = LauncherGallery() + common.launcher_widget = ApplicationLauncherWidget() common.launcher_widget.open() return common.launcher_widget -class LauncherGallery(ui.GalleryWidget): +class ApplicationLauncherWidget(ui.GalleryWidget): """A generic gallery widget used to let the user pick an item. """ def __init__(self, parent=None): super().__init__( - 'Applications', + 'Application Launcher', item_height=common.size(common.size_row_height) * 4, parent=parent ) @@ -78,9 +78,3 @@ def item_generator(self): for k in sorted(v, key=lambda _k: v[_k]['name']): yield v[k]['name'], v[k]['path'], v[k]['thumbnail'] - - def focusOutEvent(self, event): - """Event handler. - - """ - self.close() diff --git a/bookmarks/main.py b/bookmarks/main.py index b9a703ba..bbad649d 100644 --- a/bookmarks/main.py +++ b/bookmarks/main.py @@ -330,6 +330,8 @@ def _init_shortcuts(self): connect(shortcuts.Refresh, actions.refresh) connect(shortcuts.AltRefresh, actions.refresh) + connect(shortcuts.ApplicationLauncher, actions.pick_launcher_item) + connect(shortcuts.CopyItemPath, actions.copy_selected_path) connect(shortcuts.CopyAltItemPath, actions.copy_selected_alt_path) connect(shortcuts.RevealItem, actions.reveal_selected) diff --git a/bookmarks/rsc/stylesheet.qss b/bookmarks/rsc/stylesheet.qss index a54ad685..d43aaf3c 100644 --- a/bookmarks/rsc/stylesheet.qss +++ b/bookmarks/rsc/stylesheet.qss @@ -651,10 +651,21 @@ QWidget#mainRow {{ }} -GalleryWidget QWidget:focus {{ - border: {size_separator}px solid transparent; +GalleryWidget {{ + border: {size_separator}px solid {color_blue}; }} +GalleryWidget, +GalleryWidget > QScrollArea, +GalleryWidget QScrollArea, +GalleryWidget QScrollArea > QWidget, +GalleryWidget QScrollArea > QWidget > QWidget +{{ + background-color: {color_dark_background}; +}} + + + Viewer, ImageViewer {{ background-color: rgba(20,20,20,250); diff --git a/bookmarks/shortcuts.py b/bookmarks/shortcuts.py index e7a3b0df..6275a68c 100644 --- a/bookmarks/shortcuts.py +++ b/bookmarks/shortcuts.py @@ -30,6 +30,8 @@ Refresh = common.idx() AltRefresh = common.idx() +ApplicationLauncher = common.idx() + CopyItemPath = common.idx() CopyAltItemPath = common.idx() RevealItem = common.idx() @@ -172,6 +174,13 @@ 'description': 'Refresh', 'shortcut': None, }, + ApplicationLauncher: { + 'value': 'Alt+L', + 'default': 'Alt+L', + 'repeat': False, + 'description': 'Application Launcher', + 'shortcut': None, + }, CopyItemPath: { 'value': 'Ctrl+C', 'default': 'Ctrl+C', diff --git a/bookmarks/topbar/buttons.py b/bookmarks/topbar/buttons.py index fe157c63..7b7af084 100644 --- a/bookmarks/topbar/buttons.py +++ b/bookmarks/topbar/buttons.py @@ -293,3 +293,47 @@ def state(self): return True return False + + +class ApplicationLauncherButton(BaseControlButton): + """A button used to launch applications. + + """ + + def __init__(self, parent=None): + s = shortcuts.string( + shortcuts.MainWidgetShortcuts, + shortcuts.ApplicationLauncher + ) + super().__init__( + 'icon', + f'Application Launcher - {s}', + parent=parent + ) + self.clicked.connect(actions.pick_launcher_item) + self.clicked.connect(self.update) + + def state(self): + """The state of the auto-thumbnails""" + if not common.widget(): + return False + + model = common.widget().model().sourceModel() + p = model.source_path() + k = model.task() + t = model.data_type() + + if not p or not all(p) or not k or t is None: + return False + + data = common.get_task_data(p, k) + if not data: + return False + + if any( + (data[common.FileItem].refresh_needed, + data[common.SequenceItem].refresh_needed) + ): + return True + + return False diff --git a/bookmarks/topbar/topbar.py b/bookmarks/topbar/topbar.py index 0ccf25c8..e5801742 100644 --- a/bookmarks/topbar/topbar.py +++ b/bookmarks/topbar/topbar.py @@ -28,13 +28,9 @@ 'widget': tabs.FavouritesTabButton, 'hidden': False, }, - # next(n): { - # 'widget': filters.ServersFilterButton, - # 'hidden': False, - # }, next(n): { 'widget': filters.JobsFilterButton, - 'hidden': False, + 'hidden': True, }, # next(n): { # 'widget': filters.RootsFilterButton, @@ -42,24 +38,28 @@ # }, next(n): { 'widget': filters.EntityFilterButton, - 'hidden': False, + 'hidden': True, }, next(n): { 'widget': filters.TaskFilterButton, - 'hidden': False, + 'hidden': True, }, next(n): { 'widget': filters.SubdirFilterButton, - 'hidden': False, + 'hidden': True, }, next(n): { 'widget': filters.TypeFilterButton, - 'hidden': False, + 'hidden': True, }, next(n): { 'widget': buttons.FilterButton, 'hidden': False, }, + next(n): { + 'widget': buttons.ApplicationLauncherButton, + 'hidden': False, + }, next(n): { 'widget': buttons.RefreshButton, 'hidden': False, diff --git a/bookmarks/ui.py b/bookmarks/ui.py index 71040c67..9df62f01 100644 --- a/bookmarks/ui.py +++ b/bookmarks/ui.py @@ -1379,14 +1379,20 @@ def __init__( self._item_height = item_height self.setWindowFlags(QtCore.Qt.Window | QtCore.Qt.FramelessWindowHint) - self.setAttribute(QtCore.Qt.WA_NoSystemBackground) - self.setAttribute(QtCore.Qt.WA_TranslucentBackground) self.setAttribute(QtCore.Qt.WA_DeleteOnClose) - self.setWindowOpacity(0.8) + self.setWindowOpacity(0.95) + + self.installEventFilter(self) self._create_ui() self.init_data() + def eventFilter(self, widget, event): + if event.type() == QtCore.QEvent.WindowDeactivate: + self.close() + return True + return False + def _create_ui(self): if not self.parent(): common.set_stylesheet(self) @@ -1406,6 +1412,7 @@ def _create_ui(self): parent=self ) self.layout().addWidget(label) + self.layout().addSpacing(common.size(common.size_margin) * 1.5) _width = ( (common.size(common.size_indicator) * 2) + @@ -1430,9 +1437,7 @@ def _create_ui(self): ) widget = QtWidgets.QWidget(parent=self) - widget.setStyleSheet( - f'background-color: {common.rgb(common.color_separator)};' - ) + widget.eventFilter = self.eventFilter QtWidgets.QGridLayout(widget) widget.layout().setAlignment(QtCore.Qt.AlignCenter) @@ -1444,10 +1449,8 @@ def _create_ui(self): self.scroll_area.setWidgetResizable(True) self.scroll_area.setWidget(widget) - # self.layout().addSpacing(common.size(common.size_margin)) self.layout().addWidget(self.scroll_area, 1) - - self.setFocusProxy(widget) + self.setFocusProxy(self.scroll_area.widget()) def init_data(self): """Initializes data. @@ -1481,21 +1484,6 @@ def item_generator(self): """ raise NotImplementedError('Abstract method must be implemented by subclass.') - def paintEvent(self, event): - painter = QtGui.QPainter() - painter.begin(self) - painter.setBrush(common.color(common.color_separator)) - pen = QtGui.QPen(QtGui.QColor(0, 0, 0, 150)) - pen.setWidth(common.size(common.size_separator)) - painter.setPen(pen) - painter.setRenderHint(QtGui.QPainter.Antialiasing) - o = common.size(common.size_indicator) * 2.0 - painter.drawRoundedRect( - self.rect().marginsRemoved(QtCore.QMargins(o, o, o, o)), - o, o - ) - painter.end() - def focusOutEvent(self, event): self.accept() # or self.reject() @@ -1503,17 +1491,18 @@ def showEvent(self, event): """Show event handler. """ - if not self.parent(): - common.center_window(self) + common.center_to_parent(self, common.main_widget) + common.move_widget_to_available_geo(self) self.anim = QtCore.QPropertyAnimation(self, b'windowOpacity') self.anim.setDuration(500) # Animation duration in milliseconds self.anim.setStartValue(0) self.anim.setEndValue(0.95) self.anim.setEasingCurve(QtCore.QEasingCurve.OutCubic) - self.anim.start() + self.anim.start(QtCore.QAbstractAnimation.DeleteWhenStopped) - self.scroll_area.widget().setFocus(QtCore.Qt.PopupFocusReason) + self.anim.finished.connect(self.raise_) + self.anim.finished.connect(lambda: self.setFocus(QtCore.Qt.PopupFocusReason)) def done(self, r): if r == QtWidgets.QDialog.Rejected: From c37be3922ec2d29b8034c06dd41ea8e0923ec680 Mon Sep 17 00:00:00 2001 From: Gergely Wootsch Date: Thu, 26 Oct 2023 13:19:31 +0200 Subject: [PATCH 21/32] Update the Token Editor interface in the bookmark item properties This commit addresses the long-standing issue of not being able to add/remove token values from file and publish templates. I amended the Token editor so now we can reaarange, rename, add and remove items, as well as restore sections to their default values. --- bookmarks/maya/main.py | 5 + .../maya/scripts/aka_character_caches.py | 864 ++++++++++++++++++ bookmarks/maya/scripts/aka_cloth_cache.py | 270 ------ .../maya/scripts/aka_cloth_cache_animated.py | 270 ------ bookmarks/maya/scripts/scripts.json | 12 +- bookmarks/publish.py | 10 +- bookmarks/rsc/stylesheet.qss | 13 +- bookmarks/shotgun/sg_publish_clip.py | 5 - bookmarks/tokens/tokens.py | 147 +-- bookmarks/tokens/tokens_editor.py | 438 +++++++-- bookmarks/ui.py | 6 +- 11 files changed, 1291 insertions(+), 749 deletions(-) create mode 100644 bookmarks/maya/scripts/aka_character_caches.py delete mode 100644 bookmarks/maya/scripts/aka_cloth_cache.py delete mode 100644 bookmarks/maya/scripts/aka_cloth_cache_animated.py diff --git a/bookmarks/maya/main.py b/bookmarks/maya/main.py index d95218ca..e9d7b65b 100644 --- a/bookmarks/maya/main.py +++ b/bookmarks/maya/main.py @@ -68,6 +68,11 @@ def show(): for widget in QtWidgets.QApplication.instance().allWidgets(): # Skipping workspaceControls objects, just in case there's a name conflict # between what the parent().objectName() and this method yields + try: + widget.objectName() + except: + continue + if re.match(f'{common.product}_.*WorkspaceControl', widget.objectName()): continue diff --git a/bookmarks/maya/scripts/aka_character_caches.py b/bookmarks/maya/scripts/aka_character_caches.py new file mode 100644 index 00000000..6418818e --- /dev/null +++ b/bookmarks/maya/scripts/aka_character_caches.py @@ -0,0 +1,864 @@ +"""Export script for Odyssey cloth sims. + +The script automates the animation cache export from Maya. + +It performs the follow steps: + - Saves the current animation to studio library + - Removes the animation from the body and root controllers + - Adds a preroll with a reset pose + - Saves the animation caches + +Gergely Wootsch. +hello@gergely-wootsch.com +Studio Aka, 2023 October + +""" +import json +import os +import sys + +import maya.cmds as cmds +from PySide2 import QtWidgets, QtCore + +from . import aka_make_export_sets +from .. import base as mayabase +from .. import export +from ... import actions +from ... import common +from ... import database +from ... import images +from ... import log +from ... import ui +from ...tokens import tokens + +instance = None + +#: The path of the output cache file +cache_path = '{dir}/{basename}_{version}.{ext}' + +#: The default values for the dialog +DEFAULT_VALUES = { + 'Studio Library': { + 'studio_library_folder': { + 'default': '', + 'placeholder': 'C:/Users/aka/Documents/studiolibrary', + 'widget': ui.LineEdit, + }, + 'studio_library_reset_pose': { + 'default': 'Characters/IbogaineMarcus/Reset/A-Pose-v2-FullFK.pose/pose.json', + 'placeholder': 'relative/path/to/pose.json', + 'widget': ui.LineEdit, + }, + 'studio_library_output_folder': { + 'default': 'Shots/{prefix}/{asset0}_{shot}', + 'placeholder': 'relative/path/to/output/dir', + 'widget': ui.LineEdit, + }, + }, + 'Character': { + 'namespace': { + 'default': 'IbogaineMarcus_01', + 'placeholder': 'CharacterNamespace_01', + 'widget': ui.LineEdit, + }, + 'controllers_set': { + 'default': 'rig_controllers_grp', + 'placeholder': 'controller_set_name', + 'widget': ui.LineEdit, + }, + 'hip_controller': { + 'default': 'body_C0_ctl', + 'placeholder': 'hip_controller_name', + 'widget': ui.LineEdit, + }, + 'exclude_reset_pose_from': { + 'default': 'legUI_L0_ctl, armUI_R0_ctl, faceUI_C0_ctl, legUI_R0_ctl, spineUI_C0_ctl, armUI_L0_ctl', + 'placeholder': 'list, of, controllers, to, exclude', + 'widget': ui.LineEdit, + }, + 'apply_animation_exclusions': { + 'default': True, + 'placeholder': '', + 'widget': lambda: QtWidgets.QCheckBox('Apply Animation Exclusions'), + }, + 'exclude_animation_from': { + 'default': 'body_C0_ctl, world_ctl, root_C0_ctl', + 'placeholder': 'list, of, controllers, to, exclude', + 'widget': ui.LineEdit, + }, + } +} + + +def find_studio_library(): + for path in sys.path: + if not os.path.isdir(path): + continue + for entry in os.scandir(path): + if 'studiolibrary' in entry.name: + if os.path.isdir(f'{entry.path}/src'): + return os.path.normpath(os.path.abspath(f'{entry.path}/src')) + return None + + +def add_studio_library_to_path(): + path = find_studio_library() + if not path: + raise RuntimeError('Could not find Studio Library.') + + if path not in sys.path: + sys.path.insert(0, path) + + +def get_cache_path(set_name, ext, makedir=True): + """Get the path of the output cache file. + + """ + workspace = cmds.workspace(q=True, fn=True) + + export_dir = mayabase.DEFAULT_CACHE_DIR.format( + export_dir=tokens.get_folder(tokens.CacheFolder), ext=tokens.get_subfolder(tokens.CacheFolder, ext) + ) + file_path = mayabase.CACHE_PATH.format( + workspace=workspace, export_dir=export_dir, set=set_name, ext=ext + ) + file_path = mayabase.sanitize_namespace(file_path) + + file_info = QtCore.QFileInfo(file_path) + _version = 1 + while True: + version = 'v' + f'{_version}'.zfill(3) + + file_path = cache_path.format( + dir=file_info.dir().path(), basename=file_info.completeBaseName(), version=version, ext=file_info.suffix() + ) + + if not QtCore.QFileInfo(file_path).exists(): + break + if _version >= 999: + break + + _version += 1 + + if makedir: + QtCore.QFileInfo(file_path).dir().mkpath('.') + return file_path + + +def get_studio_library_default_library(): + """Get the default library from the Studio Library settings file.""" + try: + import studiolibrary + import mutils + except ImportError as e: + try: + add_studio_library_to_path() + except ImportError: + raise e + else: + import studiolibrary + import mutils + + s = studiolibrary.settingsPath() + if not s or not QtCore.QFileInfo(s).exists(): + raise RuntimeError('Could not find the Studio Library settings file.') + + with open(s, 'r') as f: + studio_library_settings = json.loads(f.read()) + + if 'Default' not in studio_library_settings: + raise RuntimeError('Studio Library does not seem to have been configured. Please check your settings.') + if 'path' not in studio_library_settings['Default']: + raise RuntimeError('Studio Library seems to be missing a required settings key "path"') + + v = studio_library_settings['Default']['path'] + if not QtCore.QFileInfo(v).exists(): + raise RuntimeError( + 'Could not find the default Studio Library directory. Check that everything is configured correctly.' + ) + + return v + + +class ExportCharacterCachesDialog(QtWidgets.QDialog): + statusChanged = QtCore.Signal(str, str) + + """Dialog to provide options for exporting character caches.""" + + def __init__(self, parent=None): + super().__init__(parent) + + self.settings = QtCore.QSettings('StudioAka', 'ExportCharacterCaches') + self.setWindowTitle('Export Character Caches') + + self._create_ui() + self._connect_signals() + + def _create_ui(self): + if not self.parent(): + common.set_stylesheet(self) + + QtWidgets.QVBoxLayout(self) + + pixmap, color = images.get_thumbnail( + common.active('server'), + common.active('job'), + common.active('root'), + common.active('asset', path=True), + size=common.size(common.size_row_height) * 3, + fallback_thumb='placeholder', + get_path=False + ) + + row = ui.add_row(None, height=None, parent=self) + label = QtWidgets.QLabel(parent=self) + label.setPixmap(pixmap) + row.layout().addWidget(label, 0) + row.layout().addSpacing(common.size(common.size_margin) * 0.5) + + active_index = common.active_index(common.AssetTab) + if not active_index or not active_index.isValid(): + raise RuntimeError('Could not find active asset. Please make sure an asset is activated.') + asset_name = active_index.data(QtCore.Qt.DisplayRole) + asset_name = asset_name if asset_name else 'Export Cache' + + row.layout().addWidget(ui.PaintedLabel(asset_name, size=common.size(common.size_font_large))) + + for k in DEFAULT_VALUES: + grp = ui.get_group(parent=self, ) + + for _k, _v in DEFAULT_VALUES[k].items(): + v = self.settings.value(_k, _v['default']) + editor = _v['widget']() + + if isinstance(editor, QtWidgets.QCheckBox): + if not isinstance(v, bool): + v = True + editor.setChecked(v) + + if isinstance(editor, ui.LineEdit): + if not isinstance(v, str): + v = '' + editor.setText(v) + if isinstance(_v['placeholder'], str): + editor.setPlaceholderText(_v['placeholder']) + + row = ui.add_row(_k.replace('_', ' ').title(), parent=grp) + if _k == 'studio_library_folder': + row.setHidden(True) + row.layout().addWidget(editor) + + setattr(self, f'{_k}_editor', editor) + + self.layout().addWidget(grp) + + # Buttons + button_layout = QtWidgets.QHBoxLayout() + self.ok_button = ui.PaintedButton('Export Caches') + self.cancel_button = ui.PaintedButton('Cancel') + self.apply_hip_pose_button = ui.PaintedButton('Apply Hip Pose') + + button_layout.addWidget(self.ok_button, 1) + button_layout.addWidget(self.apply_hip_pose_button, 1) + button_layout.addWidget(self.cancel_button, 0) + + self.layout().addStretch(1) + self.layout().addLayout(button_layout) + + def _connect_signals(self): + self.ok_button.clicked.connect(self.accept) + self.cancel_button.clicked.connect(self.reject) + self.statusChanged.connect(self.show_status) + self.apply_hip_pose_button.clicked.connect(self.apply_hip_pose) + + @QtCore.Slot(str, str) + def show_status(self, title, body): + common.show_message( + title, body=body, message_type=None, buttons=[], disable_animation=True, parent=self + ) + + def accept(self): + """Save the settings and accept the dialog. + + """ + # Get database and config + if not all(common.active('asset', args=True)): + raise RuntimeError('An asset must be active to export.') + + try: + cmds.ogs(pause=True) + except: + pass + + try: + cmds.refresh(suspend=True) + except: + pass + + try: + output_paths = self.export() + finally: + try: + cmds.refresh(suspend=False) + except: + pass + + try: + cmds.ogs(pause=False) + except: + pass + + common.show_message( + 'Success', f'All cache files were exported. Check the console for details.', message_type='success' + ) + + log.success(f'Character caches were exported successfully.') + print('\n\n>>> ======================') + print('>>> Exported files:') + for path in output_paths: + print(QtCore.QFileInfo(path).filePath()) + + super().accept() + + def _get_options(self): + # Collect all the current settings to a dictionary + self.statusChanged.emit('Preparing...', 'Validating options, please wait') + + options = {} + for k in DEFAULT_VALUES: + for _k, _v in DEFAULT_VALUES[k].items(): + options[_k] = self.settings.value(_k, _v['default']) + + config = tokens.get(*common.active('root', args=True)) + seq, shot = common.get_sequence_and_shot(common.active('asset')) + + db = database.get(*common.active('root', args=True)) + cut_in = db.value(common.active('asset', path=True), 'cut_in', database.AssetTable) + cut_out = db.value(common.active('asset', path=True), 'cut_out', database.AssetTable) + prefix = db.value(common.active('root', path=True), 'prefix', database.BookmarkTable) + + # --- Validate values --- + if not all((seq, shot)): + raise RuntimeError('Could not find sequence and shot names.') + + # Check if none of the values are None + if any((x is None for x in (cut_in, cut_out, prefix))): + raise RuntimeError('Not all required values are set: We need cut_in, cut_out or prefix to be set.') + + if cut_out <= cut_in: + raise RuntimeError(f'Cut out is smaller than cut in: {cut_out} <= {cut_in}') + + # --- Validate the options --- + + # Get the default library from the Studio Library settings file + options['studio_library_folder'] = get_studio_library_default_library() + + # studio_library_reset_pose + if not options['studio_library_reset_pose']: + raise RuntimeError('Please specify a reset pose.') + + # Check if we specified a valid reset pose + _p = options['studio_library_folder'].replace('\\', '/') + __p = options['studio_library_reset_pose'].replace('\\', '/') + options['studio_library_reset_pose'] = f'{_p}/{__p}'.replace('\\', '/') + + if not QtCore.QFileInfo(options['studio_library_reset_pose']).exists(): + raise RuntimeError(f'Reset pose "{options["studio_library_reset_pose"]}" does not exist.') + + # studio_library_output_folder + if not options['studio_library_output_folder']: + raise RuntimeError('Please specify an output folder template.') + + _p = options['studio_library_folder'].replace('\\', '/') + __p = options['studio_library_output_folder'].replace('\\', '/') + __p = config.expand_tokens( + __p, + asset=common.active('asset'), + shot=shot, + sequence=seq, + prefix=prefix.split('_')[-1].upper(), + ).replace('\\', '/') + options['studio_library_output_folder'] = f'{_p}/{__p}' + + # namespace + if not options['namespace']: + raise RuntimeError('Please specify a namespace.') + # Check if we specified a valid namespace + if not cmds.namespace(exists=options['namespace']): + raise RuntimeError(f'Namespace "{options["namespace"]}" does not exist.') + + # controllers_set + if not options['controllers_set']: + raise RuntimeError('Please specify a controllers set.') + # Check if we specified a valid set. The controllers set should be an object set and part of the namespace. + if not cmds.objExists(f'{options["namespace"]}:{options["controllers_set"]}'): + raise RuntimeError(f'Controllers set "{options["namespace"]}:{options["controllers_set"]}" does not exist.') + options['controllers_set'] = f'{options["namespace"]}:{options["controllers_set"]}' + + # Hip controller + if not options['hip_controller']: + raise RuntimeError('Please specify a hip controller.') + # Check if we specified a valid hip controller + if not cmds.objExists(f'{options["namespace"]}:{options["hip_controller"]}'): + raise RuntimeError(f'Hip controller "{options["namespace"]}:{options["hip_controller"]}" does not exist.') + options['hip_controller'] = f'{options["namespace"]}:{options["hip_controller"]}' + + # apply_animation_exclusions + # Change the value to a boolean + options['apply_animation_exclusions'] = options['apply_animation_exclusions'] == QtCore.Qt.Checked + + # exclude_animation_from + if options['apply_animation_exclusions']: + _not_found = options['exclude_animation_from'].split(',') + _not_found = [x.strip() for x in _not_found] + _not_found = [f'{options["namespace"]}:{x}' for x in _not_found if + not cmds.objExists(f'{options["namespace"]}:{x}')] + + if _not_found: + print(f'Warning: Could not find the following controllers: {_not_found}') + + options['exclude_animation_from'] = options['exclude_animation_from'].split(',') + options['exclude_animation_from'] = [x.strip() for x in options['exclude_animation_from']] + options['exclude_animation_from'] = [f'{options["namespace"]}:{x}' for x in + options['exclude_animation_from'] if + x and cmds.objExists(f'{options["namespace"]}:{x}')] + else: + options['exclude_animation_from'] = [] + + # exclude_reset_pose_from + _not_found = options['exclude_reset_pose_from'].split(',') + _not_found = [x.strip() for x in _not_found] + _not_found = [f'{options["namespace"]}:{x}' for x in _not_found if + not cmds.objExists(f'{options["namespace"]}:{x}')] + + if _not_found: + print(f'Warning: Could not find the following controllers: {_not_found}') + + options['exclude_reset_pose_from'] = options['exclude_reset_pose_from'].split(',') + options['exclude_reset_pose_from'] = [x.strip() for x in options['exclude_reset_pose_from']] + options['exclude_reset_pose_from'] = [f'{options["namespace"]}:{x}' for x in options['exclude_reset_pose_from'] + if + x and cmds.objExists(f'{options["namespace"]}:{x}')] + + options['cut_in'] = cut_in + options['cut_out'] = cut_out + options['prefix'] = prefix + + return options + + @common.error + def export(self): + """The main export function. + + """ + # Save the current settings + for k in DEFAULT_VALUES: + for _k, _v in DEFAULT_VALUES[k].items(): + editor = getattr(self, f'{_k}_editor') + if isinstance(editor, QtWidgets.QCheckBox): + self.settings.setValue(_k, editor.isChecked()) + if isinstance(editor, QtWidgets.QLineEdit): + self.settings.setValue(_k, editor.text()) + + options = self._get_options() + cut_in = options['cut_in'] + cut_out = options['cut_out'] + + # --- Start the export process --- + preroll = 51 + + # Save current selection + original_selection = cmds.ls(selection=True) + output_paths = [] + + # Clear selection + cmds.select(clear=True) + + # Let's make the export sets + try: + aka_make_export_sets.run() + except RuntimeError as e: + raise RuntimeError(f'Could not create export sets: {e}') + + # Add pre-roll to the timeline + cmds.playbackOptions( + animationStartTime=cut_in - preroll, minTime=-cut_in - preroll, animationEndTime=cut_out, maxTime=cut_out + ) + + # Move the current time to cut_in + cmds.currentTime(cut_in) + + self.statusChanged.emit('Exporting...', 'Clearing keyframes...') + + # Let's keyframe the controllers at cut_in and cut_out + for n in (cut_in, cut_out): + cmds.currentTime(n) + cmds.select(cmds.sets(options['controllers_set'], query=True), replace=True, ne=True) + cmds.setKeyframe( + cmds.sets( + options['controllers_set'], query=True + ), breakdown=False, preserveCurveShape=False, hierarchy='none', controlPoints=False, shape=True, ) + + # Remove any keyframes before between cut_in and cut_in - preroll - 1 + for node in cmds.sets(options['controllers_set'], query=True): + attrs = cmds.listAnimatable(node) + if not attrs: + continue + for attr in attrs: + if not cmds.keyframe(attr, query=True, time=(cut_in - preroll - 1, cut_in - 1)): + continue + cmds.cutKey(attr, time=(cut_in - preroll - 1, cut_in - 1)) + + # Move the current time to cut_in + cmds.currentTime(cut_in) + + # Studio Library: Save the full current animation + _dir = QtCore.QDir(options["studio_library_output_folder"]) + _dir.mkpath('.') + + self.statusChanged.emit('Exporting...', 'Exporting Studio Library clips...') + + try: + import studiolibrary + import mutils + except ImportError as e: + try: + add_studio_library_to_path() + except ImportError: + raise e + else: + import studiolibrary + import mutils + + try: + mutils.saveAnim( + cmds.sets( + options['controllers_set'], query=True + ), os.path.normpath( + f'{options["studio_library_output_folder"]}/' + f'{options["namespace"]}_fullanim.anim' + ), time=(cut_in, cut_out), bakeConnected=False, metadata='' + ) + p = os.path.normpath( + f'{options["studio_library_output_folder"]}/' + f'{options["namespace"]}_fullanim.anim' + ) + log.success(f'Studio Library clip saved to:\n{p}') + except UnicodeDecodeError as e: + # Might be a bug in the Studio Library module, seems like it can be ignored + print(e) + + # Studio Library: Save the animation start pose + try: + mutils.savePose( + os.path.normpath( + f'{options["studio_library_output_folder"]}/{options["namespace"]}_animstart.pose/pose.json' + ), cmds.sets( + options['controllers_set'], query=True + ), ) + p = os.path.normpath( + f'{options["studio_library_output_folder"]}/{options["namespace"]}_animstart.pose/pose.json' + ) + output_paths.append(p) + log.success(f'Studio Library pose saved to:\n{p}') + except UnicodeDecodeError as e: + print(e) + + self.statusChanged.emit('Exporting...', 'Baking hip animation...') + + # Parent and bake a null to the hip and save the world animation for later use + locator = cmds.spaceLocator(name='nullLocator')[0] + constraint = cmds.parentConstraint(options['hip_controller'], locator, maintainOffset=False)[0] + + cmds.bakeResults( + locator, + time=(cut_in,cut_out), + simulation=True, + sampleBy=1, oversamplingRate=1, disableImplicitControl=True, + preserveOutsideKeys=True, sparseAnimCurveBake=False, removeBakedAttributeFromLayer=False, + bakeOnOverrideLayer=False, minimizeRotation=True, controlPoints=False, shape=True + ) + + self.statusChanged.emit('Exporting...', 'Saving hip animation...') + + # Save the hip animation + try: + mutils.saveAnim( + [locator, ], os.path.normpath( + f'{options["studio_library_output_folder"]}/' + f'{options["namespace"]}_hip.anim' + ), time=(cut_in, cut_out), bakeConnected=False, metadata='' + ) + p = os.path.normpath( + f'{options["studio_library_output_folder"]}/' + f'{options["namespace"]}_hip.anim' + ) + output_paths.append(p) + log.success(f'Studio Library clip saved to:\n{p}') + except UnicodeDecodeError as e: + print(e) + + # Cleanup + cmds.delete(constraint) + cmds.delete(locator) + + # Remove all animation from any specified excluded controllers + if options['apply_animation_exclusions'] and options['exclude_animation_from']: + for obj in options['exclude_animation_from']: + attrs = cmds.listAnimatable(obj) + if not attrs: + continue + for attr in attrs: + cmds.cutKey(attr, clear=True) + + # Make sure the pose is unaltered at cut_in + cmds.currentTime(cut_in) + mutils.loadPose( + os.path.normpath( + f'{options["studio_library_output_folder"]}/{options["namespace"]}_animstart.pose/pose.json' + ), key=True + ) + + # Move the current time to cut_in - preroll + cmds.currentTime(cut_in - preroll) + + # Studio Library: Apply the reset pose at cut_in - preroll + # We only want to apply the reset pose to the controllers that are not excluded + _s1 = set(cmds.sets(options['controllers_set'], query=True)) + _s2 = set(options['exclude_animation_from']) if options["apply_animation_exclusions"] else set() + _s3 = set(options["exclude_reset_pose_from"]) + + mutils.loadPose( + os.path.normpath( + options["studio_library_reset_pose"] + ), objects=list(_s1 - _s2 - _s3), key=True, namespaces=[options['namespace'], ] + ) + + # -- Cache exports -- + + self.statusChanged.emit('Caching...', 'Saving camera cache...') + + # Export the camera + if cmds.objExists('camera_export') and cmds.sets('camera_export', query=True): + p = get_cache_path('camera_export', 'ma') + output_paths.append(p) + export.export_maya( + p, cmds.sets('camera_export', query=True), cut_in, cut_out, step=1.0 + ) + log.success(f'Camera cache saved to:\n{p}') + + p = get_cache_path('camera_export', 'abc') + output_paths.append(p) + export.export_alembic( + p, cmds.sets('camera_export', query=True), cut_in, cut_out, + step=1.0 + ) + log.success(f'Camera cache saved to:\n{p}') + else: + print('Warning: "camera_export" not found, or empty. Skipping export.') + + # Export character caches + # In the case of namespaces suffixed by _01, we want to remove the suffix + # as the export sets are not suffixed by _01 (this does not apply to _02, _03, etc.) + if options['namespace'].endswith('_01'): + export_namespace = options['namespace'].replace('_01', '') + else: + export_namespace = options['namespace'] + + # Body + self.statusChanged.emit('Caching...', 'Saving body cache...') + + if cmds.objExists(f'{export_namespace}_body_export') and cmds.sets( + f'{export_namespace}_body_export', query=True + ): + p = get_cache_path(f'{export_namespace}_body_export', 'abc') + output_paths.append(p) + export.export_alembic( + p, cmds.sets(f'{export_namespace}_body_export', query=True), cut_in - preroll, cut_out, step=1.0 + ) + log.success(f'Body cache saved to:\n{p}') + else: + print(f'Warning: "{export_namespace}_body_export" not found, or empty. Skipping export.') + + # Cloth + self.statusChanged.emit('Caching...', 'Saving cloth cache...') + + if cmds.objExists(f'{export_namespace}_cloth_export') and cmds.sets( + f'{export_namespace}_cloth_export', query=True + ): + p = get_cache_path(f'{export_namespace}_cloth_export', 'abc') + output_paths.append(p) + export.export_alembic( + p, cmds.sets( + f'{export_namespace}_cloth_export', query=True + ), cut_in - preroll, cut_in - preroll, step=1.0 + ) + log.success(f'Cloth cache saved to:\n{p}') + else: + print(f'Warning: "{export_namespace}_cloth_export" not found, or empty. Skipping export.') + + # Extra + self.statusChanged.emit('Caching...', 'Saving extra cache...') + + if cmds.objExists(f'{export_namespace}_extra_export') and cmds.sets( + f'{export_namespace}_extra_export', query=True + ): + p = get_cache_path(f'{export_namespace}_extra_export', 'abc') + output_paths.append(p) + export.export_alembic( + p, cmds.sets(f'{export_namespace}_extra_export', query=True), cut_in - preroll, cut_out, step=1.0 + ) + log.success(f'Extra cache saved to:\n{p}') + + # -- Cleanup -- + + # Studio Library: Load back the full animation + mutils.loadAnims( + [os.path.normpath( + f'{options["studio_library_output_folder"]}/{options["namespace"]}_fullanim.anim' + ), ], objects=cmds.sets( + options['controllers_set'], query=True + ), currentTime=False, option='replaceCompletely', namespaces=[options['namespace'], ] + ) + + cmds.playbackOptions(animationStartTime=cut_in, minTime=cut_in, animationEndTime=cut_out, maxTime=cut_out) + + # Move the current time to cut_in + cmds.currentTime(cut_in) + + # Reset the selection + cmds.select(original_selection, replace=True) + + return output_paths + + @common.error + def apply_hip_pose(self): + self._apply_hip_pose() + common.show_message('Success', 'Hip pose applied successfully.', message_type='success') + + def _apply_hip_pose(self): + try: + import studiolibrary + import mutils + except ImportError as e: + try: + add_studio_library_to_path() + except ImportError: + raise e + else: + import studiolibrary + import mutils + + sel = cmds.ls(selection=True) + if not sel: + raise RuntimeError('Please select a single object.') + node = sel[0] + + cmds.select(clear=True) + + # make sure the selection is a transformable node (e.g. not a shape) + if not cmds.listRelatives(node, shapes=True): + raise RuntimeError('Please select a transformable node.') + + # Collect all the current settings to a dictionary + self.statusChanged.emit('Working...', 'Validating options, please wait') + + options = {} + for k in DEFAULT_VALUES: + for _k, _v in DEFAULT_VALUES[k].items(): + options[_k] = self.settings.value(_k, _v['default']) + + config = tokens.get(*common.active('root', args=True)) + seq, shot = common.get_sequence_and_shot(common.active('asset')) + + db = database.get(*common.active('root', args=True)) + cut_in = db.value(common.active('asset', path=True), 'cut_in', database.AssetTable) + cut_out = db.value(common.active('asset', path=True), 'cut_out', database.AssetTable) + prefix = db.value(common.active('root', path=True), 'prefix', database.BookmarkTable) + + # --- Validate values --- + if not all((seq, shot)): + raise RuntimeError('Could not find sequence and shot names.') + + # Check if none of the values are None + if any((x is None for x in (cut_in, cut_out, prefix))): + raise RuntimeError('Not all required values are set: We need cut_in, cut_out or prefix to be set.') + + if cut_out <= cut_in: + raise RuntimeError(f'Cut out is smaller than cut in: {cut_out} <= {cut_in}') + + # --- Validate the options --- + + # Get the default library from the Studio Library settings file + options['studio_library_folder'] = get_studio_library_default_library() + + if not options['namespace']: + raise RuntimeError('Please specify a namespace.') + + # studio_library_output_folder + if not options['studio_library_output_folder']: + raise RuntimeError('Please specify an output folder template.') + + _p = options['studio_library_folder'].replace('\\', '/') + __p = options['studio_library_output_folder'].replace('\\', '/') + __p = config.expand_tokens( + __p, + asset=common.active('asset'), + shot=shot, + sequence=seq, + prefix=prefix.split('_')[-1].upper(), + ).replace('\\', '/') + options['studio_library_output_folder'] = f'{_p}/{__p}' + + # Delete the locator if it exists + if cmds.objExists('nullLocator'): + cmds.delete('nullLocator') + + locator = cmds.spaceLocator(name='nullLocator')[0] + + if not QtCore.QFileInfo(f'{options["studio_library_output_folder"]}/{options["namespace"]}_hip.anim').exists(): + raise RuntimeError(f'Could not find hip animation: {options["studio_library_output_folder"]}/{options["namespace"]}_hip.anim') + + self.statusChanged.emit('Working...', 'Importing animation...') + + # Load the hip animation onto the locator + mutils.loadAnims( + [os.path.normpath( + f'{options["studio_library_output_folder"]}/{options["namespace"]}_hip.anim' + ), ], + objects=[locator,], + currentTime=False, + option='replaceCompletely' + ) + + # Move the current time to cut_in + cmds.currentTime(cut_in) + + # Reset node transforms + cmds.move(0, 0, 0, node, worldSpace=True) + cmds.rotate(0, 0, 0, node, worldSpace=True) + cmds.scale(1, 1, 1, node, worldSpace=True) + + # Parent constrain the selection to the locator + cmds.parentConstraint(locator, node, maintainOffset=True)[0] + + def showEvent(self, event): + super().showEvent(event) + common.center_window(self) + + def sizeHint(self): + return QtCore.QSize(common.size(common.size_width), common.size(common.size_height)) + + +def run(): + global instance + + if instance is None: + instance = ExportCharacterCachesDialog() + instance.accepted.connect(lambda: common.source_model(common.FileTab).reset_data(force=True)) + + actions.change_tab(common.FileTab) + actions.set_task_folder(tokens.get_folder(tokens.CacheFolder)) + + instance.open() + instance.raise_() diff --git a/bookmarks/maya/scripts/aka_cloth_cache.py b/bookmarks/maya/scripts/aka_cloth_cache.py deleted file mode 100644 index 9873a28b..00000000 --- a/bookmarks/maya/scripts/aka_cloth_cache.py +++ /dev/null @@ -1,270 +0,0 @@ -"""Export script for Odyssey cloth sims. - -The scripts automates the animation cache export from Maya. - -It performs the follow steps: - - Saves the current animation to studio library - - Removes the animation from the body and root controllers - - Adds a preroll with a reset pose - - Saves the animation caches - -Gergely Wootsch. -hello@gergely-wootsch.com -Studio Aka, 2023 October - -""" -import os - -import maya.cmds as cmds -from PySide2 import QtCore -from ... import common -from ... import database -from .. import base as mayabase -from .. import export -from . import aka_make_export_sets -from ...tokens import tokens - - -studiolibrary_dir = f'{common.active("server")}/{common.active("job")}/070_Assets/Character/studiolibrary' -reset_pose = f'{studiolibrary_dir}/Characters/IbogaineMarcus/Reset/A-Pose-v2-FullFK.pose/pose.json' - -cache_destination_dir = '{studiolibrary_dir}/Shots/{prefix}/{asset0}_{shot}' - -namespace = 'IbogaineMarcus_01' -controllers_set = f'{namespace}:rig_controllers_grp' - -# These controllers should not be animated when exporting the caches -exclude_controllers1 = [ - f'{namespace}:body_C0_ctl', - f'{namespace}:world_ctl', - f'{namespace}:root_C0_ctl' -] - -# These controllers should be excluded from the a-pose -exclude_controllers2 = [ - f'{namespace}:legUI_L0_ctl', - f'{namespace}:armUI_R0_ctl', - f'{namespace}:faceUI_C0_ctl', - f'{namespace}:legUI_R0_ctl', - f'{namespace}:spineUI_C0_ctl', - f'{namespace}:armUI_L0_ctl' -] - - -def get_cache_path(set_name, ext, makedir=True): - """Get the path of the output cache file. - - """ - workspace = cmds.workspace(q=True, fn=True) - - export_dir = mayabase.DEFAULT_CACHE_DIR.format( - export_dir=tokens.get_folder(tokens.CacheFolder), - ext=tokens.get_subfolder(tokens.CacheFolder, ext) - ) - file_path = mayabase.CACHE_PATH.format( - workspace=workspace, - export_dir=export_dir, - set=set_name, - ext=ext - ) - file_path = mayabase.sanitize_namespace(file_path) - - file_info = QtCore.QFileInfo(file_path) - _version = 1 - while True: - version = 'v' + f'{_version}'.zfill(3) - - file_path = '{dir}/{basename}_{version}.{ext}'.format( - dir=file_info.dir().path(), - basename=file_info.completeBaseName(), - version=version, - ext=file_info.suffix() - ) - - if not QtCore.QFileInfo(file_path).exists(): - break - if _version >= 999: - break - - _version += 1 - - if makedir: - QtCore.QFileInfo(file_path).dir().mkpath('.') - return file_path - - -@common.debug -@common.error -def run(): - try: - import mutils - except ImportError: - raise RuntimeError('Could not export caches. Is Studio Library installed?') - - config = tokens.get(*common.active('root', args=True)) - - db = database.get(*common.active('root', args=True)) - cut_in = db.value(common.active('asset', path=True), 'cut_in', database.AssetTable) - cut_out = db.value(common.active('asset', path=True), 'cut_out', database.AssetTable) - prefix = db.value(common.active('root', path=True), 'prefix', database.BookmarkTable) - - seq, shot = common.get_sequence_and_shot(common.active('asset')) - destination_dir = config.expand_tokens( - cache_destination_dir, - asset=common.active('asset'), - shot=shot, - sequence=seq, - prefix=prefix.split('_')[-1].upper(), - studiolibrary_dir=studiolibrary_dir - ) - - # Save the current options - timeline_start = cmds.playbackOptions(animationStartTime=True, query=True) - timeline_end = cmds.playbackOptions(animationEndTime=True, query=True) - animation_start = cmds.playbackOptions(minTime=True, query=True) - animation_end = cmds.playbackOptions(maxTime=True, query=True) - - cmds.select(clear=True) - - # Create the export groups - aka_make_export_sets.run() - - # Set the cache range up - cmds.playbackOptions(animationStartTime=-50, minTime=-50, animationEndTime=cut_out, maxTime=cut_out) - cmds.currentTime(cut_in) - - for n in (cut_in, cut_out): - cmds.currentTime(n) - cmds.select(cmds.sets(controllers_set, query=True), replace=True, ne=True) - cmds.setKeyframe( - cmds.sets(controllers_set, query=True), - breakdown=False, - preserveCurveShape=False, - hierarchy='none', - controlPoints=False, - shape=True, - ) - - for node in cmds.sets(controllers_set, query=True): - attrs = cmds.listAnimatable(node) - if not attrs: - continue - for attr in attrs: - if not cmds.keyframe(attr, query=True, time=(cut_in - 51, cut_in - 1)): - continue - cmds.cutKey(attr, time=(cut_in - 51, cut_in - 1)) - - cmds.currentTime(cut_in) - - # Save the full animation - try: - mutils.saveAnim( - cmds.sets(controllers_set, query=True), - os.path.normpath(f'{destination_dir}/IbogaineMarcus_fullanim.anim'), - time=(cut_in, cut_out), - bakeConnected=False, - metadata='' - ) - except UnicodeDecodeError as e: - print(e) - - # Save the animation start pose - try: - mutils.savePose( - os.path.normpath(f'{destination_dir}/IbogaineMarcus_animstart.pose/pose.json'), - exclude_controllers1, - ) - except UnicodeDecodeError as e: - print(e) - - # Parent a null locator to the hip bake it to world and save the world animation for alter use - locator = cmds.spaceLocator(name="nullLocator")[0] - constraint = cmds.parentConstraint('IbogaineMarcus_01:body_C0_ctl', locator, maintainOffset=False)[0] - cmds.bakeResults( - locator, - time=(cut_in, cut_out), - simulation=True, - sampleBy=1, - oversamplingRate=1, - disableImplicitControl=True, - preserveOutsideKeys=True, - sparseAnimCurveBake=False, - removeBakedAttributeFromLayer=False, - bakeOnOverrideLayer=False, - minimizeRotation=True, - controlPoints=False, - shape=True - ) - - # Save the hip animation - try: - mutils.saveAnim( - [locator, ], - os.path.normpath(f'{destination_dir}/IbogaineMarcus_hip.anim'), - time=(cut_in, cut_out), - bakeConnected=False, - metadata='' - ) - except UnicodeDecodeError as e: - print(e) - - cmds.delete(constraint) - cmds.delete(locator) - - # Remove animation from body and world controllers... - for obj in exclude_controllers1: - animated_attrs = cmds.listAnimatable(obj) - animated_attrs = animated_attrs if animated_attrs else [] - for attr in animated_attrs: - cmds.cutKey(attr, clear=True) - - # ...but keep the pose at cut_in - cmds.currentTime(cut_in) - mutils.loadPose( - os.path.normpath(f'{destination_dir}/IbogaineMarcus_animstart.pose/pose.json'), - key=True - ) - - cmds.currentTime(cut_in - 51) - - mutils.loadPose( - os.path.normpath(reset_pose), - objects=list( - set(cmds.sets(controllers_set, query=True)) - set(exclude_controllers1) - set( - exclude_controllers2 - ) - ), - key=True, - namespaces=[namespace, ] - ) - - export.export_maya( - get_cache_path('camera_export', 'ma'), - cmds.sets('camera_export', query=True), cut_in, cut_out, step=1.0 - ) - export.export_alembic( - get_cache_path('camera_export', 'abc'), - cmds.sets('camera_export', query=True), cut_in, cut_out, step=1.0 - ) - export.export_alembic( - get_cache_path('IbogaineMarcus_body_export', 'abc'), - cmds.sets('IbogaineMarcus_body_export', query=True), cut_in - 51, cut_out, step=1.0 - ) - export.export_alembic( - get_cache_path('IbogaineMarcus_cloth_export', 'abc'), - cmds.sets('IbogaineMarcus_cloth_export', query=True), cut_in - 51, cut_in - 51, step=1.0 - ) - export.export_alembic( - get_cache_path('IbogaineMarcus_extra_export', 'abc'), - cmds.sets('IbogaineMarcus_extra_export', query=True), cut_in - 51, cut_out, step=1.0 - ) - - mutils.loadAnims( - [os.path.normpath(f'{destination_dir}/IbogaineMarcus_fullanim.anim'), ], - objects=cmds.sets(controllers_set, query=True), - currentTime=False, - option='replaceCompletely', - namespaces=[namespace, ] - ) - - cmds.playbackOptions(animationStartTime=cut_in, minTime=cut_in, animationEndTime=cut_out, maxTime=cut_out) diff --git a/bookmarks/maya/scripts/aka_cloth_cache_animated.py b/bookmarks/maya/scripts/aka_cloth_cache_animated.py deleted file mode 100644 index 7335728d..00000000 --- a/bookmarks/maya/scripts/aka_cloth_cache_animated.py +++ /dev/null @@ -1,270 +0,0 @@ -"""Export script for Odyssey cloth sims. - -The scripts automates the animation cache export from Maya. - -It performs the follow steps: - - Saves the current animation to studio library - - Removes the animation from the body and root controllers - - Adds a preroll with a reset pose - - Saves the animation caches - -Gergely Wootsch. -hello@gergely-wootsch.com -Studio Aka, 2023 October - -""" -import os - -import maya.cmds as cmds -from PySide2 import QtCore -from ... import common -from ... import database -from .. import base as mayabase -from .. import export -from . import aka_make_export_sets -from ...tokens import tokens - - -studiolibrary_dir = f'{common.active("server")}/{common.active("job")}/070_Assets/Character/studiolibrary' -reset_pose = f'{studiolibrary_dir}/Characters/IbogaineMarcus/Reset/A-Pose-v2-FullFK.pose/pose.json' - -cache_destination_dir = '{studiolibrary_dir}/Shots/{prefix}/{asset0}_{shot}' - -namespace = 'IbogaineMarcus_01' -controllers_set = f'{namespace}:rig_controllers_grp' - -# These controllers should not be animated when exporting the caches -exclude_controllers1 = [ - f'{namespace}:body_C0_ctl', - f'{namespace}:world_ctl', - f'{namespace}:root_C0_ctl' -] - -# These controllers should be excluded from the a-pose -exclude_controllers2 = [ - f'{namespace}:legUI_L0_ctl', - f'{namespace}:armUI_R0_ctl', - f'{namespace}:faceUI_C0_ctl', - f'{namespace}:legUI_R0_ctl', - f'{namespace}:spineUI_C0_ctl', - f'{namespace}:armUI_L0_ctl' -] - - -def get_cache_path(set_name, ext, makedir=True): - """Get the path of the output cache file. - - """ - workspace = cmds.workspace(q=True, fn=True) - - export_dir = mayabase.DEFAULT_CACHE_DIR.format( - export_dir=tokens.get_folder(tokens.CacheFolder), - ext=tokens.get_subfolder(tokens.CacheFolder, ext) - ) - file_path = mayabase.CACHE_PATH.format( - workspace=workspace, - export_dir=export_dir, - set=set_name, - ext=ext - ) - file_path = mayabase.sanitize_namespace(file_path) - - file_info = QtCore.QFileInfo(file_path) - _version = 1 - while True: - version = 'v' + f'{_version}'.zfill(3) - - file_path = '{dir}/{basename}_{version}.{ext}'.format( - dir=file_info.dir().path(), - basename=file_info.completeBaseName(), - version=version, - ext=file_info.suffix() - ) - - if not QtCore.QFileInfo(file_path).exists(): - break - if _version >= 999: - break - - _version += 1 - - if makedir: - QtCore.QFileInfo(file_path).dir().mkpath('.') - return file_path - - -@common.debug -@common.error -def run(): - try: - import mutils - except ImportError: - raise RuntimeError('Could not export caches. Is Studio Library installed?') - - config = tokens.get(*common.active('root', args=True)) - - db = database.get(*common.active('root', args=True)) - cut_in = db.value(common.active('asset', path=True), 'cut_in', database.AssetTable) - cut_out = db.value(common.active('asset', path=True), 'cut_out', database.AssetTable) - prefix = db.value(common.active('root', path=True), 'prefix', database.BookmarkTable) - - seq, shot = common.get_sequence_and_shot(common.active('asset')) - destination_dir = config.expand_tokens( - cache_destination_dir, - asset=common.active('asset'), - shot=shot, - sequence=seq, - prefix=prefix.split('_')[-1].upper(), - studiolibrary_dir=studiolibrary_dir - ) - - # Save the current options - timeline_start = cmds.playbackOptions(animationStartTime=True, query=True) - timeline_end = cmds.playbackOptions(animationEndTime=True, query=True) - animation_start = cmds.playbackOptions(minTime=True, query=True) - animation_end = cmds.playbackOptions(maxTime=True, query=True) - - cmds.select(clear=True) - - # Create the export groups - aka_make_export_sets.run() - - # Set the cache range up - cmds.playbackOptions(animationStartTime=-50, minTime=-50, animationEndTime=cut_out, maxTime=cut_out) - cmds.currentTime(cut_in) - - for n in (cut_in, cut_out): - cmds.currentTime(n) - cmds.select(cmds.sets(controllers_set, query=True), replace=True, ne=True) - cmds.setKeyframe( - cmds.sets(controllers_set, query=True), - breakdown=False, - preserveCurveShape=False, - hierarchy='none', - controlPoints=False, - shape=True, - ) - - for node in cmds.sets(controllers_set, query=True): - attrs = cmds.listAnimatable(node) - if not attrs: - continue - for attr in attrs: - if not cmds.keyframe(attr, query=True, time=(cut_in - 51, cut_in - 1)): - continue - cmds.cutKey(attr, time=(cut_in - 51, cut_in - 1)) - - cmds.currentTime(cut_in) - - # Save the full animation - try: - mutils.saveAnim( - cmds.sets(controllers_set, query=True), - os.path.normpath(f'{destination_dir}/IbogaineMarcus_fullanim.anim'), - time=(cut_in, cut_out), - bakeConnected=False, - metadata='' - ) - except UnicodeDecodeError as e: - print(e) - - # Save the animation start pose - try: - mutils.savePose( - os.path.normpath(f'{destination_dir}/IbogaineMarcus_animstart.pose/pose.json'), - exclude_controllers1, - ) - except UnicodeDecodeError as e: - print(e) - - # Parent a null locator to the hip bake it to world and save the world animation for alter use - locator = cmds.spaceLocator(name="nullLocator")[0] - constraint = cmds.parentConstraint('IbogaineMarcus_01:body_C0_ctl', locator, maintainOffset=False)[0] - cmds.bakeResults( - locator, - time=(cut_in, cut_out), - simulation=True, - sampleBy=1, - oversamplingRate=1, - disableImplicitControl=True, - preserveOutsideKeys=True, - sparseAnimCurveBake=False, - removeBakedAttributeFromLayer=False, - bakeOnOverrideLayer=False, - minimizeRotation=True, - controlPoints=False, - shape=True - ) - - # Save the hip animation - try: - mutils.saveAnim( - [locator, ], - os.path.normpath(f'{destination_dir}/IbogaineMarcus_hip.anim'), - time=(cut_in, cut_out), - bakeConnected=False, - metadata='' - ) - except UnicodeDecodeError as e: - print(e) - - cmds.delete(constraint) - cmds.delete(locator) - - # Remove animation from body and world controllers... - # for obj in exclude_controllers1: - # animated_attrs = cmds.listAnimatable(obj) - # animated_attrs = animated_attrs if animated_attrs else [] - # for attr in animated_attrs: - # cmds.cutKey(attr, clear=True) - - # ...but keep the pose at cut_in - cmds.currentTime(cut_in) - mutils.loadPose( - os.path.normpath(f'{destination_dir}/IbogaineMarcus_animstart.pose/pose.json'), - key=True - ) - - cmds.currentTime(cut_in - 51) - - mutils.loadPose( - os.path.normpath(reset_pose), - objects=list( - set(cmds.sets(controllers_set, query=True)) - set(exclude_controllers1) - set( - exclude_controllers2 - ) - ), - key=True, - namespaces=[namespace, ] - ) - - export.export_maya( - get_cache_path('camera_export', 'ma'), - cmds.sets('camera_export', query=True), cut_in, cut_out, step=1.0 - ) - export.export_alembic( - get_cache_path('camera_export', 'abc'), - cmds.sets('camera_export', query=True), cut_in, cut_out, step=1.0 - ) - export.export_alembic( - get_cache_path('IbogaineMarcus_body_export', 'abc'), - cmds.sets('IbogaineMarcus_body_export', query=True), cut_in - 51, cut_out, step=1.0 - ) - export.export_alembic( - get_cache_path('IbogaineMarcus_cloth_export', 'abc'), - cmds.sets('IbogaineMarcus_cloth_export', query=True), cut_in - 51, cut_in - 51, step=1.0 - ) - export.export_alembic( - get_cache_path('IbogaineMarcus_extra_export', 'abc'), - cmds.sets('IbogaineMarcus_extra_export', query=True), cut_in - 51, cut_out, step=1.0 - ) - - mutils.loadAnims( - [os.path.normpath(f'{destination_dir}/IbogaineMarcus_fullanim.anim'), ], - objects=cmds.sets(controllers_set, query=True), - currentTime=False, - option='replaceCompletely', - namespaces=[namespace, ] - ) - - cmds.playbackOptions(animationStartTime=cut_in, minTime=cut_in, animationEndTime=cut_out, maxTime=cut_out) diff --git a/bookmarks/maya/scripts/scripts.json b/bookmarks/maya/scripts/scripts.json index d426ae81..1baef0a9 100644 --- a/bookmarks/maya/scripts/scripts.json +++ b/bookmarks/maya/scripts/scripts.json @@ -10,19 +10,15 @@ "description": "" }, "2": { - "name": "Studio Aka | Odyssey | Export IbogaineMarcus Cloth caches for Simulation (static)", - "module": "aka_cloth_cache", + "name": "Studio Aka | Odyssey | Export Character Caches", + "module": "aka_character_caches", + "needs_active": "asset", "description": "Exports cloth caches for simulation" }, "3": { - "name": "Studio Aka | Odyssey | Export IbogaineMarcus Cloth caches for Simulation (animated)", - "module": "aka_cloth_cache_animated", - "description": "Exports cloth caches for simulation" - }, - "4": { "name": "separator" }, - "5": { + "4": { "name": "Reset joint orientations", "module": "reset_joint_orientations", "description": "Resets bone orientation values to zero without moving the bones" diff --git a/bookmarks/publish.py b/bookmarks/publish.py index c01825b2..a342607e 100644 --- a/bookmarks/publish.py +++ b/bookmarks/publish.py @@ -587,12 +587,10 @@ def save_changes(self): payload = get_payload(kwargs, destination) config = tokens.get(*common.active('root', args=True)) - data = config.data() - flag = next( - (v['filter'] for v in data[tokens.PublishConfig].values() if - v['value'] == kwargs['publish_template']), None - ) - exts = config.get_extensions(flag) + + flag = tokens.SceneFormat | tokens.ImageFormat | tokens.MovieFormat | tokens.AudioFormat | tokens.CacheFormat + exts = config.get_extensions(flag, force=True) + if kwargs['ext'] not in exts: raise RuntimeError( f'"{kwargs["ext"]}" is not a valid publish format.\n' diff --git a/bookmarks/rsc/stylesheet.qss b/bookmarks/rsc/stylesheet.qss index d43aaf3c..a5b7bdd1 100644 --- a/bookmarks/rsc/stylesheet.qss +++ b/bookmarks/rsc/stylesheet.qss @@ -185,10 +185,8 @@ QComboBox QListView::item:hover {{ QLineEdit, -QLineEdit:disabled, -QTextEdit, -QTextEdit:disabled {{ - background-color: {color_opaque}; +QTextEdit {{ + background-color: transparent; height: {size_row_height2}px; max-height: {size_row_height2}px; min-height: {size_row_height2}px; @@ -201,7 +199,12 @@ QTextEdit:disabled {{ QTextEdit:disabled, QLineEdit:disabled {{ color: {color_disabled_text}; - background-color: {color_dark_background}; + background-color: transparent; +}} + +QLineEdit:focus, +QTextEdit:focus {{ + background-color: {color_opaque}; }} QTextBrowser {{ diff --git a/bookmarks/shotgun/sg_publish_clip.py b/bookmarks/shotgun/sg_publish_clip.py index b114c820..4235d9ca 100644 --- a/bookmarks/shotgun/sg_publish_clip.py +++ b/bookmarks/shotgun/sg_publish_clip.py @@ -327,7 +327,6 @@ def save_changes(self): disable_animation=True, parent=self ) - QtWidgets.QApplication.instance().processEvents(QtCore.QEventLoop.ExcludeUserInputEvents) existing_version = sg.find_one( 'Version', [['code', 'is', data['name']]] @@ -370,7 +369,6 @@ def save_changes(self): disable_animation=True, parent=self ) - QtWidgets.QApplication.instance().processEvents(QtCore.QEventLoop.ExcludeUserInputEvents) cut_data = { 'project': data['project_entity'], @@ -392,7 +390,6 @@ def save_changes(self): disable_animation=True, parent=self ) - QtWidgets.QApplication.instance().processEvents(QtCore.QEventLoop.ExcludeUserInputEvents) cut_item_data = { 'project': data['project_entity'], @@ -418,7 +415,6 @@ def save_changes(self): disable_animation=True, parent=self ) - QtWidgets.QApplication.instance().processEvents(QtCore.QEventLoop.ExcludeUserInputEvents) published_file_data = { 'project': data['project_entity'], @@ -459,7 +455,6 @@ def save_changes(self): disable_animation=True, parent=self ) - QtWidgets.QApplication.instance().processEvents(QtCore.QEventLoop.ExcludeUserInputEvents) # Upload the actual file to ShotGrid sg.upload("Version", version['id'], data['file_path'], field_name='sg_uploaded_movie') diff --git a/bookmarks/tokens/tokens.py b/bookmarks/tokens/tokens.py index 2838392b..c9434c93 100644 --- a/bookmarks/tokens/tokens.py +++ b/bookmarks/tokens/tokens.py @@ -71,6 +71,7 @@ SceneFolder = 'scene' CacheFolder = 'cache' +CaptureFolder = 'captures' RenderFolder = 'render' DataFolder = 'data' ReferenceFolder = 'reference' @@ -81,7 +82,7 @@ #: The default token value configuration DEFAULT_TOKEN_CONFIG = { FileFormatConfig: { - 0: { + common.idx(reset=True, start=0): { 'name': 'Scene Formats', 'flag': SceneFormat, 'value': common.sort_words( @@ -90,7 +91,7 @@ ), 'description': 'Scene file formats' }, - 1: { + common.idx(): { 'name': 'Image Formats', 'flag': ImageFormat, 'value': common.sort_words( @@ -98,7 +99,7 @@ ), 'description': 'Image file formats' }, - 2: { + common.idx(): { 'name': 'Cache Formats', 'flag': CacheFormat, 'value': common.sort_words( @@ -107,7 +108,7 @@ ), 'description': 'CG cache formats' }, - 3: { + common.idx(): { 'name': 'Movie Formats', 'flag': MovieFormat, 'value': common.sort_words( @@ -115,7 +116,7 @@ ), 'description': 'Movie file formats' }, - 4: { + common.idx(): { 'name': 'Audio Formats', 'flag': AudioFormat, 'value': common.sort_words( @@ -123,7 +124,7 @@ ), 'description': 'Audio file formats' }, - 5: { + common.idx(): { 'name': 'Document Formats', 'flag': DocFormat, 'value': common.sort_words( @@ -131,7 +132,7 @@ ), 'description': 'Audio file formats' }, - 6: { + common.idx(): { 'name': 'Script Formats', 'flag': ScriptFormat, 'value': common.sort_words( @@ -139,7 +140,7 @@ ), 'description': 'Various script file formats' }, - 7: { + common.idx(): { 'name': 'Miscellaneous Formats', 'flag': MiscFormat, 'value': common.sort_words( @@ -149,103 +150,36 @@ }, }, FileNameConfig: { - 0: { - 'name': 'Asset Scene Task', - 'value': '{prefix}_{asset}_{mode}_{element}_{user}_{version}.{ext}', + common.idx(reset=True, start=0): { + 'name': 'Asset Scene', + 'value': '{prefix}_{asset}_{element}.{version}.{ext}', 'description': 'Uses the project prefix, asset, task, element, ' - 'user and version names' + 'user and version names', }, - 1: { - 'name': 'Asset Scene File (without task and element)', - 'value': '{prefix}_{asset}_{user}_{version}.{ext}', - 'description': 'Uses the project prefix, asset, user and version names' - }, - 2: { - 'name': 'Shot Scene Task', - 'value': '{prefix}_{seq}_{shot}_{mode}_{element}_{user}_{version}.{ext}', - 'description': 'Uses the project prefix, sequence, shot, mode, element, ' - 'user and version names' - }, - 3: { - 'name': 'Shot Scene Task (without task and element)', - 'value': '{prefix}_{seq}_{shot}_{user}_{version}.{ext}', - 'description': 'Uses the project prefix, sequence, shot, user and ' - 'version names' - }, - 4: { - 'name': 'Versioned Element', - 'value': '{element}_{version}.{ext}', - 'description': 'File name with an element and version name' - }, - 5: { - 'name': 'Non-Versioned Element', - 'value': '{element}.{ext}', - 'description': 'A non-versioned element file' - }, - 6: { - 'name': 'Studio Aka - Shot', + common.idx(): { + 'name': 'Shot Scene', 'value': '{prefix}_{seq}_{shot}_{mode}_{element}.{version}.{ext}', - 'description': 'Studio Aka - ShotGrid file template' - }, - 7: { - 'name': 'Studio Aka - Asset', - 'value': '{asset1}_{asset5}_{element}.{version}.{ext}', - 'description': 'Studio Aka - ShotGrid file template' + 'description': 'Template name used save shot scene files', } }, PublishConfig: { - 0: { - 'name': 'Shot Task', - 'value': '{server}/{job}/publish/{sequence}_{shot}/{task}/{element}/{prefix}_{sequence}_{shot}_{task}_{' - 'element}.{ext}', - 'description': 'Publish a shot task element', - 'filter': SceneFormat | ImageFormat | MovieFormat | CacheFormat, + common.idx(): { + 'name': 'Publish: Asset Item', + 'value': '{server}/{job}/{root}/{asset}/publish/{prefix}_{asset}_{task}_{element}.{ext}', + 'description': 'Publish an asset scene', }, - 1: { - 'name': 'Asset Task', - 'value': '{server}/{job}/publish/asset_{asset}/{task}/{element}/{prefix}_{asset}_{task}_{element}.{ext}', - 'description': 'Publish an asset task element', - 'filter': SceneFormat | ImageFormat | MovieFormat | CacheFormat, - }, - 3: { - 'name': 'Shot Thumbnail', - 'value': '{server}/{job}/publish/{sequence}_{shot}/thumbnail.{ext}', - 'description': 'Publish an shot thumbnail', - 'filter': ImageFormat, - }, - 4: { - 'name': 'Asset Thumbnail', - 'value': '{server}/{job}/publish/asset_{asset}/thumbnail.{ext}', - 'description': 'Publish an asset thumbnail', - 'filter': ImageFormat, - }, - 5: { - 'name': 'Asset (local publish)', - 'value': '{server}/{job}/{root}/{asset}/publish/{prefix}_{asset1}_{task}_{element}.{ext}', - 'description': 'Publish an asset', - 'filter': SceneFormat | ImageFormat | MovieFormat | CacheFormat, - }, - 6: { - 'name': 'Shot (local publish)', + common.idx(): { + 'name': 'Publish: Shot Item', 'value': '{server}/{job}/{root}/{asset}/publish/{prefix}_{seq}_{shot}_{element}.{ext}', - 'description': 'Publish a shot element', - 'filter': SceneFormat | ImageFormat | MovieFormat | CacheFormat, - }, - 7: { - 'name': 'Studio Aka - vCur', - 'value': '{server}/{job}/{root}/{asset0}/{asset1}/{asset2}/publish/{asset4}/{asset5}/{asset1}_{asset5}_{' - 'element}.vCur.{ext}', - 'description': 'Studio Aka - vCur', - 'filter': SceneFormat | ImageFormat | MovieFormat | CacheFormat, + 'description': 'Publish a shot scene', }, }, AssetFolderConfig: { - 0: { + common.idx(reset=True, start=0): { 'name': CacheFolder, 'value': CacheFolder, 'description': 'Alembic, FBX, OBJ and other CG caches', - 'filter': SceneFormat | ImageFormat | MovieFormat | AudioFormat | - CacheFormat, + 'filter': SceneFormat | ImageFormat | MovieFormat | AudioFormat | CacheFormat, 'subfolders': { 0: { 'name': 'abc', @@ -314,29 +248,26 @@ } } }, - 1: { + common.idx(): { 'name': DataFolder, 'value': DataFolder, 'description': 'Temporary data files, or content generated by ' 'applications', 'filter': AllFormat, - 'subfolders': {}, }, - 2: { + common.idx(): { 'name': ReferenceFolder, 'value': ReferenceFolder, 'description': 'References, e.g., images, videos or sound files', 'filter': ImageFormat | DocFormat | AudioFormat | MovieFormat, - 'subfolders': {}, }, - 3: { + common.idx(): { 'name': RenderFolder, 'value': 'images', 'description': 'Render layer outputs', 'filter': ImageFormat | AudioFormat | MovieFormat, - 'subfolders': {}, }, - 4: { + common.idx(): { 'name': SceneFolder, 'value': 'scenes', 'description': 'Project and scene files', @@ -389,39 +320,43 @@ }, }, }, - 5: { + common.idx(): { 'name': PublishFolder, 'value': 'publish', 'description': 'Asset publish files', + 'filter': SceneFormat | ImageFormat | MovieFormat | AudioFormat + }, + common.idx(): { + 'name': CaptureFolder, + 'value': 'captures', + 'description': 'Viewport captures and preview files', 'filter': ImageFormat | MovieFormat | AudioFormat }, - 6: { + common.idx(): { 'name': TextureFolder, 'value': 'sourceimages', 'description': '2D and 3D texture files', 'filter': ImageFormat | MovieFormat | AudioFormat, - 'subfolders': {}, }, - 7: { + common.idx(): { 'name': MiscFolder, 'value': 'other', 'description': 'Miscellaneous asset files', 'filter': AllFormat, - 'subfolders': {}, } }, FFMpegTCConfig: { - 0: { + common.idx(reset=True, start=0): { 'name': 'Shot', 'value': '{job} | {sequence}-{shot}-{task}-{version} | {date} {user} | {in_frame}-{out_frame}', 'description': 'Timecode to use for shots' }, - 1: { + common.idx(): { 'name': 'Asset', 'value': '{job} | {asset}-{task}-{version} | {date} {user}', 'description': 'Timecode to use for assets' }, - 2: { + common.idx(): { 'name': 'Date and user', 'value': '{job} | {date} {user}', 'description': 'Sparse timecode with the date and username' diff --git a/bookmarks/tokens/tokens_editor.py b/bookmarks/tokens/tokens_editor.py index 6a0a1064..66e56205 100644 --- a/bookmarks/tokens/tokens_editor.py +++ b/bookmarks/tokens/tokens_editor.py @@ -3,6 +3,7 @@ See the :mod:`bookmarks.tokens.tokens` for the interface details. """ +import copy import functools from PySide2 import QtWidgets, QtCore @@ -13,13 +14,34 @@ from .. import ui from ..editor import base -SECTIONS = ( - (tokens.FileNameConfig, 'File name template'), - (tokens.AssetFolderConfig, 'Asset folders template'), - (tokens.FileFormatConfig, 'Format whitelist'), - (tokens.PublishConfig, 'Publish template'), - (tokens.FFMpegTCConfig, 'Timecode template'), -) +MoveUp = 0 +MoveDown = 1 + +SECTIONS = { + tokens.FileNameConfig: { + 'name': 'File Name Templates', + 'description': 'File name templates are used to define the names scene files. These usually include the ' + 'project\'s prefix, sequence and shot numbers, and the task name.', + }, + tokens.PublishConfig: { + 'name': 'Publish Templates', + 'description': 'Publish templates are used to define the save location of published files.', + }, + tokens.FFMpegTCConfig: { + 'name': 'Timecode Template', + 'description': 'The template used by ffmpeg for video text overlays and burn-ins.', + }, + tokens.AssetFolderConfig: { + 'name': 'Asset Folders', + 'description': 'Common folders that define the principal folders of an asset item. These values are' + 'used when browsing files, saving scene files and publishing items.', + }, + tokens.FileFormatConfig: { + 'name': 'Format Whitelist', + 'description': 'The list of file formats that are allowed to be shown.', + + }, +} def _set(d, keys, v): @@ -123,7 +145,7 @@ class FormatEditor(QtWidgets.QDialog): """ def __init__(self, *args, **kwargs): - super(FormatEditor, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) self.listwidget = None self.setWindowTitle('Edit Formats') @@ -166,18 +188,18 @@ def _create_ui(self): class SubfolderEditor(QtWidgets.QDialog): - """A popup editor used to edit the subfolders of a task folder. + """A popup editor used to edit the sub-folders of a task folder. """ def __init__(self, section, k, v, data, parent=None): - super(SubfolderEditor, self).__init__(parent=parent) + super().__init__(parent=parent) self.section = section self.k = k self.v = v self.data = data - self.setWindowTitle('Edit Subfolders') + self.setWindowTitle('Edit Sub-folders') self._create_ui() def _create_ui(self): @@ -186,7 +208,7 @@ def _create_ui(self): self.layout().setContentsMargins(o, o, o, o) self.layout().setSpacing(o) - main_grp = base.add_section('', 'Edit Subfolders', self) + main_grp = base.add_section('', 'Edit Sub-folders', self) grp = ui.get_group(parent=main_grp) for _k, _v in self.v['subfolders'].items(): @@ -235,7 +257,7 @@ class TokenConfigEditor(QtWidgets.QWidget): """ def __init__(self, server, job, root, parent=None): - super(TokenConfigEditor, self).__init__(parent=parent) + super().__init__(parent=parent) self.server = server self.job = job self.root = root @@ -250,6 +272,8 @@ def __init__(self, server, job, root, parent=None): self.init_data() + self.ui_groups = {} + self._create_ui() self._connect_signals() @@ -258,41 +282,178 @@ def _create_ui(self): QtWidgets.QVBoxLayout(self) o = common.size(common.size_margin) - h = common.size(common.size_row_height) self.layout().setContentsMargins(0, 0, 0, 0) self.layout().setSpacing(o) + # Re-fetch the config data from the database + + for k in SECTIONS: + self._add_section(k) + + def _add_section_item(self, parent, section, data): + """Adds a new item to a section. + + Args: + parent (QtWidgets.QWidget): The parent widget. + section (str): The section to add the item to. + data (dict): The data to add. + + Returns: + QtWidgets.QWidget: The widget that was added. + + """ + grp = ui.get_group(vertical=False, parent=parent) + grp.setObjectName('section_item_group') + grp.section = section + + _row1 = ui.add_row(None, height=None, vertical=True, parent=grp) + + for _k in ('name', 'value', 'description'): + row = ui.add_row(_k.title(), height=common.size(common.size_row_height), parent=_row1) + editor = ui.LineEdit(parent=row) + editor.setObjectName(f'section_item_{_k}') + + editor.setAlignment(QtCore.Qt.AlignRight) + editor.setText(data[_k]) + editor.setPlaceholderText(f'Edit {_k}...') + + if _k in ('name', 'description'): + editor.setStyleSheet( + f'color: {common.rgb(common.color_secondary_text)};' + ) + else: + editor.setStyleSheet( + f'color: {common.rgb(common.color_text)};' + ) + row.layout().addWidget(editor, 1) + + _row2 = ui.add_row(None, height=None, vertical=False, parent=None) + grp.layout().addWidget(_row2, 0) + + button = ui.ClickableIconButton( + 'add_circle', + (common.color(common.color_text), common.color(common.color_text)), + common.size(common.size_margin), + description='Insert token', + parent=_row2 + ) + _row2.layout().addWidget(button, 0) + value_editor = [x for x in grp.findChildren(QtWidgets.QLineEdit) if x.objectName() == 'section_item_value'][0] + button.clicked.connect( + functools.partial(self.show_token_editor, value_editor) + ) + + button = ui.ClickableIconButton( + 'arrow_up', + (common.color(common.color_text), common.color(common.color_text)), + common.size(common.size_margin), + description='Move item up', + parent=_row2 + ) + _row2.layout().addWidget(button, 0) + button.clicked.connect( + functools.partial(self.move_item, grp, MoveUp) + ) + + button = ui.ClickableIconButton( + 'arrow_down', + (common.color(common.color_text), common.color(common.color_text)), + common.size(common.size_margin), + description='Move item down', + parent=_row2 + ) + _row2.layout().addWidget(button, 0) + button.clicked.connect( + functools.partial(self.move_item, grp, MoveDown) + ) + + button = ui.ClickableIconButton( + 'archive', + (common.color(common.color_red), common.color(common.color_red)), + common.size(common.size_margin), + description='Remove item', + parent=_row2 + ) + _row2.layout().addWidget(button, 0) + button.clicked.connect( + functools.partial(self.remove_item, grp) + ) + + return grp + + def _add_section(self, section): # Re-fetch the config data from the database data = self.tokens.data(force=True) - for section, section_name in SECTIONS: - if section not in data: - continue - if not isinstance(data[section], dict): - log.error('Invalid data.') + if section not in data: + print(f'Invalid section: {section}. Skipping.') + return + + if not isinstance(data[section], dict): + log.error('Invalid data.') + return + + for k, v in data[section].items(): + if not isinstance( + v, dict + ) or 'name' not in v or 'description' not in v: + log.error(f'Invalid data. Key: {k}, value: {v}') return - for k, v in data[section].items(): - if not isinstance( - v, dict - ) or 'name' not in v or 'description' not in v: - log.error(f'Invalid data. Key: {k}, value: {v}') - return - - main_grp = base.add_section( - '', - section_name, - self, - color=common.color(common.color_dark_background) + + h = common.size(common.size_row_height) + + main_grp = base.add_section( + 'file', + SECTIONS[section]['name'], + self, + color=common.color(common.color_dark_background) + ) + + self.ui_groups[section] = main_grp + self.header_buttons.append((SECTIONS[section]['name'], main_grp)) + + _grp = ui.get_group(parent=main_grp) + + # Control buttons + control_row = ui.add_row( + None, height=None, parent=_grp + ) + + ui.add_description( + SECTIONS[section]['description'], + height=None, + label=None, + parent=control_row + ) + control_row.layout().addStretch(1) + + if section in (tokens.PublishConfig, tokens.FFMpegTCConfig, tokens.FileNameConfig): + add_button = ui.ClickableIconButton( + 'add', + (common.color(common.color_green), common.color(common.color_green)), + common.size(common.size_margin) * 1.5, + description='Add new item', + parent=control_row ) + control_row.layout().addWidget(add_button, 0) - # Save header data for later use - self.header_buttons.append((section_name, main_grp)) + add_button.clicked.connect( + functools.partial(self.add_item, _grp, section) + ) - _grp = ui.get_group(parent=main_grp) - for k, v in data[section].items(): + reset_button = ui.PaintedButton('Revert to defaults') + reset_button.clicked.connect(functools.partial(self.restore_defaults, section)) + + control_row.layout().addWidget(reset_button, 0) + + for k, v in data[section].items(): + + if section in (tokens.PublishConfig, tokens.FFMpegTCConfig, tokens.FileNameConfig): + grp = self._add_section_item(_grp, section, v) + else: _name = v['name'].title() - _name = f'{_name} Folder' if section == tokens.AssetFolderConfig else _name row = ui.add_row(_name, height=h, parent=_grp) + row.setStatusTip(v['description']) row.setWhatsThis(v['description']) row.setToolTip(v['description']) @@ -302,6 +463,8 @@ def _create_ui(self): editor.setAlignment(QtCore.Qt.AlignRight) editor.setText(v['value']) + row.layout().addWidget(editor, 1) + # Save current data key = f'{section}/{k}/value' self.current_data[key] = v['value'] @@ -310,22 +473,6 @@ def _create_ui(self): functools.partial(self.text_changed, key, editor) ) - row.layout().addWidget(editor) - - if section == tokens.PublishConfig or section == tokens.FFMpegTCConfig: - button = ui.PaintedButton('+', parent=row) - button.clicked.connect( - functools.partial(self.show_token_editor, editor) - ) - row.layout().addWidget(button, 0) - - if section == tokens.FileNameConfig: - button = ui.PaintedButton('+', parent=row) - button.clicked.connect( - functools.partial(self.show_token_editor, editor) - ) - row.layout().addWidget(button, 0) - if section == tokens.AssetFolderConfig: button = ui.PaintedButton('Formats', parent=row) row.layout().addWidget(button, 0) @@ -349,24 +496,119 @@ def _create_ui(self): else: button.setDisabled(True) + def init_data(self): """Initializes data. """ self.tokens = tokens.get(self.server, self.job, self.root) - def contextMenuEvent(self, event): - action = QtWidgets.QAction( - 'Reset all template settings to their defaults' + @QtCore.Slot(QtWidgets.QWidget) + @QtCore.Slot(int) + def move_item(self, widget, direction): + """Moves an item up or down in its own section. + + Args: + widget (QtWidgets.QWidget): The widget to move. + direction (int): The direction to move the widget in. + + """ + # Get the layout index of the widget + layout = widget.parent().layout() + index = layout.indexOf(widget) + + # Get the new index + if direction == MoveUp: + min_index = 1 # Skip the first row as this is the control row + new_index = index - 1 + new_index = min_index if new_index < min_index else new_index + else: + new_index = index + 1 + new_index = layout.count() - 1 if new_index >= layout.count() else new_index + + # Set the new layout index + layout.insertWidget(new_index, layout.takeAt(index).widget()) + + @QtCore.Slot(QtWidgets.QWidget) + @QtCore.Slot(str) + @common.error + def add_item(self, parent, section): + """Adds a new item to a section. + + Args: + parent (QtWidgets.QWidget): The parent widget. + section (str): The section to add the item to. + + """ + grp = self._add_section_item(parent, section, {'name': '', 'value': '', 'description': ''}) + + # Find the value editor + editor = next( + (x for x in grp.findChildren(QtWidgets.QLineEdit) if x.objectName() == 'section_item_value'), + None ) - action.triggered.connect(self.restore_to_defaults) + if not editor: + raise RuntimeError('Unable to find the value editor.') + editor.setFocus(QtCore.Qt.PopupFocusReason) + self.window().scroll_to_section(grp) - menu = QtWidgets.QMenu(parent=self) - menu.addAction(action) - pos = self.mapToGlobal(event.pos()) - menu.move(pos) - menu.exec_() - menu.deleteLater() + @QtCore.Slot(QtWidgets.QWidget) + def remove_item(self, widget): + """Removes an item from a section. + + Args: + widget (QtWidgets.QWidget): The widget to remove. + + """ + # Prompt the user for confirmation + if common.show_message( + 'Are you sure you want to remove this item?', + body='This action cannot be undone.', + buttons=[common.YesButton, common.CancelButton], + modal=True, + ) == QtWidgets.QDialog.Rejected: + return + layout = widget.parent().layout() + layout.removeWidget(widget) + widget.deleteLater() + + @QtCore.Slot(str) + def restore_defaults(self, section): + """Restores the default values for a given section. + + Args: + section (str): The section to restore. + + """ + if common.show_message( + 'Are you sure you want to restore the default values?', + body='Any custom values will be permanently lost.', + buttons=[common.YesButton, common.CancelButton], + modal=True, + ) == QtWidgets.QDialog.Rejected: + return False + + if section not in self.ui_groups: + print(f'Invalid section: {section}. Skipping.') + return + + if section not in self.tokens.data(): + print(f'Invalid section: {section}. Skipping.') + return + + if not self.ui_groups[section]: + print(f'Invalid section: {section}. Skipping.') + return + + if not self.write_default_to_database(section): + return + + for k in SECTIONS: + self.ui_groups[k].deleteLater() + self.ui_groups[k] = None + del self.ui_groups[k] + + self._add_section(k) @QtCore.Slot(str) @QtCore.Slot(dict) @@ -398,17 +640,29 @@ def show_filter_editor(self, key, v, data): ) editor.exec_() - @QtCore.Slot() - def restore_to_defaults(self): - if common.show_message( - 'Are you sure you want to restore all templates to the default value?', - body='Your custom settings will be permanently lost.', - buttons=[common.YesButton, common.CancelButton], - modal=True, - ) == QtWidgets.QDialog.Rejected: - return - self.tokens.set_data(tokens.DEFAULT_TOKEN_CONFIG.copy()) - self.window().close() + def write_default_to_database(self, section): + """Restore the default values for a given section and write them to the database. + + Args: + section (str): The section to restore. + + Returns: + bool: True if successful. + + """ + data = self.tokens.data(force=True) + if section not in data: + print(f'Invalid section: {section}. Skipping.') + return False + if section not in tokens.DEFAULT_TOKEN_CONFIG: + print(f'Invalid section: {section}. Skipping.') + return False + + default_section = copy.deepcopy(tokens.DEFAULT_TOKEN_CONFIG[section]) + data[section] = default_section + + self.tokens.set_data(data) + return True @QtCore.Slot(str) @QtCore.Slot(dict) @@ -423,6 +677,12 @@ def show_subfolders_editor(self, section, k, v, data): @QtCore.Slot(QtWidgets.QWidget) def show_token_editor(self, editor): + """Shows the token editor. + + Args: + editor (QtWidgets.QWidget): The editor to insert the token into. + + """ w = TokenEditor(self.server, self.job, self.root, parent=editor) w.tokenSelected.connect(editor.insert) w.exec_() @@ -462,12 +722,38 @@ def save_changes(self): """Saves changes. """ - if not self.changed_data: - return - data = self.tokens.data() + # Retrieve the current data from the database + data = self.tokens.data(force=True) + + # Update the data with the changed values for keys, v in self.changed_data.copy().items(): _set(data, keys, v) del self.changed_data[keys] + + for section in (tokens.PublishConfig, tokens.FFMpegTCConfig, tokens.FileNameConfig): + if section not in data: + print(f'"{section}" not found in data! Skipping.') + + # Reset the current data section and replace it with the values in the UI. + data[section] = {} + + # Let's find the list of section group widgets + items = [f for f in self.findChildren(QtWidgets.QWidget, 'section_item_group') if f.section == section] + for item in items: + + name_editor = item.findChildren(QtWidgets.QWidget, 'section_item_name')[0] + value_editor = item.findChildren(QtWidgets.QWidget, 'section_item_value')[0] + description_editor = item.findChildren(QtWidgets.QWidget, 'section_item_description')[0] + + if not all((name_editor.text(), value_editor.text())): + continue + + data[section][len(data[section])] = { + 'name': name_editor.text(), + 'value': value_editor.text(), + 'description': description_editor.text() if description_editor.text() else '', + } + self.tokens.set_data(data) def _connect_signals(self): diff --git a/bookmarks/ui.py b/bookmarks/ui.py index 9df62f01..aa058bb9 100644 --- a/bookmarks/ui.py +++ b/bookmarks/ui.py @@ -283,7 +283,7 @@ class LineEdit(QtWidgets.QLineEdit): """Custom line edit widget with a single underline.""" def __init__(self, parent=None): - super(LineEdit, self).__init__(parent=parent) + super().__init__(parent=parent) self.setSizePolicy( QtWidgets.QSizePolicy.MinimumExpanding, QtWidgets.QSizePolicy.MinimumExpanding, @@ -1196,7 +1196,7 @@ def add_line_edit(label, parent=None): def add_description( - text, label=' ', color=common.color(common.color_secondary_text), parent=None + text, label=' ', height=None, color=common.color(common.color_secondary_text), parent=None ): """Utility method for adding a description field. @@ -1204,7 +1204,7 @@ def add_description( QLabel: the added QLabel. """ - row = add_row(label, height=None, parent=parent) + row = add_row(label, height=height, parent=parent) row.layout().setSpacing(0) label = Label(text, color=color, parent=parent) From 37f8fab136289f6850be2801830f2a5d940094e7 Mon Sep 17 00:00:00 2001 From: Gergely Wootsch Date: Thu, 26 Oct 2023 15:29:17 +0200 Subject: [PATCH 22/32] Bump version number --- README.md | 2 +- bookmarks/__init__.py | 4 +- bookmarks/maya/plugin.py | 2 +- .../maya/scripts/aka_character_caches.py | 37 +++++++++++++++++++ docs/src/conf.py | 2 +- docs/src/guide.rst | 2 +- package/CMakeLists.txt | 2 +- 7 files changed, 44 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index fa2c76cb..7dd42fa4 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ - +

diff --git a/bookmarks/__init__.py b/bookmarks/__init__.py index a4817da4..9841ac9d 100644 --- a/bookmarks/__init__.py +++ b/bookmarks/__init__.py @@ -76,7 +76,7 @@ .. |label3| image:: https://img.shields.io/badge/Platform-Windows-lightgrey :height: 18 -.. |label4| image:: https://img.shields.io/badge/Version-v0.8.9-green +.. |label4| image:: https://img.shields.io/badge/Version-v0.9.0-green :height: 18 .. |image1| image:: ./images/active_bookmark.png @@ -100,7 +100,7 @@ __email__ = 'hello@gergely-wootsch.com' #: Project version -__version__ = '0.8.9' +__version__ = '0.9.0' #: Project version __version_info__ = __version__.split('.') diff --git a/bookmarks/maya/plugin.py b/bookmarks/maya/plugin.py index 3d68bfcf..01aa229d 100644 --- a/bookmarks/maya/plugin.py +++ b/bookmarks/maya/plugin.py @@ -17,7 +17,7 @@ product = 'bookmarks' __author__ = 'Gergely Wootsch' -__version__ = '0.8.9' +__version__ = '0.9.0' maya_useNewAPI = True diff --git a/bookmarks/maya/scripts/aka_character_caches.py b/bookmarks/maya/scripts/aka_character_caches.py index 6418818e..cdc8c842 100644 --- a/bookmarks/maya/scripts/aka_character_caches.py +++ b/bookmarks/maya/scripts/aka_character_caches.py @@ -90,6 +90,37 @@ } +def export_nullLocator_to_alembic(output_file_path, cut_in, cut_out): + """Export the nullLocator to Alembic. + + Args: + output_file_path (str): The output file path. + cut_in (int): The cut in frame. + cut_out (int): The cut out frame. + + """ + # Check if the nullLocator exists + if not cmds.objExists('nullLocator'): + raise ValueError('nullLocator does not exist in the scene.') + + # Ensure that the nullLocator is in world space + parent = cmds.listRelatives('nullLocator', parent=True) + if parent: + # Parent it to the world + cmds.parent('nullLocator', world=True) + + # Export to Alembic + export_command = ( + f'-frameRange {cut_in} {cut_out} ' + f'-worldSpace ' # Ensures the object is exported in world space + f'-root nullLocator ' # The object we want to export + f'-file "{output_file_path}"' + ) + cmds.AbcExport(j=export_command) + + print("Exported nullLocator to: {}".format(output_file_path)) + + def find_studio_library(): for path in sys.path: if not os.path.isdir(path): @@ -598,6 +629,12 @@ def export(self): except UnicodeDecodeError as e: print(e) + # Export the nullLocator to Alembic + self.statusChanged.emit('Exporting...', 'Saving nullLocator animation...') + p = get_cache_path('nullLocator', 'abc') + output_paths.append(p) + export_nullLocator_to_alembic(p, cut_in, cut_out) + # Cleanup cmds.delete(constraint) cmds.delete(locator) diff --git a/docs/src/conf.py b/docs/src/conf.py index 5de04469..420d1b59 100644 --- a/docs/src/conf.py +++ b/docs/src/conf.py @@ -33,7 +33,7 @@ author = 'Gergely Wootsch' # The full version, including alpha/beta/rc tags -release = '0.8.9' +release = '0.9.0' html_baseurl = 'https://bookmarks-vfx.com' html_extra_path = [ diff --git a/docs/src/guide.rst b/docs/src/guide.rst index 20b85f87..4a4e1f0f 100644 --- a/docs/src/guide.rst +++ b/docs/src/guide.rst @@ -25,7 +25,7 @@ Get Bookmarks The project is hosted on `Github `_. -.. admonition:: Download the latest Windows release: `Bookmarks v0.8.9 `_ +.. admonition:: Download the latest Windows release: `Bookmarks v0.9.0 `_ ☹ Currently, Bookmarks only supports Windows. diff --git a/package/CMakeLists.txt b/package/CMakeLists.txt index f993a181..51b70d8f 100644 --- a/package/CMakeLists.txt +++ b/package/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.20) project( Bookmarks - VERSION 0.8.9 + VERSION 0.9.0 DESCRIPTION "Bookmarks is a lightweight asset manager for digital artists working in the animation, motion graphics and VFX industries." HOMEPAGE_URL "https://bookmarks-vfx.com" ) From 6964d7f564f8f343df6824e9b64b5b68c0a6dc14 Mon Sep 17 00:00:00 2001 From: Gergely Wootsch Date: Tue, 31 Oct 2023 09:25:12 +0100 Subject: [PATCH 23/32] Add options to default to the scenes folder (request by Vincent @ Studio Aka) --- bookmarks/actions.py | 30 ++++++++++++++++++++++++++++++ bookmarks/external/akaconvert.py | 0 bookmarks/main.py | 4 ++++ 3 files changed, 34 insertions(+) create mode 100644 bookmarks/external/akaconvert.py diff --git a/bookmarks/actions.py b/bookmarks/actions.py index 07a123e1..f22b6e92 100644 --- a/bookmarks/actions.py +++ b/bookmarks/actions.py @@ -450,6 +450,30 @@ def set_active(k, v): common.settings.setValue(f'active/{k}', v) +@QtCore.Slot(QtCore.QModelIndex) +def apply_default_to_scenes_folder(index): + """Slot responsible for applying the default to scenes folder setting. + + The slot is connected to the :class:`bookmarks.items.file_items.FileItemModel`'s + `activeChanged` signal, and we're using it to modify the active 'task' path before + resetting the file model. + + Args: + index (QtCore.QModelIndex): The index of the activated asset items. + + """ + v = common.settings.value('settings/default_to_scenes_folder') + if not v: + return + + if not index.isValid(): + return + + # Get the current scene folder name from the token configuration. + from .tokens import tokens + set_active('task', tokens.get_folder(tokens.SceneFolder)) + + @common.error @common.debug def set_task_folder(v): @@ -1679,6 +1703,12 @@ def convert_image_sequence(index): from .external import ffmpeg_widget ffmpeg_widget.show(index) +@common.debug +@common.error +@selection +def convert_image_sequence_with_akaconvert(index): + from .external import akaconvert + akaconvert.show(index) def add_zip_template(source, mode, prompt=False): """Adds the selected source zip archive as a `mode` template file. diff --git a/bookmarks/external/akaconvert.py b/bookmarks/external/akaconvert.py new file mode 100644 index 00000000..e69de29b diff --git a/bookmarks/main.py b/bookmarks/main.py index bbad649d..4efbfeae 100644 --- a/bookmarks/main.py +++ b/bookmarks/main.py @@ -181,6 +181,9 @@ def _connect_signals(self): a.model().sourceModel().reset_data ) # Asset -> File + a.model().sourceModel().activeChanged.connect( + actions.apply_default_to_scenes_folder + ) a.model().sourceModel().activeChanged.connect( f.model().sourceModel().reset_data ) @@ -219,6 +222,7 @@ def _connect_signals(self): ) ) + # Load bookmark items upon initialization self.initialized.connect(b.model().sourceModel().reset_data) @QtCore.Slot() From c987eb838d14b6d5d56cd6b8b07ed973013d5b78 Mon Sep 17 00:00:00 2001 From: Gergely Wootsch Date: Tue, 31 Oct 2023 09:26:40 +0100 Subject: [PATCH 24/32] Add AkaConvert UI If the `AKACONVERT_ROOT` environment is set to the root directory of AkaConvert, we'll offer up a context menu to feed a source sequence to the converter. --- bookmarks/external/akaconvert.py | 501 +++++++++++++++++++++++++++++++ 1 file changed, 501 insertions(+) diff --git a/bookmarks/external/akaconvert.py b/bookmarks/external/akaconvert.py index e69de29b..46a29e6b 100644 --- a/bookmarks/external/akaconvert.py +++ b/bookmarks/external/akaconvert.py @@ -0,0 +1,501 @@ +"""AkaConvert control widget. + +""" +import functools +import os +import re + +from PySide2 import QtCore, QtWidgets + +from .. import common +from .. import database +from .. import log +from ..editor import base + + +def close(): + """Closes the :class:`AkaConvertWidget` editor. + + """ + if common.akaconvert_widget is None: + return + try: + common.akaconvert_widget.close() + common.akaconvert_widget.deleteLater() + except: + pass + common.akaconvert_widget = None + + +def show(index): + """Opens the :class:`AkaConvertWidget` editor. + + Args: + index (QModelIndex): The source image sequence index. + + Returns: + QWidget: The AkaConvertWidget instance. + + """ + close() + common.akaconvert_widget = AkaConvertWidget(index) + common.akaconvert_widget.open() + return common.akaconvert_widget + + +KEY = 'AKACONVERT_ROOT' + +SIZE_PRESETS = { + common.idx(reset=True, start=0): { + 'name': 'Original', + 'value': (None, None) + }, + common.idx(): { + 'name': '1080p', + 'value': (1920, 1080) + }, + common.idx(): { + 'name': f'{int(1080 * 1.5)}p', + 'value': (1920 * 1.5, 1080 * 1.5) + }, + common.idx(): { + 'name': f'{int(1080 * 2)}p', + 'value': (1920 * 2, 1080 * 2) + }, +} + + +def get_framerate(fallback_framerate=24.0): + """Get the currently set frame-rate from the bookmark item database. + + Returns: + float: The current frame-rate set in the active context. + + """ + if not all(common.active('root', args=True)): + return fallback_framerate + + db = database.get(*common.active('root', args=True)) + + bookmark_framerate = db.value(common.active('root', path=True), 'framerate', database.BookmarkTable) + asset_framerate = db.value(common.active('asset', path=True), 'asset_framerate', database.AssetTable) + + v = asset_framerate or bookmark_framerate or fallback_framerate + if not isinstance(v, (int, float)) or v < 1.0: + return fallback_framerate + + return v + + +def get_environment(func): + """Decorator function to check if the environment variable exists. + + """ + + def wrapper(*args, **kwargs): + """Wrapper function. + + """ + if KEY not in os.environ: + raise RuntimeError(f'Environment variable not found: {KEY}') + + if not QtCore.QFileInfo(os.environ[KEY]).exists(): + raise RuntimeError(f'Environment variable found, but the folder does not exist: {os.environ[KEY]}') + + return func(QtCore.QFileInfo(os.environ[KEY]).absoluteFilePath(), *args, **kwargs) + + return wrapper + + +@get_environment +def get_convert_script_path(root): + """Return the path to AkaConvert.bat. + + """ + v = f'{root}/AkaConvert.bat' + if not QtCore.QFileInfo(v).exists(): + raise RuntimeError(f'AkaConvert not found: {v}') + return QtCore.QFileInfo(v).absoluteFilePath() + + +@get_environment +def get_oiiotool_path(root): + """Return the path to oiiotool.exe. + + """ + v = f'{root}/bin/oiiotool.exe' + if not QtCore.QFileInfo(v).exists(): + raise RuntimeError(f'Could not find {v}.') + return QtCore.QFileInfo(v).absoluteFilePath() + + +@get_environment +def get_ocio_colourspaces(root): + """Return the list of OCIO colourspaces. + + """ + assumed = ( + 'ACES - ACES2065-1', 'ACES - ACEScg', 'Utility - sRGB - Texture', 'Utility - Raw', 'Utility - Curve - Rec.709', + 'Utility - Curve - sRGB', 'Utility - Linear - Rec.2020', 'Utility - Linear - Rec.709', + 'Utility - Linear - sRGB', 'Utility - Gamma 1.8 - Rec.709 - Texture', 'Utility - Gamma 2.2 - Rec.709 - Texture', + 'Output - sRGB', 'Output - sRGB (D60 sim.)', 'Output - Rec.709', 'Output - Rec.709 (D60 sim.)',) + + proc = QtCore.QProcess() + proc.setProgram(get_oiiotool_path()) + proc.setArguments(['--help', ]) + + env = QtCore.QProcessEnvironment.systemEnvironment() + # We want to pass on AkaConvert's ocio environment variable + + # aces config directory + ocio_config_path = f'{root}/ocio_config/aces_1.2/config.ocio' + + if not QtCore.QFileInfo(ocio_config_path).exists(): + raise RuntimeError(f'Could not find {ocio_config_path}.') + + # Insert the OCIO config path into the environment variable + env.insert('OCIO', ocio_config_path) + proc.setProcessEnvironment(env) + + # Start the process and capture the output + proc.start() + proc.waitForFinished() + output = proc.readAllStandardOutput().data().decode('utf-8') + + # Check if the output contains the OCIO config path + if 'OpenColorIO' not in output: + raise RuntimeError( + f'Was not able to find OpenColorIO in the output: {output}. Was OpenImageIO compiled with OCIO support?' + ) + + # Find the line starting with "Known color spaces:" and extract the colourspaces + m = re.search(r'Known color spaces:(.*)', output, re.IGNORECASE | re.MULTILINE) + if not m: + raise RuntimeError(f'Was not able to find the colourspaces in the output.') + + all_colourspaces = [x.strip().strip('"') for x in m.group(1).split(',')] + + # Check if the assumed colourspaces are in the list of all colourspaces + good_colourspaces = [] + for c in assumed: + if [f for f in all_colourspaces if c.strip().lower() in f.strip().lower()]: + good_colourspaces.append(c) + + if not good_colourspaces: + raise RuntimeError(f'Was not able to find any of the expected colourspaces. Is the OCIO config correct?') + + return sorted(good_colourspaces) + + +class PresetComboBox(QtWidgets.QComboBox): + """FFMpeg preset picker. + + """ + + def __init__(self, parent=None): + super().__init__(parent=parent) + self.setView(QtWidgets.QListView()) + self.init_data() + + def init_data(self): + """Initializes data. + + """ + self.blockSignals(True) + for k in ('h264', 'prores', 'dnxhd'): + self.addItem(k.upper(), userData=k) + self.blockSignals(False) + + +class SizeComboBox(QtWidgets.QComboBox): + """FFMpeg output size picker. + + """ + + def __init__(self, parent=None): + super().__init__(parent=parent) + self.setView(QtWidgets.QListView()) + self.init_data() + + def init_data(self): + """Initializes data. + + """ + db = database.get(*common.active('root', args=True)) + bookmark_width = db.value(db.source(), 'width', database.BookmarkTable) + bookmark_height = db.value(db.source(), 'height', database.BookmarkTable) + asset_width = db.value(common.active('asset', path=True), 'asset_width', database.AssetTable) + asset_height = db.value(common.active('asset', path=True), 'asset_height', database.AssetTable) + + width = asset_width or bookmark_width or None + height = asset_height or bookmark_height or None + + if all((width, height)): + self.addItem(f'Project | {int(height)}p', userData=(width, height)) + self.addItem(f'Project | {int(height * 0.5)}p', userData=(int(width * 0.5), int(height * 0.5))) + + self.blockSignals(True) + for v in SIZE_PRESETS.values(): + self.addItem(v['name'], userData=v['value']) + + self.blockSignals(False) + + +class AcesComboBox(QtWidgets.QComboBox): + """FFMpeg preset picker. + + """ + + def __init__(self, parent=None): + super().__init__(parent=parent) + self.setView(QtWidgets.QListView()) + self.init_data() + + def init_data(self): + """Initializes data. + + """ + self.blockSignals(True) + for k in ('aces_1.2',): + self.addItem(k, userData=k) + self.blockSignals(False) + + +class ColorComboBox(QtWidgets.QComboBox): + """FFMpeg preset picker. + + """ + + def __init__(self, parent=None): + super().__init__(parent=parent) + self.setView(QtWidgets.QListView()) + self.init_data() + + def init_data(self): + """Initializes data. + + """ + self.blockSignals(True) + for k in get_ocio_colourspaces(): + self.addItem(k, userData=k) + self.blockSignals(False) + + +class AkaConvertWidget(base.BasePropertyEditor): + """Widget used to convert an image sequence to a video. + + """ + #: UI layout definition + sections = { + 0: { + 'name': 'Convert Image Sequence', + 'icon': 'convert', + 'color': common.color(common.color_dark_background), + 'groups': { + 0: { + common.idx(reset=True, start=0): { + 'name': 'Video Preset', + 'key': 'akaconvert_preset', + 'validator': None, + 'widget': PresetComboBox, + 'placeholder': None, + 'description': 'Select the video preset', + }, + common.idx(): { + 'name': 'Output Size', + 'key': 'akaconvert_size', + 'validator': None, + 'widget': SizeComboBox, + 'placeholder': None, + 'description': 'Set the output video size', + }, + common.idx(): { + 'name': 'Aces Config', + 'key': 'akaconvert_acesprofile', + 'validator': None, + 'widget': AcesComboBox, + 'placeholder': None, + 'description': 'Select the Aces config', + }, + }, + 1: { + common.idx(): { + 'name': 'Input Colour Profile', + 'key': 'akaconvert_inputcolor', + 'validator': None, + 'widget': ColorComboBox, + 'placeholder': None, + 'description': 'Select the image source\'s colour profile', + }, + common.idx(): { + 'name': 'Output Colour Profile', + 'key': 'akaconvert_outputcolor', + 'validator': None, + 'widget': ColorComboBox, + 'placeholder': None, + 'description': 'Select the output colour profile', + }, + }, + 2: { + common.idx(): { + 'name': 'Add Burn-In', + 'key': 'akaconvert_videoburnin', + 'validator': None, + 'widget': functools.partial(QtWidgets.QCheckBox, 'Add Label'), + 'placeholder': None, + 'description': 'Add video burn-in with timecode to the output video', + }, + common.idx(): { + 'name': 'Push to RV', + 'key': 'akaconvert_pushtorv', + 'validator': None, + 'widget': functools.partial(QtWidgets.QCheckBox, 'Push to RV'), + 'placeholder': None, + 'description': 'View the converted clip with RV.', + }, + }, + }, + }, + } + + def __init__(self, index, parent=None): + super().__init__( + None, None, None, fallback_thumb='convert', hide_thumbnail_editor=True, buttons=( + 'Convert', 'Cancel'), parent=parent + ) + self._index = index + self._connect_settings_save_signals(common.SECTIONS['akaconvert']) + + self.setAttribute(QtCore.Qt.WA_DeleteOnClose) + + self.setFixedWidth(common.size(common.size_width)) + self.setFixedHeight(common.size(common.size_height * 1.05)) + self.setWindowFlags( + self.windowFlags() | QtCore.Qt.FramelessWindowHint + ) + + @common.error + @common.debug + def init_data(self): + """Initializes data. + + """ + self.load_saved_user_settings(common.SECTIONS['akaconvert']) + + @common.debug + @common.error + def save_changes(self): + """Saves changes. + + """ + index = self._index + if not index.isValid(): + return False + + path = index.data(common.PathRole) + if not path: + return False + + is_collapsed = common.is_collapsed(path) + if not is_collapsed: + raise RuntimeError(f'{index.data(QtCore.Qt.DisplayRole)} is not a sequence.') + + frames = index.data(common.FramesRole) + if not frames: + raise RuntimeError( + f'{index.data(QtCore.Qt.DisplayRole)} does not seem to have any frames.' + ) + + if len(frames) < 4: + raise RuntimeError( + f'{index.data(QtCore.Qt.DisplayRole)} is too short.' + ) + + source = common.get_sequence_start_path(path) + if not QtCore.QFileInfo(source).exists(): + raise RuntimeError(f'{source} does not exist.') + + args = (source, '-video-framerate', f'{get_framerate()}', '-video-width', + f'{self.akaconvert_size_editor.currentData()[0]}', '-video-height', + f'{self.akaconvert_size_editor.currentData()[1]}', '-video-burnin', + 'true' if self.akaconvert_videoburnin_editor.isChecked() else 'false', '-video-codec', + self.akaconvert_preset_editor.currentData(), '-aces-config', + self.akaconvert_acesprofile_editor.currentData(), '-ocio-in-profile', + self.akaconvert_inputcolor_editor.currentData(), '-ocio-out-profile', + self.akaconvert_outputcolor_editor.currentData(),) + + self._error_lines = [] + self._progress_lines = [] + + self.process = QtCore.QProcess(parent=self) + self.process.readyReadStandardOutput.connect(self.read_output) + self.process.finished.connect(self.on_finished) + + self.process.setProgram(get_convert_script_path()) + self.process.setArguments(args) + + env = QtCore.QProcessEnvironment.systemEnvironment() + self.process.setProcessEnvironment(env) + self.process.start() + + @QtCore.Slot() + def read_output(self): + data = self.process.readAllStandardOutput().data().decode('utf-8') + + for line in data.splitlines(): + if 'Finished' in line: + self.on_finished(0, QtCore.QProcess.NormalExit) + return + + if line.startswith('[AkaConvert Error]'): + if line not in self._error_lines: + self._error_lines.append(line) + elif line.startswith('[AkaConvert Info]'): + if line not in self._progress_lines: + self._progress_lines.append(line) + + current_progress_line = self._progress_lines[-1] if self._progress_lines else None + if current_progress_line: + common.show_message( + 'Converting...', body=current_progress_line.replace('[AkaConvert Info]', ''), message_type=None, + buttons=[], disable_animation=True, ) + + def on_finished(self, exit_code, exit_status): + # I don't know why, but the process doesn't terminate itself and will + # keep running in the background. So we need to terminate it manually I guess! + if exit_code != 0: + common.show_message( + 'Finished.', + f'Conversion has finished but process exited with a code {exit_code}.\n{self._error_lines[-1]}' + ) + if exit_code == 0 and exit_status == QtCore.QProcess.NormalExit: + common.show_message( + 'Finished.', f'Conversion has finished successfully.' + ) + raise RuntimeError('Finished.') + + def sizeHint(self): + """Returns a size hint. + + """ + return QtCore.QSize( + common.size(common.size_width) * 0.66, common.size(common.size_height) * 1.2 + ) + + def showEvent(self, event): + super().showEvent(event) + + if not self._index: + return + + item_rect = common.widget().visualRect(self._index) + corner = common.widget().mapToGlobal(item_rect.bottomLeft()) + + self.move(corner) + self.setGeometry( + self.geometry().x() + (item_rect.width() / 2) - ( + self.geometry().width() / 2), self.geometry().y(), self.geometry().width(), self.geometry( + + ).height() + ) + common.move_widget_to_available_geo(self) From 5af21570871f43f2c4edfdbcef35e3c51bc37463 Mon Sep 17 00:00:00 2001 From: Gergely Wootsch Date: Tue, 31 Oct 2023 09:30:04 +0100 Subject: [PATCH 25/32] Fix RV push url handler not ready when pushing footage to RV The RV url handler does not work if the RV session doesn't have the handler ready. This happens every time when a fresh instance is launched. I'm trying to balance this by launching a dummy python command, wait for the process to run, then a litte more, THEN push footages to RV. More testing is needed, but seems to work at first glance. --- bookmarks/external/rv.py | 89 ++++++++++++++++++++-------------------- 1 file changed, 44 insertions(+), 45 deletions(-) diff --git a/bookmarks/external/rv.py b/bookmarks/external/rv.py index 432b5125..dcb88aad 100644 --- a/bookmarks/external/rv.py +++ b/bookmarks/external/rv.py @@ -3,83 +3,82 @@ TODO: This module is a stub and needs more testing and development. """ -import os -import subprocess +import uuid + +from PySide2 import QtCore from .. import common from .. import database -from .. import log -#: The base command used to call an RV push command -URL = f'"{{executable}}" -tag {common.product} url \'rvlink:// {{rv_command}}\'' -MERGE = f'"{{executable}}" -tag {common.product} merge {{rv_command}}' -PushAndClear = 'PushAndClear' -PushAndClearFullScreen = 'PushAndClearFullScreen' -Add = 'Add' +tag = f'{common.product}{uuid.uuid4().hex}' + +PushAndClear = common.idx(reset=True, start=0) +PushAndClearFullScreen = common.idx() +Add = common.idx() +InitializeSession = common.idx() RV_COMMANDS = { - PushAndClearFullScreen: '-reuse 1 -inferSequence -l -play -fullscreen -nofloat -lookback 0 -nomb -fps {framerate} ' - '\"{source}\"', - PushAndClear: '-reuse 1 -inferSequence -l -play -lookback 0 -fps {framerate} \"{source}\"', - Add: '\"{source}\"', + InitializeSession: '-tag {tag} py-eval-return "print(\'Starting RV\')"', + PushAndClearFullScreen: '-tag {tag} url rvlink:// "-reuse 1 -inferSequence -l -play -fullscreen -nofloat -lookback 0 -nomb -fps {framerate} "{source}""', + PushAndClear: '-tag {tag} url rvlink:// "-reuse 1 -inferSequence -l -play -lookback 0 -fps {framerate} "{source}""', + Add: '-tag {tag} merge "{source}"', } -def _execute_rvpush_command(cmd): - """Executes the given command. - - Args: - cmd (str): The command to execute. - - """ - # Execute the command - if common.get_platform() == common.PlatformWindows: - startupinfo = subprocess.STARTUPINFO() - startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW - startupinfo.wShowWindow = subprocess.SW_HIDE - - with open(os.devnull, 'w') as devnull: - subprocess.Popen(cmd, stdout=devnull, stderr=devnull, startupinfo=startupinfo) - - else: - raise NotImplementedError('RV push is not implemented for this platform.') - - @common.error @common.debug -def execute_rvpush_command(source, command, basecommand=URL): +def execute_rvpush_command(source, command): """Calls rvpush with the given source and commands. Args: source (str): The path to a footage source. command (str): An RV command enum. - basecommand (str): The base command to use when calling rvpush. """ common.check_type(source, str) - common.check_type(command, str) - common.check_type(basecommand, str) + common.check_type(command, int) if command not in RV_COMMANDS: - raise ValueError(f'Invalid RV command: {command}. Valid commands are: {RV_COMMANDS.keys()}') + raise ValueError(f'Invalid RV command: {command}') # Get the command string command = RV_COMMANDS[command] - # Get the path to the executable executable = common.get_binary('rvpush') if not executable: - log.error(f'Could not find rvpush.') - return + raise RuntimeError(f'Could not find RV. Make sure the app is added as an Application Launcher Item,' + f'or that the RV binary is added to the PATH environment variable.') # Get the framerate from the database db = database.get(*common.active('root', args=True)) framerate = db.value(db.source(), 'framerate', database.BookmarkTable) framerate = float(framerate) if framerate else 24.0 - # Format the command - rv_command = command.format(framerate=framerate, source=source) - cmd = basecommand.format(executable=executable, rv_command=rv_command) + # The url protocol cannot communicate with RV unless there's already an instance running. + # So, let's execute a dummy python command to start RV. I'm assuming after calling + # rvpush initialized RV with the correct tag we can push commands to it. - _execute_rvpush_command(cmd) + process1 = QtCore.QProcess() + env = QtCore.QProcessEnvironment.systemEnvironment() + process1.setProcessEnvironment(env) + + _cmd = RV_COMMANDS[InitializeSession].format(tag=tag) + _cmd = f'"{executable}" {_cmd}' + process1.start(_cmd) + process1.waitForFinished(7000) + + # Wait 3 seconds for the process + QtCore.QThread.msleep(3000) + + # Format the command + cmd = command.format( + tag=tag, + framerate=framerate, + source=source + ) + cmd = f'"{executable}" {cmd}' + process2 = QtCore.QProcess() + process2.setProcessEnvironment(env) + process2.start(cmd) + process2.waitForFinished(7000) From 2ac53af971568127e15ae1aacc7a59506d725d25 Mon Sep 17 00:00:00 2001 From: Gergely Wootsch Date: Tue, 31 Oct 2023 09:33:22 +0100 Subject: [PATCH 26/32] Add episode entity linkage to bookmark items. --- bookmarks/database.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/bookmarks/database.py b/bookmarks/database.py index 4b4fc6ea..8f1da2ea 100644 --- a/bookmarks/database.py +++ b/bookmarks/database.py @@ -259,6 +259,14 @@ 'sql': 'TEXT', 'type': str }, + 'sg_episode_id': { + 'sql': 'INT', + 'type': int + }, + 'sg_episode_name': { + 'sql': 'TEXT', + 'type': str + }, 'url1': { 'sql': 'TEXT', 'type': str, From a7941ff01ec465a27ddedcfb4686e30c288b22be Mon Sep 17 00:00:00 2001 From: Gergely Wootsch Date: Tue, 31 Oct 2023 09:33:45 +0100 Subject: [PATCH 27/32] Various code improvements and bug fixes. --- bookmarks/common/__init__.py | 1 + bookmarks/common/env.py | 55 +++++++--- bookmarks/common/settings.py | 11 ++ bookmarks/contextmenu.py | 26 +++-- bookmarks/editor/base_widgets.py | 18 ++- bookmarks/editor/bookmark_properties.py | 35 ++++-- bookmarks/editor/preferences.py | 19 +++- bookmarks/external/ffmpeg_widget.py | 2 +- bookmarks/items/file_items.py | 2 +- bookmarks/launcher/gallery.py | 58 +++++++++- bookmarks/launcher/main.py | 56 ++++++++-- bookmarks/maya/actions.py | 41 ++++++- bookmarks/maya/contextmenu.py | 66 ++++++----- bookmarks/maya/main.py | 2 +- .../maya/scripts/aka_character_caches.py | 23 ++-- bookmarks/scripts/scripts.json | 2 +- .../scripts/send_images_to_after_effects.py | 103 ++++++++++++------ bookmarks/shotgun/shotgun.py | 17 ++- bookmarks/standalone.py | 3 +- bookmarks/topbar/buttons.py | 28 +---- bookmarks/topbar/tabs.py | 2 +- bookmarks/topbar/topbar.py | 12 +- 22 files changed, 412 insertions(+), 170 deletions(-) diff --git a/bookmarks/common/__init__.py b/bookmarks/common/__init__.py index b0e8d2b4..f208879a 100644 --- a/bookmarks/common/__init__.py +++ b/bookmarks/common/__init__.py @@ -106,6 +106,7 @@ publish_widget = None maya_export_widget = None ffmpeg_export_widget = None +akaconvert_widget = None screen_capture_widget = None pick_thumbnail_widget = None notes_widget = None diff --git a/bookmarks/common/env.py b/bookmarks/common/env.py index 84a03cf6..f63d33d6 100644 --- a/bookmarks/common/env.py +++ b/bookmarks/common/env.py @@ -9,6 +9,7 @@ from PySide2 import QtCore, QtWidgets + external_binaries = ( 'ffmpeg', 'rvpush', @@ -18,36 +19,54 @@ def get_binary(binary_name): - """External binary paths must be set explicitly by the environment or by the - user in the user settings. + """Binary path getter. - Bookmarks will look for user defined binary paths, or failing that, - environment values in a ``{PREFIX}_{BINARY_NAME}`` format, - e.g. ``BOOKMARKS_FFMPEG``, or ``BOOKMARKS_RV``. These environment variables - should point to an appropriate executable, e.g. - ``BOOKMARKS_FFMPEG=C:/ffmpeg/ffmpeg.exe`` + The paths are resolved from the following sources and order: + - active bookmark item's application launcher items + - user settings + - environment variables in a ``{PREFIX}_{BINARY_NAME}`` format, + e.g. ``BOOKMARKS_FFMPEG``, or ``BOOKMARKS_RV``. These environment variables + should point to an appropriate executable, e.g. + ``BOOKMARKS_FFMPEG=C:/ffmpeg/ffmpeg.exe`` - If the environment variable is absent, we'll look at the PATH environment to - see if the binary is available there. + If the environment variable is absent, we'll look at the PATH environment to + see if the binary is available there. Args: - binary_name (str): One of the pre-defined external binary names. - E.g. `ffmpeg`. + binary_name (str): Name of a binary, lower-case, without spaces. E.g. `aftereffects`, `oiiotool`, `ffmpeg`, etc. Returns: - str: Path to an executable binary, or `None` if the binary is not found. + str: Path to an executable binary, or `None` if the binary is not found in any of the sources. """ - if binary_name.lower() not in [f.lower() for f in external_binaries]: - raise ValueError(f'{binary_name} is not a recognised binary name.') - + # Sanitize the binary name + binary_name = re.sub(r'\s+', '', binary_name).lower().strip() + + # Check the active bookmark item's database for possible values + from .. import database + from .. import common + + args = common.active('root', args=True) + + if args: + db = database.get(*args) + applications = db.value(db.source(), 'applications', database.BookmarkTable) + if applications: + # Sanitize names, so they're all lower-case and without spaces + names = [re.sub(r'\s+', '', v['name']).lower().strip() for v in applications.values()] + if binary_name in names: + # We have a match, return the path + v = applications[names.index(binary_name)]['path'] + if v and QtCore.QFileInfo(v).exists(): + return v + + # Check the user settings for possible values v = get_user_setting(binary_name) if v: return v - from . import product - - key = f'{product}_{binary_name}'.upper() + # Check the environment variables for possible values + key = f'{common.product}_{binary_name}'.upper() if key in os.environ: v = os.environ[key] try: diff --git a/bookmarks/common/settings.py b/bookmarks/common/settings.py index b5957298..ae60dca5 100644 --- a/bookmarks/common/settings.py +++ b/bookmarks/common/settings.py @@ -36,6 +36,7 @@ 'settings/paint_thumbnail_bg', 'settings/disable_oiio', 'settings/hide_item_descriptions', + 'settings/default_to_scenes_folder', 'settings/always_always_on_top', 'settings/bin_ffmpeg', 'settings/bin_rv', @@ -93,6 +94,15 @@ 'ffmpeg/timecode_preset', 'ffmpeg/pushtorv', ), + 'akaconvert': ( + 'akaconvert/preset', + 'akaconvert/size', + 'akaconvert/acesprofile', + 'akaconvert/inputcolor', + 'akaconvert/outputcolor', + 'akaconvert/videoburnin', + 'akaconvert/pushtorv', + ), 'maya': ( 'maya/sync_workspace', 'maya/workspace_save_warnings', @@ -352,6 +362,7 @@ def __init__(self, parent=None): self.verify_timer.setSingleShot(False) self.verify_timer.setTimerType(QtCore.Qt.CoarseTimer) self.verify_timer.timeout.connect(self.load_active_values) + self.verify_timer.start() def load_active_values(self): """Load active path elements from the settings file. diff --git a/bookmarks/contextmenu.py b/bookmarks/contextmenu.py index b71087a6..393a3ba6 100644 --- a/bookmarks/contextmenu.py +++ b/bookmarks/contextmenu.py @@ -1510,7 +1510,7 @@ def sg_rv_menu(self): self.menu[k][key()] = { 'text': 'Add as source', 'icon': ui.get_icon('sg'), - 'action': functools.partial(rv.execute_rvpush_command, path, rv.Add, basecommand=rv.MERGE) + 'action': functools.partial(rv.execute_rvpush_command, path, rv.Add) } self.separator(self.menu[k]) @@ -1533,7 +1533,7 @@ def sg_publish_menu(self): self.separator(self.menu[k]) self.menu[k][key()] = { - 'text': 'SG Publish: MP4 as Version', + 'text': 'Publish Video', 'icon': ui.get_icon('sg', color=common.color(common.color_green)), 'action': functools.partial(sg_actions.publish, formats=('mp4', 'mov')), } @@ -1585,12 +1585,21 @@ def convert_menu(self): if ext.lower() not in images.get_oiio_extensions(): return + # AkaConvert + from .external import akaconvert + if akaconvert.KEY in os.environ and os.environ[akaconvert.KEY]: + self.menu[key()] = { + 'text': 'AkaConvert...', + 'icon': ui.get_icon('convert', color=common.color(common.color_blue)), + 'action': actions.convert_image_sequence_with_akaconvert + } + # Can only convert when FFMpeg is present if not common.get_binary('ffmpeg'): return self.menu[key()] = { - 'text': 'Convert Sequence', + 'text': 'Convert Sequence...', 'icon': ui.get_icon('convert'), 'action': actions.convert_image_sequence } @@ -1690,14 +1699,9 @@ def _run(name): continue # Check if the script needs an application to be set if 'needs_application' in v and v['needs_application']: - if not common.active('root', args=True): - continue - # Get the bookmark database - db = database.get(*common.active('root', args=True)) - applications = db.value(db.source(), 'applications', database.BookmarkTable) - if not applications: - continue - if not [app for app in applications.values() if v['needs_application'].lower() in app['name'].lower()]: + afxs = ('aftereffects', 'afx', 'afterfx') + if not any(([common.get_binary(f)] for f in afxs)): + print(f'Could not find After Effects. Tried: {afxs}') continue if 'icon' in v and v['icon']: icon = ui.get_icon(v['icon']) diff --git a/bookmarks/editor/base_widgets.py b/bookmarks/editor/base_widgets.py index cb0bed6b..6650945e 100644 --- a/bookmarks/editor/base_widgets.py +++ b/bookmarks/editor/base_widgets.py @@ -16,8 +16,7 @@ HEIGHT = common.size(common.size_row_height) * 0.8 TEMP_THUMBNAIL_PATH = '{temp}/{product}/temp/{uuid}.{ext}' -ProjectTypes = ('Project',) -AssetTypes = ('Episode', 'Asset', 'Sequence', 'Shot') +AssetTypes = ('Asset', 'Sequence', 'Shot') @common.error @@ -142,8 +141,19 @@ def init_items(self): """Initialize items. """ - for entity_type in ProjectTypes: - self.addItem(entity_type) + self.addItem('Project') + + +class SGEpisodeTypesWidget(BaseComboBox): + """ShotGrid entity type picker. + + """ + + def init_items(self): + """Initialize items. + + """ + self.addItem('Episode') class SGAssetTypesWidget(BaseComboBox): diff --git a/bookmarks/editor/bookmark_properties.py b/bookmarks/editor/bookmark_properties.py index c50a6d9c..28c86974 100644 --- a/bookmarks/editor/bookmark_properties.py +++ b/bookmarks/editor/bookmark_properties.py @@ -242,31 +242,52 @@ class BookmarkPropertyEditor(base.BasePropertyEditor): 'description': 'Link item with a ShotGrid Entity', 'button': 'Link with ShotGrid Entity', }, - 1: { - 'name': 'Type', + }, + 1: { + 0: { + 'name': 'ShotGrid Entity Type', 'key': 'shotgun_type', 'validator': base.int_validator, 'widget': base_widgets.SGProjectTypesWidget, 'placeholder': None, 'description': 'Select the item\'s ShotGrid type', }, - 2: { - 'name': 'ID', + 1: { + 'name': 'ShotGrid Project Id', 'key': 'shotgun_id', 'validator': base.int_validator, 'widget': ui.LineEdit, 'placeholder': 'ShotGrid Project ID, e.g. \'123\'', - 'description': 'The ShotGrid ID number this item is associated ' + 'description': 'The ShotGrid entity id number this item is associated ' 'with. e.g. \'123\'.', }, - 3: { - 'name': 'Name', + 2: { + 'name': 'ShotGrid Project Name', 'key': 'shotgun_name', 'validator': None, 'widget': ui.LineEdit, 'placeholder': 'ShotGrid project name, e.g. \'MyProject\'', 'description': 'The ShotGrid project name', }, + }, + 2: { + 0: { + 'name': 'ShotGrid Episode Id', + 'key': 'sg_episode_id', + 'validator': base.int_validator, + 'widget': ui.LineEdit, + 'placeholder': 'ShotGrid episode id, e.g. \'123\'', + 'description': 'The ShotGrid episode entity number this item is associated ' + 'with. e.g. \'123\'.', + }, + 1: { + 'name': 'ShotGrid Episode Name', + 'key': 'sg_episode_name', + 'validator': None, + 'widget': ui.LineEdit, + 'placeholder': 'ShotGrid episode entity name, e.g. \'Episode1\'', + 'description': 'The ShotGrid episode entity name', + }, } } }, diff --git a/bookmarks/editor/preferences.py b/bookmarks/editor/preferences.py index 3412e4dc..d651be84 100644 --- a/bookmarks/editor/preferences.py +++ b/bookmarks/editor/preferences.py @@ -136,6 +136,21 @@ class PreferenceEditor(base.BasePropertyEditor): }, }, 1: { + 0: { + 'name': 'Default to scene folder', + 'key': 'settings/default_to_scenes_folder', + 'validator': None, + 'widget': functools.partial( + QtWidgets.QCheckBox, 'Enable' + ), + 'placeholder': 'Default to scene folder', + 'description': 'Default to the scene Folder when the active asset changes', + 'help': 'If enabled, the files tab will always show the ' + 'contents of the scene folder (instead of the last ' + 'selected folder) when the active asset changes.', + }, + }, + 2: { 0: { 'name': 'Bookmark item search depth', 'key': 'settings/job_scan_depth', @@ -244,13 +259,13 @@ class PreferenceEditor(base.BasePropertyEditor): 'captures in the file explorer.', }, 2: { - 'name': 'Disable "Latest" Capture', + 'name': 'Copy capture to "latest" folder', 'key': 'maya/publish_capture', 'validator': None, 'widget': functools.partial(QtWidgets.QCheckBox, 'Disable'), 'placeholder': None, 'description': 'The last capture by default will be ' - 'published into a "Latest" folder with using a ' + 'published into a "latest" folder with using a ' 'generic filename.\nThis can be useful for ' 'creating quick edits in RV. Check the box ' 'above to disable.', diff --git a/bookmarks/external/ffmpeg_widget.py b/bookmarks/external/ffmpeg_widget.py index 877139d9..e9b5f0d4 100644 --- a/bookmarks/external/ffmpeg_widget.py +++ b/bookmarks/external/ffmpeg_widget.py @@ -148,7 +148,7 @@ def init_data(self): """ db = database.get(*common.active('root', args=True)) bookmark_width = db.value(db.source(), 'width', database.BookmarkTable) - bookmark_height = db.value(db.source(), 'width', database.BookmarkTable) + bookmark_height = db.value(db.source(), 'height', database.BookmarkTable) asset_width = db.value(common.active('asset', path=True), 'asset_width', database.AssetTable) asset_height = db.value(common.active('asset', path=True), 'asset_height', database.AssetTable) diff --git a/bookmarks/items/file_items.py b/bookmarks/items/file_items.py index d652bb9f..7b5903ff 100644 --- a/bookmarks/items/file_items.py +++ b/bookmarks/items/file_items.py @@ -746,7 +746,7 @@ class FileItemView(views.ThreadedItemView): queues = (threads.FileInfo, threads.FileThumbnail) def __init__(self, icon='file', parent=None): - super(FileItemView, self).__init__( + super().__init__( icon=icon, parent=parent ) diff --git a/bookmarks/launcher/gallery.py b/bookmarks/launcher/gallery.py index 7b240b88..3217d41f 100644 --- a/bookmarks/launcher/gallery.py +++ b/bookmarks/launcher/gallery.py @@ -1,9 +1,10 @@ """The application launcher item viewer. """ -from PySide2 import QtWidgets +from PySide2 import QtWidgets, QtCore from .. import actions +from .. import images from .. import common from .. import database from .. import ui @@ -76,5 +77,56 @@ def item_generator(self): return actions.edit_bookmark() - for k in sorted(v, key=lambda _k: v[_k]['name']): - yield v[k]['name'], v[k]['path'], v[k]['thumbnail'] + for k in sorted(v, key=lambda idx: v[idx]['name']): + yield v[k] + + def init_data(self): + """Initializes data. + + """ + row = 0 + idx = 0 + + + for v in self.item_generator(): + if 'name' not in v or not v['name']: + continue + label = v['name'] + + if 'path' not in v or not v['path']: + continue + path = v['path'] + + if not QtCore.QFileInfo(path).exists(): + continue + + if 'thumbnail' not in v or not v['thumbnail']: + thumbnail = images.rsc_pixmap( + 'icon', + None, + None, + get_path=True, + ) + else: + thumbnail = v['thumbnail'] + if 'hidden' not in v or not v['hidden']: + is_hidden = False + else: + is_hidden = v['hidden'] + + if is_hidden: + continue + + item = ui.GalleryItem( + label, path, thumbnail, height=self._item_height, parent=self + ) + + column = idx % self.columns + if column == 0: + row += 1 + + self.scroll_area.widget().layout().addWidget(item, row, column) + item.clicked.connect(self.itemSelected) + item.clicked.connect(self.close) + + idx += 1 \ No newline at end of file diff --git a/bookmarks/launcher/main.py b/bookmarks/launcher/main.py index 169abd56..a8c8f995 100644 --- a/bookmarks/launcher/main.py +++ b/bookmarks/launcher/main.py @@ -12,27 +12,34 @@ #: Default launcher item definition DEFAULT_ITEM = { - 0: { + common.idx(reset=True, start=0): { 'key': 'name', 'placeholder': 'Name, e.g. "Maya"', 'widget': ui.LineEdit, 'description': 'Enter the item\'s name, e.g. Maya', 'button': None, }, - 1: { + common.idx(): { 'key': 'path', 'placeholder': 'Path, e.g. "C:/maya/maya.exe"', 'widget': ui.LineEdit, 'description': 'Path to the executable.', 'button': 'Pick', }, - 2: { + common.idx(): { 'key': 'thumbnail', 'placeholder': 'Path to an image, e.g. "C:/images/maya.png"', 'widget': ui.LineEdit, 'description': 'Path to an image file used to represent this item', 'button': 'Pick', }, + common.idx(): { + 'key': 'hidden', + 'placeholder': None, + 'widget': functools.partial(QtWidgets.QCheckBox, 'Hidden'), + 'description': 'Hide the item from the application launcher.', + 'button': None, + }, } #: Default launcher item size @@ -49,7 +56,7 @@ class LauncherItemEditor(QtWidgets.QDialog): itemAdded = QtCore.Signal(dict) def __init__(self, data=None, parent=None): - super(LauncherItemEditor, self).__init__(parent=parent) + super().__init__(parent=parent) self.thumbnail_viewer_widget = None self.done_button = None @@ -88,6 +95,8 @@ def _create_ui(self): self.thumbnail_viewer_widget.setFixedSize(QtCore.QSize(w, w)) grp.layout().addWidget(self.thumbnail_viewer_widget, 0) + self.thumbnail_viewer_widget.setPixmap(images.rsc_pixmap('icon', color=None, size=w)) + _grp = ui.get_group( margin=common.size( common.size_indicator @@ -134,8 +143,22 @@ def init_data(self, item): for idx in DEFAULT_ITEM: k = DEFAULT_ITEM[idx]['key'] + + if k not in item: + continue + + if not hasattr(self, k + '_editor'): + continue + editor = getattr(self, k + '_editor') - editor.setText(item[k]) + + if isinstance(editor, QtWidgets.QCheckBox): + editor.setChecked(item[k]) + continue + + if isinstance(editor, QtWidgets.QLineEdit): + editor.setText(item[k]) + continue @QtCore.Slot(str) def update_thumbnail_image(self, path): @@ -146,8 +169,10 @@ def update_thumbnail_image(self, path): """ image = QtGui.QImage(path) - if image.isNull(): - self.thumbnail_viewer_widget.setPixmap(QtGui.QPixmap()) + if not path or image.isNull(): + h = common.size(common.size_margin) * 2 + w = h * len(DEFAULT_ITEM) + (common.size(common.size_indicator) * 2) + self.thumbnail_viewer_widget.setPixmap(images.rsc_pixmap('icon', None, w)) return image.setDevicePixelRatio(common.pixel_ratio) @@ -171,13 +196,26 @@ def action(self): raise RuntimeError('Must specify a path to an executable.') if not self.thumbnail_editor.text(): - raise RuntimeError('Must specify thumbnail image path.') + if common.show_message( + 'No thumbnail image specified.', + body=f'Are you sure you want continue without specifying a thumbnail image?', + buttons=[common.YesButton, common.CancelButton], + modal=True, ) == QtWidgets.QDialog.Rejected: + return data = {} for idx in DEFAULT_ITEM: k = DEFAULT_ITEM[idx]['key'] editor = getattr(self, k + '_editor') - v = editor.text() + + if isinstance(editor, QtWidgets.QCheckBox): + v = editor.isChecked() + elif isinstance(editor, QtWidgets.QLineEdit): + v = editor.text() + v = v if v else None + else: + v = None + data[k] = v if self._data and self._data != data: diff --git a/bookmarks/maya/actions.py b/bookmarks/maya/actions.py index ab7bc782..5646469b 100644 --- a/bookmarks/maya/actions.py +++ b/bookmarks/maya/actions.py @@ -121,12 +121,41 @@ def apply_settings(*args, **kwargs): ) == QtWidgets.QDialog.Rejected: return - base.patch_workspace_file_rules() - base.set_framerate(props.framerate) - base.set_startframe(props.startframe) - base.set_endframe(props.endframe) - base.apply_default_render_values() - base.set_render_resolution(props.width, props.height) + try: + base.patch_workspace_file_rules() + except Exception as e: + log.error(f'Could not patch workspace.mel:\n{e}') + return + + try: + base.set_framerate(props.framerate) + except Exception as e: + log.error(f'Could not set framerate:\n{e}') + return + + try: + base.set_startframe(props.startframe) + except Exception as e: + log.error(f'Could not set startframe:\n{e}') + return + + try: + base.set_endframe(props.endframe) + except Exception as e: + log.error(f'Could not set endframe:\n{e}') + return + + try: + base.apply_default_render_values() + except Exception as e: + log.error(f'Could not apply default render values:\n{e}') + return + + try: + base.set_render_resolution(props.width, props.height) + except Exception as e: + log.error(f'Could not set render resolution:\n{e}') + return @common.error diff --git a/bookmarks/maya/contextmenu.py b/bookmarks/maya/contextmenu.py index b22a289e..aeef48ba 100644 --- a/bookmarks/maya/contextmenu.py +++ b/bookmarks/maya/contextmenu.py @@ -31,12 +31,12 @@ def setup(self): """Creates the context menu. """ + self.save_menu() + self.separator() self.scripts_menu() self.separator() self.apply_bookmark_settings_menu() self.separator() - self.save_menu() - self.separator() self.open_import_scene_menu() self.separator() self.export_menu() @@ -47,6 +47,8 @@ def setup(self): self.capture_menu() self.separator() self.hud_menu() + self.separator() + self.maya_preferences_menu() def apply_bookmark_settings_menu(self): """Apply settings action. @@ -265,6 +267,39 @@ def hud_menu(self): 'action': actions.toggle_hud } + def maya_preferences_menu(self): + item_on_icon = ui.get_icon('check', color=common.color(common.color_green)) + item_off_icon = ui.get_icon('close', color=common.color(common.color_red)) + + k = 'Options' + self.menu[k] = collections.OrderedDict() + self.menu[f'{k}:icon'] = ui.get_icon('settings') + + for item in ( + ('maya/sync_workspace', 'Set Workspace'), + ('maya/set_sg_context', 'Set ShotGrid Context'), + 'separator', + ('maya/reveal_capture', 'Reveal capture in explorer'), + ('maya/publish_capture', 'Copy capture to \'latest\''), + 'separator', + ('maya/workspace_save_warnings', 'Show Warnings'), + ): + if item == 'separator': + self.separator(menu=self.menu[k]) + continue + + key, name = item + + value = common.settings.value(key) + value = value if value is not None else False + + # "True" values refer to the functionality in "disabled" state, so we need to flip the values + self.menu[k][contextmenu.key()] = { + 'text': name, + 'icon': item_on_icon if not value else item_off_icon, + 'action': functools.partial(common.settings.setValue, key, not value) + } + class MayaButtonWidgetContextMenu(PluginContextMenu): """The context-menu associated with the BrowserButton.""" @@ -273,30 +308,3 @@ def __init__(self, parent=None): super().__init__( QtCore.QModelIndex(), parent=parent ) - - -class MayaWidgetContextMenu(PluginContextMenu): - """Context menu associated with :class:`MayaWidget`. - - """ - - @common.error - @common.debug - def setup(self): - """Creates the context menu. - - """ - self.apply_bookmark_settings_menu() - self.separator() - self.save_menu() - self.separator() - self.open_import_scene_menu() - self.separator() - self.export_menu() - self.separator() - self.import_camera_menu() - self.separator() - self.viewport_presets_menu() - self.capture_menu() - self.separator() - self.hud_menu() diff --git a/bookmarks/maya/main.py b/bookmarks/maya/main.py index e9d7b65b..0daf277d 100644 --- a/bookmarks/maya/main.py +++ b/bookmarks/maya/main.py @@ -591,7 +591,7 @@ def customFilesContextMenuEvent(self, index, parent): width = (width * 0.5) if width > common.size(common.size_width) else width width = width - common.size(common.size_indicator) - widget = contextmenu.MayaWidgetContextMenu(index, parent=parent) + widget = contextmenu.PluginContextMenu(index, parent=parent) if index.isValid(): rect = parent.visualRect(index) widget.move( diff --git a/bookmarks/maya/scripts/aka_character_caches.py b/bookmarks/maya/scripts/aka_character_caches.py index cdc8c842..44094444 100644 --- a/bookmarks/maya/scripts/aka_character_caches.py +++ b/bookmarks/maya/scripts/aka_character_caches.py @@ -72,7 +72,7 @@ 'widget': ui.LineEdit, }, 'exclude_reset_pose_from': { - 'default': 'legUI_L0_ctl, armUI_R0_ctl, faceUI_C0_ctl, legUI_R0_ctl, spineUI_C0_ctl, armUI_L0_ctl', + 'default': 'legUI_L0_ctl, armUI_R0_ctl, faceUI_C0_ctl, legUI_R0_ctl, spineUI_C0_ctl, armUI_L0_ctl, body_C0_ctl, world_ctl, root_C0_ctl', 'placeholder': 'list, of, controllers, to, exclude', 'widget': ui.LineEdit, }, @@ -662,13 +662,13 @@ def export(self): # Studio Library: Apply the reset pose at cut_in - preroll # We only want to apply the reset pose to the controllers that are not excluded _s1 = set(cmds.sets(options['controllers_set'], query=True)) - _s2 = set(options['exclude_animation_from']) if options["apply_animation_exclusions"] else set() _s3 = set(options["exclude_reset_pose_from"]) mutils.loadPose( - os.path.normpath( - options["studio_library_reset_pose"] - ), objects=list(_s1 - _s2 - _s3), key=True, namespaces=[options['namespace'], ] + os.path.normpath(options["studio_library_reset_pose"]), + objects=list(_s1 - _s3), + key=True, + namespaces=[options['namespace'], ] ) # -- Cache exports -- @@ -889,11 +889,16 @@ def sizeHint(self): def run(): global instance + if instance: + try: + instance.close() + instance.deleteLater() + instance = None + except: + pass - if instance is None: - instance = ExportCharacterCachesDialog() - instance.accepted.connect(lambda: common.source_model(common.FileTab).reset_data(force=True)) - + instance = ExportCharacterCachesDialog() + instance.accepted.connect(lambda: common.source_model(common.FileTab).reset_data(force=True)) actions.change_tab(common.FileTab) actions.set_task_folder(tokens.get_folder(tokens.CacheFolder)) diff --git a/bookmarks/scripts/scripts.json b/bookmarks/scripts/scripts.json index 61fd1a60..38d5cd5c 100644 --- a/bookmarks/scripts/scripts.json +++ b/bookmarks/scripts/scripts.json @@ -19,7 +19,7 @@ "3": { "name": "Send Images to After Effects", "module": "send_images_to_after_effects", - "description": "View clips and export them to editorial", + "description": "Send image sequences to After Effects", "needs_active": "task", "needs_application": "after effects", "icon": "add_file" diff --git a/bookmarks/scripts/send_images_to_after_effects.py b/bookmarks/scripts/send_images_to_after_effects.py index fda6329e..88c88245 100644 --- a/bookmarks/scripts/send_images_to_after_effects.py +++ b/bookmarks/scripts/send_images_to_after_effects.py @@ -1,8 +1,9 @@ """ -This is a utility script to send image sequences to After Effects rendered with Maya. +This is a utility script to send image sequences to After Effects rendered with the Maya. The script expects the images sequences to use the Maya specified render template format. -Any valid image sequences will be included in an ExtendScript export script. +Any valid image sequences will be included in an ExtendScript script that will be sent to +After Effects. """ import os @@ -15,23 +16,36 @@ from .. import database from ..tokens import tokens +#: The render name template used by Maya RENDER_NAME_TEMPLATE = '///__' -pattern = r'{source_dir}/(.+)/(.+)/(.+)/.+\_\d+.exr$' +#: The pattern used to match the render name template +pattern = r'{source_dir}/(.+)/(.+)/(.+)/.+\.[a-z]{{2,4}}$' -def recursive_parse(path): +def items_it(): + """Yield a list of paths of the currently visible file items. - for entry in os.scandir(path): - if entry.is_dir(): - yield from recursive_parse(entry.path) - continue + """ + if not common.widget(): + return + + if not common.active('task'): + raise RuntimeError('An active task folder must be set to export items.') - if '_broken' in entry.name: + model = common.model(common.FileTab) + for idx in range(model.rowCount()): + index = model.index(idx, 0) + if not index.isValid(): continue - if os.path.splitext(entry.name)[-1] != '.exr': + + # Skip broken render images (RoyalRender) + if '_broken__' in index.data(QtCore.Qt.DisplayRole): continue - yield entry.path.replace('\\', '/') + path = index.data(common.PathRole) + if not path: + continue + yield index def get_footage_sources(): @@ -40,25 +54,30 @@ def get_footage_sources(): """ source_dir = common.active('task', path=True) if not source_dir: - raise RuntimeError('No active task') - if not QtCore.QFileInfo(source_dir).isDir(): - raise RuntimeError('Active task is not a directory') + raise RuntimeError('Must have an active task folder selected!') db = database.get(*common.active('root', args=True)) framerate = db.value(db.source(), 'framerate', database.BookmarkTable) data = {} - for path in recursive_parse(source_dir): - seq = common.get_sequence(path) + _pattern = pattern.format(source_dir=source_dir) + + for index in items_it(): + seq = index.data(common.SequenceRole) + path = index.data(common.PathRole) + if not seq: + print(f'Skipping non-sequence item:\n{path}') continue - match = re.match(pattern.format(source_dir=source_dir), path, re.IGNORECASE) + match = re.match(_pattern, path, re.IGNORECASE) if not match: + print(f'Skipping malformed sequence:\n{path}') continue k = f'{seq.group(1).strip("/_-.")}{seq.group(3).strip("/_-.")}'.split('/')[-1] if k not in data: + print(f'Found sequence:\n{path}') data[k] = { 'name': f'{match.group(1)} -> {match.group(3)} ({match.group(2)})', 'files': [], @@ -68,7 +87,7 @@ def get_footage_sources(): 'framerate': framerate, } - data[k]['files'].append(path) + data[k]['files'].append(common.get_sequence_start_path(path)) return data @@ -170,14 +189,20 @@ def generate_jsx_script(footage_sources): for (var i = 1; i <= project.numItems; i++) {{ if (project.item(i) instanceof CompItem && project.item(i).name == '{comp_name}') {{ comp = project.item(i); + // Apply the cut_out and cut_in values to the composition break; }} }} if (!comp) {{ comp = project.items.addComp('{comp_name}', {width}, {height}, 1, ({cut_out}-{cut_in})/{framerate}, {framerate}); - comp.displayStartTime = {cut_in}/{framerate}; }} +// Apply the cut out and cut in values to the composition +comp.duration = ({cut_out}-{cut_in}+1)/{framerate}; +comp.displayStartFrame = {cut_in}; +comp.workAreaStart = 0; +comp.workAreaDuration = comp.duration; + """ # Iterate over footage sources and add to the ExtendScript @@ -194,29 +219,43 @@ def generate_jsx_script(footage_sources): def send_to_after_effects(script_path): - if not common.active('root', args=True): - return False + """Send the generated script to After Effects. - db = database.get(*common.active('root', args=True)) - applications = db.value(db.source(), 'applications', database.BookmarkTable) + Args: + script_path (str): The path to the generated script. + + """ + + if not common.active('root', args=True): + raise RuntimeError('Must have an active bookmark') - if not applications: - return False - apps = [app for app in applications.values() if 'after effects' in app['name'].lower()] - if not apps: - return False - app = apps[0]['path'] - if not os.path.isfile(app): - return False + # Get the path to the executable + possible_names = ['afterfx', 'aftereffects', 'afx'] + for name in possible_names: + executable = common.get_binary(name) + if executable: + break + else: + raise RuntimeError(f'Could not find After Effects. Tried: {possible_names}') # Call after effects using with the generated script as an argument: - actions.execute_detached(app, ['-r', os.path.normpath(script_path)]) + actions.execute_detached(executable, ['-r', os.path.normpath(script_path)]) return True def run(): + """Run the script. + + """ data = get_footage_sources() + if not data: + common.show_message( + 'No sequences found.', + body='No sequences were found in the current task folder.', + ) + return + script_path = f'{common.active("task", path=True)}/import_footage.jsx' jsx_script, footage_sources = generate_jsx_script(data) with open(script_path, 'w') as f: diff --git a/bookmarks/shotgun/shotgun.py b/bookmarks/shotgun/shotgun.py index d2c7d50d..c31b2099 100644 --- a/bookmarks/shotgun/shotgun.py +++ b/bookmarks/shotgun/shotgun.py @@ -247,6 +247,9 @@ def __init__(self, *args, **kwargs): self.bookmark_id = None self.bookmark_name = None + self.episode_id = None + self.episode_name = None + self.asset_type = None self.asset_id = None self.asset_name = None @@ -297,11 +300,15 @@ def _load_values_from_database(self, db): self.bookmark_id = db.value(s, 'shotgun_id', t) self.bookmark_name = db.value(s, 'shotgun_name', t) + self.episode_id = db.value(s, 'sg_episode_id', t) + self.episode_name = db.value(s, 'sg_episode_name', t) + if not self.asset: return t = database.AssetTable s = db.source(self.asset) + self.asset_type = db.value(s, 'shotgun_type', t) self.asset_id = db.value(s, 'shotgun_id', t) self.asset_name = db.value(s, 'shotgun_name', t) @@ -442,13 +449,17 @@ def urls(self): return [] urls = [] + urls.append(self.domain) if all((self.bookmark_id, self.bookmark_type)): - urls.append( - ENTITY_URL.format(domain=self.domain, entity_type=self.bookmark_type, entity_id=self.bookmark_id) - ) + urls.append(ENTITY_URL.format(domain=self.domain, entity_type=self.bookmark_type, entity_id=self.bookmark_id)) + if not self.episode_id is None: + urls.append(ENTITY_URL.format(domain=self.domain, entity_type='Episode', entity_id=self.episode_id)) if all((self.asset_id, self.asset_type)): urls.append(ENTITY_URL.format(domain=self.domain, entity_type=self.asset_type, entity_id=self.asset_id)) + if not self.asset_task_id is None: + urls.append(ENTITY_URL.format(domain=self.domain, entity_type='Task', entity_id=self.asset_task_id)) + return urls diff --git a/bookmarks/standalone.py b/bookmarks/standalone.py index 1418c8ff..68ae80cf 100644 --- a/bookmarks/standalone.py +++ b/bookmarks/standalone.py @@ -345,7 +345,6 @@ def _connect_standalone_signals(self): """Extra signal connections when Bookmarks runs in standalone mode. """ - func = functools.partial(common.save_window_state, self) self.files_widget.activated.connect(actions.execute) self.favourites_widget.activated.connect(actions.execute) @@ -446,6 +445,8 @@ def eventFilter(self, widget, event): if hasattr(widget, 'statusTip') and widget.statusTip(): common.signals.showStatusTipMessage.emit(widget.statusTip()) if event.type() == QtCore.QEvent.Leave: + if not common.signals: + return False common.signals.clearStatusBarMessage.emit() return False diff --git a/bookmarks/topbar/buttons.py b/bookmarks/topbar/buttons.py index 7b7af084..17dbe53c 100644 --- a/bookmarks/topbar/buttons.py +++ b/bookmarks/topbar/buttons.py @@ -308,32 +308,14 @@ def __init__(self, parent=None): super().__init__( 'icon', f'Application Launcher - {s}', + color=( + common.color(common.color_green), + common.color(common.color_green), + ), parent=parent ) self.clicked.connect(actions.pick_launcher_item) self.clicked.connect(self.update) def state(self): - """The state of the auto-thumbnails""" - if not common.widget(): - return False - - model = common.widget().model().sourceModel() - p = model.source_path() - k = model.task() - t = model.data_type() - - if not p or not all(p) or not k or t is None: - return False - - data = common.get_task_data(p, k) - if not data: - return False - - if any( - (data[common.FileItem].refresh_needed, - data[common.SequenceItem].refresh_needed) - ): - return True - - return False + return True \ No newline at end of file diff --git a/bookmarks/topbar/tabs.py b/bookmarks/topbar/tabs.py index d1089175..a2ca9c42 100644 --- a/bookmarks/topbar/tabs.py +++ b/bookmarks/topbar/tabs.py @@ -308,7 +308,7 @@ class FavouritesTabButton(BaseTabButton): def __init__(self, parent=None): super().__init__( - 'Starred', + 'Favourites', common.FavouriteTab, 'Click to see your saved favourites', parent=parent diff --git a/bookmarks/topbar/topbar.py b/bookmarks/topbar/topbar.py index e5801742..4bb464e6 100644 --- a/bookmarks/topbar/topbar.py +++ b/bookmarks/topbar/topbar.py @@ -28,14 +28,14 @@ 'widget': tabs.FavouritesTabButton, 'hidden': False, }, + next(n): { + 'widget': buttons.ApplicationLauncherButton, + 'hidden': False, + }, next(n): { 'widget': filters.JobsFilterButton, 'hidden': True, }, - # next(n): { - # 'widget': filters.RootsFilterButton, - # 'hidden': False, - # }, next(n): { 'widget': filters.EntityFilterButton, 'hidden': True, @@ -56,10 +56,6 @@ 'widget': buttons.FilterButton, 'hidden': False, }, - next(n): { - 'widget': buttons.ApplicationLauncherButton, - 'hidden': False, - }, next(n): { 'widget': buttons.RefreshButton, 'hidden': False, From 93a20ba5e054680890b4432240655e6e2c2c70d0 Mon Sep 17 00:00:00 2001 From: Gergely Wootsch Date: Tue, 31 Oct 2023 09:34:27 +0100 Subject: [PATCH 28/32] bump version number --- README.md | 2 +- bookmarks/__init__.py | 4 ++-- bookmarks/maya/plugin.py | 2 +- docs/src/conf.py | 2 +- docs/src/guide.rst | 2 +- package/CMakeLists.txt | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 7dd42fa4..116b3ce2 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ - +

diff --git a/bookmarks/__init__.py b/bookmarks/__init__.py index 9841ac9d..59e139e9 100644 --- a/bookmarks/__init__.py +++ b/bookmarks/__init__.py @@ -76,7 +76,7 @@ .. |label3| image:: https://img.shields.io/badge/Platform-Windows-lightgrey :height: 18 -.. |label4| image:: https://img.shields.io/badge/Version-v0.9.0-green +.. |label4| image:: https://img.shields.io/badge/Version-v0.9.1-green :height: 18 .. |image1| image:: ./images/active_bookmark.png @@ -100,7 +100,7 @@ __email__ = 'hello@gergely-wootsch.com' #: Project version -__version__ = '0.9.0' +__version__ = '0.9.1' #: Project version __version_info__ = __version__.split('.') diff --git a/bookmarks/maya/plugin.py b/bookmarks/maya/plugin.py index 01aa229d..641263cb 100644 --- a/bookmarks/maya/plugin.py +++ b/bookmarks/maya/plugin.py @@ -17,7 +17,7 @@ product = 'bookmarks' __author__ = 'Gergely Wootsch' -__version__ = '0.9.0' +__version__ = '0.9.1' maya_useNewAPI = True diff --git a/docs/src/conf.py b/docs/src/conf.py index 420d1b59..f238bdff 100644 --- a/docs/src/conf.py +++ b/docs/src/conf.py @@ -33,7 +33,7 @@ author = 'Gergely Wootsch' # The full version, including alpha/beta/rc tags -release = '0.9.0' +release = '0.9.1' html_baseurl = 'https://bookmarks-vfx.com' html_extra_path = [ diff --git a/docs/src/guide.rst b/docs/src/guide.rst index 4a4e1f0f..5c4e787c 100644 --- a/docs/src/guide.rst +++ b/docs/src/guide.rst @@ -25,7 +25,7 @@ Get Bookmarks The project is hosted on `Github `_. -.. admonition:: Download the latest Windows release: `Bookmarks v0.9.0 `_ +.. admonition:: Download the latest Windows release: `Bookmarks v0.9.1 `_ ☹ Currently, Bookmarks only supports Windows. diff --git a/package/CMakeLists.txt b/package/CMakeLists.txt index 51b70d8f..7b60b07a 100644 --- a/package/CMakeLists.txt +++ b/package/CMakeLists.txt @@ -2,7 +2,7 @@ cmake_minimum_required(VERSION 3.20) project( Bookmarks - VERSION 0.9.0 + VERSION 0.9.1 DESCRIPTION "Bookmarks is a lightweight asset manager for digital artists working in the animation, motion graphics and VFX industries." HOMEPAGE_URL "https://bookmarks-vfx.com" ) From 2b6ea488cefea012c8d8b87b3dce31b9cf13a65b Mon Sep 17 00:00:00 2001 From: Gergely Wootsch Date: Tue, 31 Oct 2023 10:05:38 +0100 Subject: [PATCH 29/32] Handle execution based on file type to allow launching files via Application Launcher items --- bookmarks/actions.py | 65 +++++++++++++++++++++++++++++++++++++++ bookmarks/maya/actions.py | 4 +-- 2 files changed, 67 insertions(+), 2 deletions(-) diff --git a/bookmarks/actions.py b/bookmarks/actions.py index f22b6e92..79dfa1e0 100644 --- a/bookmarks/actions.py +++ b/bookmarks/actions.py @@ -1434,6 +1434,68 @@ def execute(index, first=False): else: path = common.get_sequence_end_path(path) + ext = QtCore.QFileInfo(path).suffix() + + # Handle Maya files + if ext in ('ma', 'mb'): + for app in ( + 'maya', 'maya2017', 'maya2018', 'maya2019', 'maya2020', 'maya2022', 'maya2023', 'maya2024', + 'maya2025', 'maya2026' + ): + executable = common.get_binary(app) + if not executable: + continue + execute_detached(executable, args=['-file', path]) + return + + # Handle Nuke files + if ext in ('nk', 'nknc'): + executable = common.get_binary('nuke') + if executable: + execute_detached(path, args=[path,]) + return + + # Handle Houdini files + if ext == 'hiplc': + for app in ('houdiniinidie', 'houindie', 'houind', 'houdiniind', 'hindie'): + executable = common.get_binary(app) + if executable: + execute_detached(executable, args=[path,]) + return + + if ext == 'hip': + for app in ('houdini', 'houdinifx', 'houfx', 'hfx', 'houdinicore', 'hcore'): + executable = common.get_binary(app) + if executable: + execute_detached(executable, args=[path,]) + return + + # Handle RV files + if ext == 'rv': + for app in ('rv', 'tweakrv', 'shotgunrv', 'shotgridrv', 'sgrv'): + executable = common.get_binary(app) + if executable: + execute_detached(executable, args=[path,]) + return + + # Handle blender files + if ext in ('blend', ): + for app in ('blender', 'blender2.8', 'blender2.9', 'blender3', 'blender3.0', 'blender3.1', 'blender3.2', + 'blender3.3', 'blender3.4', 'blender3.5' + ): + executable = common.get_binary(app) + if executable: + execute_detached(executable, args=[path,]) + return + + # Handle After Effects files + if ext in ('aep', ): + for app in ('afterfx', 'aftereffects', 'ae', 'afx'): + executable = common.get_binary(app) + if executable: + execute_detached(executable, args=[path,]) + return + url = QtCore.QUrl.fromLocalFile(path) QtGui.QDesktopServices.openUrl(url) @@ -1544,6 +1606,9 @@ def execute_detached(path, args=None): On Windows, we'll call the given file using the file explorer as we want to avoid the process inheriting the parent process' environment variables. + Args: + path (str): The path to the file to execute. + args (list): A list of optional arguments to pass to the process. """ if common.get_platform() == common.PlatformWindows: proc = QtCore.QProcess() diff --git a/bookmarks/maya/actions.py b/bookmarks/maya/actions.py index 5646469b..872a7814 100644 --- a/bookmarks/maya/actions.py +++ b/bookmarks/maya/actions.py @@ -217,7 +217,7 @@ def save_scene(increment=False, type='mayaAscii'): @common.error @common.debug def execute(index): - """Action used to execute a selected file item. + """Action used to execute a selected file item in Maya. """ file_path = common.get_sequence_end_path( @@ -226,7 +226,7 @@ def execute(index): file_info = QtCore.QFileInfo(file_path) # Open alembic, and maya files: - if file_info.suffix().lower() in ('ma', 'mb', 'abc'): + if file_info.suffix().lower() in ('ma', 'mb', 'abc', 'obj', 'fbx', 'usd', 'usda', 'usdc'): open_scene(file_info.filePath()) return From dd8a2e99528aefa506e413a13513dcedbe2485ba Mon Sep 17 00:00:00 2001 From: Gergely Wootsch Date: Wed, 1 Nov 2023 13:03:23 +0100 Subject: [PATCH 30/32] Update publish scripts to add stamp and correct entity linkage --- bookmarks/common/settings.py | 2 - bookmarks/external/akaconvert.py | 28 ++- bookmarks/external/rv.py | 6 +- bookmarks/items/views.py | 17 +- bookmarks/maya/actions.py | 6 +- bookmarks/publish.py | 302 +++++++++++++-------------- bookmarks/shotgun/sg_publish_clip.py | 51 ++++- bookmarks/ui.py | 35 +++- 8 files changed, 268 insertions(+), 179 deletions(-) diff --git a/bookmarks/common/settings.py b/bookmarks/common/settings.py index ae60dca5..7b193a0a 100644 --- a/bookmarks/common/settings.py +++ b/bookmarks/common/settings.py @@ -121,10 +121,8 @@ 'publish': ( 'publish/archive_existing', 'publish/template', - 'publish/task', 'publish/copy_path', 'publish/reveal', - 'publish/teams_notification', ), } diff --git a/bookmarks/external/akaconvert.py b/bookmarks/external/akaconvert.py index 46a29e6b..6f3bae6b 100644 --- a/bookmarks/external/akaconvert.py +++ b/bookmarks/external/akaconvert.py @@ -9,7 +9,6 @@ from .. import common from .. import database -from .. import log from ..editor import base @@ -288,8 +287,8 @@ class AkaConvertWidget(base.BasePropertyEditor): #: UI layout definition sections = { 0: { - 'name': 'Convert Image Sequence', - 'icon': 'convert', + 'name': 'AkaConvert', + 'icon': 'studioaka', 'color': common.color(common.color_dark_background), 'groups': { 0: { @@ -429,7 +428,7 @@ def save_changes(self): self.process = QtCore.QProcess(parent=self) self.process.readyReadStandardOutput.connect(self.read_output) - self.process.finished.connect(self.on_finished) + self.process.finished.connect(self.convert_process_finished) self.process.setProgram(get_convert_script_path()) self.process.setArguments(args) @@ -443,8 +442,24 @@ def read_output(self): data = self.process.readAllStandardOutput().data().decode('utf-8') for line in data.splitlines(): + if 'Movie saved to' in line: + + # Get the path to the output video + video_path = line.split('Movie saved to')[-1].strip().strip('"') + + if not QtCore.QFileInfo(video_path).exists(): + print(f'Could not find the output video: {video_path}') + else: + # Show the video in the files tab + common.signals.fileAdded.emit(video_path) + + # Push to RV + if self.akaconvert_pushtorv_editor.isChecked(): + from ..external import rv + rv.execute_rvpush_command(video_path, rv.PushAndClear) + if 'Finished' in line: - self.on_finished(0, QtCore.QProcess.NormalExit) + self.convert_process_finished(0, QtCore.QProcess.NormalExit) return if line.startswith('[AkaConvert Error]'): @@ -460,7 +475,7 @@ def read_output(self): 'Converting...', body=current_progress_line.replace('[AkaConvert Info]', ''), message_type=None, buttons=[], disable_animation=True, ) - def on_finished(self, exit_code, exit_status): + def convert_process_finished(self, exit_code, exit_status): # I don't know why, but the process doesn't terminate itself and will # keep running in the background. So we need to terminate it manually I guess! if exit_code != 0: @@ -472,6 +487,7 @@ def on_finished(self, exit_code, exit_status): common.show_message( 'Finished.', f'Conversion has finished successfully.' ) + raise RuntimeError('Finished.') def sizeHint(self): diff --git a/bookmarks/external/rv.py b/bookmarks/external/rv.py index dcb88aad..c9129ef1 100644 --- a/bookmarks/external/rv.py +++ b/bookmarks/external/rv.py @@ -69,7 +69,7 @@ def execute_rvpush_command(source, command): process1.waitForFinished(7000) # Wait 3 seconds for the process - QtCore.QThread.msleep(3000) + QtCore.QThread.msleep(2000) # Format the command cmd = command.format( @@ -82,3 +82,7 @@ def execute_rvpush_command(source, command): process2.setProcessEnvironment(env) process2.start(cmd) process2.waitForFinished(7000) + + errors = process2.readAllStandardError().data().decode('utf-8') + if errors: + raise RuntimeError(errors) diff --git a/bookmarks/items/views.py b/bookmarks/items/views.py index b3e2f34d..e3f5c3fe 100644 --- a/bookmarks/items/views.py +++ b/bookmarks/items/views.py @@ -1873,7 +1873,9 @@ def clickable_rectangle_event(self, event): if shift_modifier: # If the filter is empty we'll add a positive filter if not filter_texts: - self.model().set_filter_text(f'"/{_text}"') + if '#' not in _text: + _text = f'"/{_text}"' + self.model().set_filter_text(_text) self.repaint(self.rect()) return @@ -1885,7 +1887,10 @@ def clickable_rectangle_event(self, event): self.repaint(self.rect()) return # If the filter has items we'll append - self.model().set_filter_text(f'{filter_text} "/{_text}"') + if '#' not in _text: + _text = f'"/{_text}"' + self.model().set_filter_text(f'{filter_text} {_text}') + self.repaint(self.rect()) return @@ -1893,7 +1898,9 @@ def clickable_rectangle_event(self, event): if alt_modifier or control_modifier: # If the filter is empty we'll add a negative filter if not filter_texts: - self.model().set_filter_text(f'--"/{_text}"') + if '#' not in _text: + _text = f'"/{_text}"' + self.model().set_filter_text(f'--{_text}') self.repaint(self.rect()) return @@ -1902,7 +1909,9 @@ def clickable_rectangle_event(self, event): if _text in filter_text_element: del filter_texts[idx] - filter_texts.append(f'--"/{_text}"') + if '#' not in _text: + _text = f'"/{_text}"' + filter_texts.append(f'--{_text}') # add negative filter self.model().set_filter_text(' '.join(filter_texts)) diff --git a/bookmarks/maya/actions.py b/bookmarks/maya/actions.py index 872a7814..8085b06c 100644 --- a/bookmarks/maya/actions.py +++ b/bookmarks/maya/actions.py @@ -259,9 +259,11 @@ def save_warning(*args): return if workspace_info.path().lower() not in scene_file.filePath().lower(): + p = workspace_info.path() common.show_message( - f'Looks like you are saving "{scene_file.fileName()}" outside the current project\n\n', - body=f'The current project is:\n "{workspace_info.path()}"', + f'Scene is not in the current project.', + body=f'Look like "{scene_file.fileName()}" is being saved to another project:\n"{p}"\n\n' + f'You can safely ignore this message, it\'s just a friendly reminder.', message_type=None, disable_animation=True ) diff --git a/bookmarks/publish.py b/bookmarks/publish.py index a342607e..54c05656 100644 --- a/bookmarks/publish.py +++ b/bookmarks/publish.py @@ -8,7 +8,6 @@ import functools import json import os -import re import time import pyimageutil @@ -122,7 +121,7 @@ def get_payload(kwargs, destination): class TemplateModel(ui.AbstractListModel): - """Model used to list all available publish templates. + """Model used to list all available publish templates from the active bookmark item's token configuration. """ @@ -141,6 +140,16 @@ def init_data(self): template = common.settings.value('publish/template') for v in data[tokens.PublishConfig].values(): + if 'name' not in v: + print(f'Warning: Invalid publish template: {v}') + continue + if 'value' not in v: + print(f'Warning: Invalid publish template: {v}') + continue + if 'description' not in v: + print(f'Warning: Invalid publish template: {v}') + continue + if template == v['name']: pixmap = images.rsc_pixmap( 'check', common.color(common.color_green), common.size(common.size_margin) * 2 @@ -166,6 +175,8 @@ def init_data(self): class TemplateComboBox(QtWidgets.QComboBox): """Publish template picker. + The editor will list all relevant templates stored in the bookmark item database's token configuration. + """ def __init__(self, parent=None): @@ -174,82 +185,6 @@ def __init__(self, parent=None): self.setModel(TemplateModel()) -class TaskModel(ui.AbstractListModel): - def __init__(self, parent=None): - super(TaskModel, self).__init__(parent=parent) - - @common.error - @common.debug - def init_data(self): - """Initializes data. - - """ - self._data = {} - - k = common.active('asset', path=True) - if not k or not QtCore.QFileInfo(k).exists(): - return - - # Load the available task folders from the active bookmark item's `tokens`. - self._add_sub_folders(tokens.SceneFolder) - self._add_separator('Custom (click \'Add\' to add new)') - - def _add_separator(self, label): - self._data[len(self._data)] = { - QtCore.Qt.DisplayRole: label, - QtCore.Qt.DecorationRole: None, - QtCore.Qt.ForegroundRole: common.color(common.color_disabled_text), - QtCore.Qt.SizeHintRole: self.row_size, - QtCore.Qt.UserRole: None, - common.FlagsRole: QtCore.Qt.NoItemFlags - } - - def _add_sub_folders(self, token): - _icon = ui.get_icon('icon_bw', size=common.size(common.size_margin) * 2) - description = tokens.get_description(token) - for sub_folder in tokens.get_subfolders(token): - self._data[len(self._data)] = { - QtCore.Qt.DisplayRole: self.display_name(sub_folder), - QtCore.Qt.DecorationRole: _icon, - QtCore.Qt.ForegroundRole: common.color(common.color_text), - QtCore.Qt.SizeHintRole: self.row_size, - QtCore.Qt.StatusTipRole: description, - QtCore.Qt.AccessibleDescriptionRole: description, - QtCore.Qt.WhatsThisRole: description, - QtCore.Qt.ToolTipRole: description, - QtCore.Qt.UserRole: sub_folder, - } - - def add_item(self, task): - """Adds a new task item. - - """ - icon = ui.get_icon('icon_bw', size=common.size(common.size_margin) * 2) - - self.modelAboutToBeReset.emit() - self.beginResetModel() - - self._data[len(self._data)] = { - QtCore.Qt.DisplayRole: self.display_name(task), - QtCore.Qt.DecorationRole: icon, - QtCore.Qt.ForegroundRole: common.color(common.color_text), - QtCore.Qt.SizeHintRole: self.row_size, - QtCore.Qt.UserRole: task, - } - - self.endResetModel() - - -class TaskComboBox(QtWidgets.QComboBox): - """Task picker. - - """ - - def __init__(self, parent=None): - super().__init__(parent=parent) - self.setView(QtWidgets.QListView()) - self.setModel(TaskModel()) - class PublishWidget(base.BasePropertyEditor): """Publishes a footage. @@ -258,16 +193,20 @@ class PublishWidget(base.BasePropertyEditor): #: UI layout definition sections = { 0: { - 'name': None, - 'icon': None, - 'color': common.color(common.color_dark_background), + 'name': 'Publish File', + 'icon': 'icon', + 'color': common.color(common.color_green), 'groups': { 0: { 0: { 'name': 'Source', 'key': 'source', 'validator': None, - 'widget': QtWidgets.QLabel, + 'widget': functools.partial( + ui.PaintedLabel, + '', + color=common.color(common.color_selected_text) + ), 'placeholder': '', 'description': 'Source file path', }, @@ -275,9 +214,13 @@ class PublishWidget(base.BasePropertyEditor): 'name': 'Destination', 'key': 'destination', 'validator': None, - 'widget': QtWidgets.QLabel, + 'widget': functools.partial( + ui.PaintedLabel, + '', + color=common.color(common.color_green) + ), 'placeholder': '', - 'description': 'Final output path', + 'description': 'The publish\'s destination path', }, }, }, @@ -285,7 +228,7 @@ class PublishWidget(base.BasePropertyEditor): 1: { 'name': 'Template', 'icon': None, - 'color': common.color(common.color_dark_background), + 'color': None, 'groups': { 0: { 0: { @@ -294,17 +237,13 @@ class PublishWidget(base.BasePropertyEditor): 'validator': None, 'widget': TemplateComboBox, 'placeholder': None, - 'description': 'Select the publish template', - }, - 1: { - 'name': 'Task', - 'key': 'publish/task', - 'validator': None, - 'widget': TaskComboBox, - 'placeholder': None, - 'description': 'Select the publish template', + 'description': 'Select a publish template', + 'help': 'Select a publish template from the list. The templates can be customized in the ' + 'bookmark item\'s property editor.', }, - 2: { + }, + 1: { + 0: { 'name': 'Description', 'key': 'description', 'validator': None, @@ -314,7 +253,7 @@ class PublishWidget(base.BasePropertyEditor): 'publish.\nIndicate significant changes and ' 'notes here.', }, - 3: { + 1: { 'name': 'Specify Element', 'key': 'element', 'validator': base.text_validator, @@ -327,7 +266,7 @@ class PublishWidget(base.BasePropertyEditor): }, }, 2: { - 'name': 'Settings', + 'name': 'Options', 'icon': None, 'color': common.color(common.color_dark_background), 'groups': { @@ -343,15 +282,15 @@ class PublishWidget(base.BasePropertyEditor): }, 1: { 0: { - 'name': 'Copy Path to Clipboard', + 'name': 'Copy publish path', 'key': 'publish/copy_path', 'validator': None, 'widget': functools.partial(QtWidgets.QCheckBox, 'Enable'), 'placeholder': None, - 'description': 'Copy the path to the clipboard after finish.', + 'description': 'Copy the publish path to the clipboard after finish.', }, 1: { - 'name': 'Reveal Publish', + 'name': 'Reveal files', 'key': 'publish/reveal', 'validator': None, 'widget': functools.partial(QtWidgets.QCheckBox, 'Enable'), @@ -380,6 +319,44 @@ def __init__(self, index, parent=None): self._connect_settings_save_signals(common.SECTIONS['publish']) + self.setAttribute(QtCore.Qt.WA_DeleteOnClose) + + self.source_editor.clicked.connect(self.source_editor_clicked) + self.destination_editor.clicked.connect(self.destination_editor_clicked) + + @QtCore.Slot() + def source_editor_clicked(self): + v = self.source_editor.text() + if not v: + return + v = common.get_sequence_start_path(v) + file_info = QtCore.QFileInfo(v) + if not file_info.exists(): + common.show_message( + 'Warning', + f'Publish source file does not exist:\n\n{v}' + ) + return + + actions.reveal(v) + + @QtCore.Slot() + def destination_editor_clicked(self): + text = self.destination_editor.text() + if not text: + return + + file_info = QtCore.QFileInfo(text) + _dir = file_info.dir() + if not _dir.exists(): + common.show_message( + 'Publish directory does not exist', + f'Publish has not yet been created:\n\n{_dir.path()}' + ) + return + + actions.reveal(_dir.path()) + def init_progress_bar(self): """Initializes the progress bar. @@ -412,59 +389,12 @@ def init_data(self): raise ValueError('Invalid index value.') self.load_saved_user_settings(common.SECTIONS['publish']) - self.guess_task_and_element() self.init_db_data() self.init_thumbnail() self.set_source_text() - self.update_timer.start() self.init_progress_bar() - def guess_task_and_element(self): - """Guess the task and element name from the input source file name. - - Publishing an element requires both the task and the element - to be specified. The user can set these manually but if we can suggest them - based on the input name we'll set them for the user. - - """ - kwargs = self.get_publish_kwargs() - - p = self._index.data(common.PathRole) - p = common.get_sequence_start_path(p) - - i = QtCore.QFileInfo(p) - - s = f'{i.dir().path()}/{i.baseName()}' - s = s.replace(kwargs['prefix'], '') # remove prefix - s = _strip(s) - s = re.sub(r'v[0-9]{1,9}', '', s) # remove version string - s = _strip(s) - s = s.replace(kwargs['asset'], '') # remove asset name - s = _strip(s) - s = s.replace(common.get_username(), '') # remove username - s = _strip(s) - - u = common.settings.value('file_saver/user') - u = u if u else '' - s = s.replace(u, '') - s = _strip(s) - - sub_dirs = tokens.get_subfolders(tokens.SceneFolder) - if sub_dirs: - task_candidates = [f for f in sub_dirs if f in s] - if len(task_candidates) == 1: - self.publish_task_editor.setCurrentText(task_candidates[0]) - s = s.replace(task_candidates[0], '') - s = _strip(s) - - if s and len(s) >= 3 and 'main' not in s and '_' not in s: - if common.show_message( - 'Publish', body=f'Found a possible element name. Is it correct?\n\n{s}', buttons=[common.YesButton, - common.NoButton], modal=True, ) == QtWidgets.QDialog.Rejected: - return - self.element_editor.setText(s) - def init_thumbnail(self): """Load the item's current thumbnail. @@ -483,19 +413,15 @@ def set_source_text(self): """Set source item label. """ - c = common.rgb(common.color_green) - n = self._index.data(common.PathRole) - - self.source_editor.setText( - f'{n}' - ) + self.source_editor.setText(self._index.data(common.PathRole)) @QtCore.Slot() def update_expanded_template(self): - """Slot connected to the update timer used to preview the output file name. + """Slot used update the source and destination editors. """ kwargs = self.get_publish_kwargs() + if not kwargs['publish_template']: return tokens.invalid_token @@ -550,7 +476,6 @@ def get_publish_kwargs(self, **_kwargs): kwargs['sq'] = kwargs['sequence'] kwargs['seq'] = kwargs['sequence'] - kwargs['task'] = self.publish_task_editor.currentData() kwargs['element'] = self.element_editor.text() kwargs['element'] = kwargs['element'] if kwargs['element'] else 'main' @@ -851,3 +776,70 @@ def post_publish(self, destination, kwargs): actions.reveal(QtCore.QFileInfo(destination).dir().path()) if kwargs['publish_copy_path']: actions.copy_path(destination) + + # Get the source's description from the database + index = self._index + if not index.isValid(): + return + + path = index.data(common.PathRole) + if kwargs['is_collapsed']: + path = common.proxy_path(path) + + db = database.get(*common.active('root', args=True)) + description = db.value(path, 'description', database.AssetTable) + description = description if description else '' + + if '#published' not in description: + description += f' #published' + db.set_value(path, 'description', description, database.AssetTable) + + # Add a note to the database + note = ( + f'\nTime: {time.strftime("%d/%m/%Y %H:%M:%S")}' + f'\nUser: {common.get_username()}' + f'\nDestination:\n{QtCore.QFileInfo(destination).dir().path()}' + ) + notes = db.value(path, 'notes', database.AssetTable) + notes = notes if notes else {} + notes[len(notes)] = { + 'title': 'Publish Log (server)', + 'body': note, + 'extra_data': { + 'created_by': common.get_username(), + 'created_at': time.strftime('%d/%m/%Y %H:%M:%S'), + 'fold': False, + } + } + db.set_value(path, 'notes', notes, database.AssetTable) + + def showEvent(self, event): + """Called when the widget is shown. + + Args: + event (QtCore.QEvent): The event that triggered this slot. + + """ + super().showEvent(event) + self.update_timer.start() + + def hideEvent(self, event): + """Called when the widget is hidden. + + Args: + event (QtCore.QEvent): The event that triggered this slot. + + """ + super().hideEvent(event) + self.update_timer.stop() + + def closeEvent(self, event): + """Called when the widget is closed. + + Args: + event (QtCore.QEvent): The event that triggered this slot. + + """ + super().closeEvent(event) + self.update_timer.stop() + close() \ No newline at end of file diff --git a/bookmarks/shotgun/sg_publish_clip.py b/bookmarks/shotgun/sg_publish_clip.py index 4235d9ca..80204afc 100644 --- a/bookmarks/shotgun/sg_publish_clip.py +++ b/bookmarks/shotgun/sg_publish_clip.py @@ -1,11 +1,12 @@ """The publishing widget used by Bookmarks to create new PublishedFiles and Version entities on ShotGrid. -Our publishing logic creates `Version` and `PublishFile` entities linked against +The publish logic creates `Version` and `PublishFile` entities linked against the current active project and asset and uploads any custom thumbnails set. """ import re +import time from PySide2 import QtCore, QtGui, QtWidgets @@ -313,7 +314,6 @@ def save_changes(self): if not sg_properties.verify(asset=True): raise ValueError('Asset not configured.') - errors = [] with sg_properties.connection() as sg: @@ -347,11 +347,19 @@ def save_changes(self): ) # Publish steps for creating the Cut, CutInfo and Version entities + # Get the entity type + if 'task_entity' in data and data['task_entity']: + entity = data['task_entity'] + elif 'asset_entity' in data and data['asset_entity']: + entity = data['asset_entity'] + else: + raise RuntimeError('No asset or task entity found to associate with the publish.') + version_data = { 'project': data['project_entity'], 'code': data['name'], 'description': data['description'], - 'entity': data['asset_entity'], + 'entity': entity, 'user': user_entity } version = sg.create('Version', version_data) @@ -372,7 +380,7 @@ def save_changes(self): cut_data = { 'project': data['project_entity'], - 'entity': data['asset_entity'], + 'entity': entity, 'description': data['description'], 'version': version } @@ -474,6 +482,8 @@ def save_changes(self): common.close_message() + self.annotate_local_file(data) + errs = '\n\n'.join([str(e) for e in errors]) body = f'Publish successful.\n{len(errors)} errors occurred:\n\n{errs}' if errors else 'Publish successful.' common.show_message( @@ -482,7 +492,38 @@ def save_changes(self): message_type='success', parent=common.main_widget ) - return True + + def annotate_local_file(self, data): + # Stamp the file + # Get the source's description from the database + db = database.get(*common.active('root', args=True)) + description = db.value(data['file_path'], 'description', database.AssetTable) + description = description if description else '' + + if '#sg_published' not in description: + description += f' #sg_published' + db.set_value(data['file_path'], 'description', description, database.AssetTable) + + # Add a note to the database + note = ( + f'Publish Log (ShotGrid)' + f'\nName: {data["name"]}' + f'\nDescription: {data["description"]}' + f'\nTime: {time.strftime("%d/%m/%Y %H:%M:%S")}' + f'\nUser: {common.get_username()}' + ) + notes = db.value(data['file_path'], 'notes', database.AssetTable) + notes = notes if notes else {} + notes[len(notes)] = { + 'title': 'Publish Log (ShotGrid)', + 'body': note, + 'extra_data': { + 'created_by': common.get_username(), + 'created_at': time.strftime('%d/%m/%Y %H:%M:%S'), + 'fold': False, + } + } + db.set_value(data['file_path'], 'notes', notes, database.AssetTable) def get_publish_args(self): """Get all necessary arguments for publishing. diff --git a/bookmarks/ui.py b/bookmarks/ui.py index aa058bb9..fafd1201 100644 --- a/bookmarks/ui.py +++ b/bookmarks/ui.py @@ -361,7 +361,10 @@ def paintEvent(self, event): class PaintedLabel(QtWidgets.QLabel): - """QLabel used for static aliased label.""" + """QLabel used for static aliased label. + + """ + clicked = QtCore.Signal() def __init__( self, text, color=common.color(common.color_text), @@ -369,9 +372,21 @@ def __init__( parent=None ): super().__init__(text, parent=parent) + self._size = size self._color = color self._text = text + + self.update_size() + + def setText(self, v): + self._text = v + self.update_size() + + super().setText(v) + + def set_color(self, v): + self._color = v self.update_size() def update_size(self): @@ -393,12 +408,12 @@ def paintEvent(self, event): painter.begin(self) hover = option.state & QtWidgets.QStyle.State_MouseOver - pressed = option.state & QtWidgets.QStyle.State_Sunken - focus = option.state & QtWidgets.QStyle.State_HasFocus + # pressed = option.state & QtWidgets.QStyle.State_Sunken + # focus = option.state & QtWidgets.QStyle.State_HasFocus disabled = not self.isEnabled() o = 1.0 if hover else 0.8 - o = 0.3 if disabled else 1.0 + o = 0.3 if disabled else o painter.setOpacity(o) rect = self.rect() @@ -425,6 +440,18 @@ def enterEvent(self, event): """ self.update() + def mouseReleaseEvent(self, event): + """Event handler. + + """ + if not isinstance(event, QtGui.QMouseEvent): + return + # Check if the click was inside the label + if not self.rect().contains(event.pos()): + return + if event.button() == QtCore.Qt.LeftButton: + self.clicked.emit() + class ClickableIconButton(QtWidgets.QLabel): """A utility class for creating a square icon button. From 42818dcce65ca01da888462e6b023dc289510656 Mon Sep 17 00:00:00 2001 From: Gergely Wootsch Date: Thu, 2 Nov 2023 14:59:35 +0100 Subject: [PATCH 31/32] Add information widget to topbar and update file system monitor A mish-mash of work in one commit for adding an information row for the top bar and reworking how the file system monitor is updating internal data when a directory changes. --- bookmarks/common/core.py | 8 + bookmarks/common/data.py | 21 ++ bookmarks/common/monitor.py | 103 ++++-- bookmarks/contextmenu.py | 4 +- bookmarks/external/akaconvert.py | 14 +- bookmarks/items/file_items.py | 11 +- bookmarks/items/views.py | 175 ++++++++-- bookmarks/maya/actions.py | 4 +- bookmarks/maya/base.py | 8 +- bookmarks/maya/export.py | 9 +- .../maya/scripts/aka_character_caches.py | 19 +- bookmarks/rsc/gui/arrow_left.png | Bin 0 -> 6594 bytes bookmarks/rsc/gui/arrow_right.png | Bin 0 -> 6561 bytes bookmarks/topbar/quickswitch.py | 26 +- bookmarks/topbar/topbar.py | 312 +++++++++++++++--- 15 files changed, 563 insertions(+), 151 deletions(-) create mode 100644 bookmarks/rsc/gui/arrow_left.png create mode 100644 bookmarks/rsc/gui/arrow_right.png diff --git a/bookmarks/common/core.py b/bookmarks/common/core.py index ba50837a..d3b4bdc4 100644 --- a/bookmarks/common/core.py +++ b/bookmarks/common/core.py @@ -646,6 +646,14 @@ class DataDict(dict): """ + def __str__(self): + return ( + f'' + ) + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) diff --git a/bookmarks/common/data.py b/bookmarks/common/data.py index 669a43f6..e6a59d5a 100644 --- a/bookmarks/common/data.py +++ b/bookmarks/common/data.py @@ -88,6 +88,27 @@ def get_data(key, task, data_type): return common.item_data[key][task][data_type] +def get_data_from_value(value, data_type, role=common.PathRole): + """Get the internal data dictionary associated with a path. + + Args: + value (object): A value to match. + data_type (int): One of :attr:`~bookmarks.common.FileItem` or :attr:`~bookmarks.common.SequenceItem`. + + Returns: + common.DataDict: The cached data or None if not found. + + """ + for key in common.item_data: + for task in common.item_data[key]: + if data_type not in common.item_data[key][task]: + return None + data = common.item_data[key][task][data_type] + for idx in data: + if value in data[idx][role]: + return data + return None + def get_task_data(key, task): """Get cached data from :attr:`~bookmarks.common.item_data`. diff --git a/bookmarks/common/monitor.py b/bookmarks/common/monitor.py index 6a18d37b..93008b59 100644 --- a/bookmarks/common/monitor.py +++ b/bookmarks/common/monitor.py @@ -1,6 +1,7 @@ """QFileSystemWatchers used to monitor for file and directory changes. """ +import weakref from PySide2 import QtCore @@ -17,29 +18,43 @@ def get_watcher(tab_idx): class FileWatcher(QtCore.QFileSystemWatcher): + modelNeedsRefresh = QtCore.Signal(weakref.ref) + def __init__(self, tab_idx, parent=None): super().__init__(parent=parent) self.tab_idx = tab_idx - self.update_queue_timer = common.Timer(parent=self) - self.update_queue_timer.setSingleShot(True) - self.update_queue_timer.setInterval(500) + #: Timer used to limit the number of updates + self.update_timer = common.Timer(parent=self) + self.update_timer.setInterval(500) + self.update_timer.setTimerType(QtCore.Qt.CoarseTimer) + self.update_timer.setSingleShot(True) - self.changed_items = set() + self.update_queue = set() self._connect_signals() def _connect_signals(self): - self.update_queue_timer.timeout.connect(self.item_changed) + self.update_timer.timeout.connect(self.process_update_queue) + self.directoryChanged.connect(self.queue_changed_item) - QtCore.Slot(str) + self.modelNeedsRefresh.connect(common.signals.updateTopBarButtons) + self.modelNeedsRefresh.connect(self.update_model) + @QtCore.Slot(str) def queue_changed_item(self, v): - if v not in self.changed_items: - self.changed_items.add(v) - self.update_queue_timer.start(self.update_queue_timer.interval()) + """Slot used to add an updated path to the update queue. + + Args: + v (str): The path to add to the update queue. + + """ + if v not in self.update_queue.copy(): + self.update_queue.add(v) + + self.update_timer.start(self.update_timer.interval()) def add_directories(self, paths): """Adds the given list of directories to the file system watcher. @@ -55,30 +70,70 @@ def add_directories(self, paths): self.addPath(path) def reset(self): - """Remove all watch directories. + """Reset the watcher to its initial state. """ for v in self.directories(): self.removePath(v) for v in self.files(): self.removePath(v) + self.update_queue.clear() @QtCore.Slot() - def item_changed(self): - """Slot used to update the model status. + def process_update_queue(self): + """Slot used to mark a data dictionary as needing to be refreshed. + + Emits the modelNeedsRefresh signal for each data dictionary in the update queue. + + """ + refs = [] + for path in self.update_queue: + for data_type in (common.SequenceItem, common.FileItem): + data_dict = common.get_data_from_value(path, data_type, role=common.PathRole) + + if not data_dict: + continue + + ref = weakref.ref(data_dict) + if not ref in refs: + refs.append(data_dict) + else: + continue + + ref().refresh_needed = True + common.widget(self.tab_idx).filter_indicator_widget.repaint() + self.modelNeedsRefresh.emit(ref) + + self.update_queue.clear() + self.update_timer.stop() + + @QtCore.Slot(weakref.ref) + def update_model(self, ref): + """Slot used to update the model associated with the item tab index. + + Args: + ref (weakref.ref): A weak reference to the data dictionary that needs updating. """ - if self.tab_idx == common.FileTab: - self._file_item_updated() + if not ref(): + return - def _file_item_updated(self): - p = common.active('task', path=True) - if not p: + # If the data is relatively small, we don't have to bail out... + if len(ref()) > 999: return - for v in self.changed_items.copy(): - if p in v: - model = common.source_model(common.FileTab) - model.set_refresh_needed(True) - common.widget(common.FileTab).filter_indicator_widget.repaint() - break - self.changed_items = set() + + source_model = common.source_model(self.tab_idx) + p = source_model.source_path() + k = source_model.task() + + for t in (common.FileItem, common.SequenceItem,): + data = common.get_data(p, k, t) + + if not data: + source_model.reset_data(force=True) + return + + # Force reset the source model if we find a data match + if data == ref(): + source_model.reset_data(force=True) + return diff --git a/bookmarks/contextmenu.py b/bookmarks/contextmenu.py index 393a3ba6..34fc39f5 100644 --- a/bookmarks/contextmenu.py +++ b/bookmarks/contextmenu.py @@ -1011,7 +1011,7 @@ def task_folder_toggle_menu(self): item_on_pixmap = ui.get_icon('check', color=common.color(common.color_green)) item_off_pixmap = ui.get_icon('folder') - k = 'Select Task Folder...' + k = 'Select asset folder...' self.menu[k] = collections.OrderedDict() self.menu[f'{k}:icon'] = ui.get_icon( 'folder', color=common.color(common.color_green) @@ -1590,7 +1590,7 @@ def convert_menu(self): if akaconvert.KEY in os.environ and os.environ[akaconvert.KEY]: self.menu[key()] = { 'text': 'AkaConvert...', - 'icon': ui.get_icon('convert', color=common.color(common.color_blue)), + 'icon': ui.get_icon('studioaka', color=common.color(common.color_blue)), 'action': actions.convert_image_sequence_with_akaconvert } diff --git a/bookmarks/external/akaconvert.py b/bookmarks/external/akaconvert.py index 6f3bae6b..f202e4a2 100644 --- a/bookmarks/external/akaconvert.py +++ b/bookmarks/external/akaconvert.py @@ -293,7 +293,7 @@ class AkaConvertWidget(base.BasePropertyEditor): 'groups': { 0: { common.idx(reset=True, start=0): { - 'name': 'Video Preset', + 'name': 'Video preset', 'key': 'akaconvert_preset', 'validator': None, 'widget': PresetComboBox, @@ -301,7 +301,7 @@ class AkaConvertWidget(base.BasePropertyEditor): 'description': 'Select the video preset', }, common.idx(): { - 'name': 'Output Size', + 'name': 'Output size', 'key': 'akaconvert_size', 'validator': None, 'widget': SizeComboBox, @@ -309,7 +309,7 @@ class AkaConvertWidget(base.BasePropertyEditor): 'description': 'Set the output video size', }, common.idx(): { - 'name': 'Aces Config', + 'name': 'ACES version', 'key': 'akaconvert_acesprofile', 'validator': None, 'widget': AcesComboBox, @@ -319,7 +319,7 @@ class AkaConvertWidget(base.BasePropertyEditor): }, 1: { common.idx(): { - 'name': 'Input Colour Profile', + 'name': 'Input colour profile', 'key': 'akaconvert_inputcolor', 'validator': None, 'widget': ColorComboBox, @@ -327,7 +327,7 @@ class AkaConvertWidget(base.BasePropertyEditor): 'description': 'Select the image source\'s colour profile', }, common.idx(): { - 'name': 'Output Colour Profile', + 'name': 'Output colour profile', 'key': 'akaconvert_outputcolor', 'validator': None, 'widget': ColorComboBox, @@ -337,10 +337,10 @@ class AkaConvertWidget(base.BasePropertyEditor): }, 2: { common.idx(): { - 'name': 'Add Burn-In', + 'name': 'Add burn-in', 'key': 'akaconvert_videoburnin', 'validator': None, - 'widget': functools.partial(QtWidgets.QCheckBox, 'Add Label'), + 'widget': functools.partial(QtWidgets.QCheckBox, 'Add burn-in to video'), 'placeholder': None, 'description': 'Add video burn-in with timecode to the output video', }, diff --git a/bookmarks/items/file_items.py b/bookmarks/items/file_items.py index 7b5903ff..96842ef1 100644 --- a/bookmarks/items/file_items.py +++ b/bookmarks/items/file_items.py @@ -540,8 +540,9 @@ def init_data(self): data[idx][common.IdRole] = idx watcher = common.get_watcher(common.FileTab) - watcher.reset() - watcher.add_directories(sorted(set([f for f in _watch_paths if f]))) + _directories = watcher.directories() + # watcher.reset() + watcher.add_directories(sorted(set([f for f in _watch_paths if f] + _directories))) # Add the list of file extensions to the model's data _extensions = sorted(set([('.' + f) for f in _extensions if f])) @@ -553,7 +554,9 @@ def init_data(self): common.get_data(p, k, common.FileItem).subdirectories = _subdirectories common.get_data(p, k, common.SequenceItem).subdirectories = _subdirectories - self.set_refresh_needed(False) + # Model does not need a refresh at this point + common.get_data(p, k, common.FileItem).refresh_needed = False + common.get_data(p, k, common.SequenceItem).refresh_needed = False def disable_filter(self): """Overrides the token config and disables file filters.""" @@ -780,7 +783,7 @@ def get_hint_string(self): model = self.model().sourceModel() k = model.task() if not k: - return 'Click the File tab to select a folder' + return 'No asset folder select. Click the file tab to select a folder to browse' return f'No files found in "{k}"' @common.error diff --git a/bookmarks/items/views.py b/bookmarks/items/views.py index e3f5c3fe..87be6a43 100644 --- a/bookmarks/items/views.py +++ b/bookmarks/items/views.py @@ -374,7 +374,7 @@ class ProgressWidget(QtWidgets.QWidget): """Widget responsible for indicating files are being loaded.""" def __init__(self, parent=None): - super(ProgressWidget, self).__init__(parent=parent) + super().__init__(parent=parent) self.setAttribute(QtCore.Qt.WA_NoSystemBackground) self.setAttribute(QtCore.Qt.WA_TranslucentBackground) self.setAttribute(QtCore.Qt.WA_TransparentForMouseEvents) @@ -382,6 +382,11 @@ def __init__(self, parent=None): self.setWindowFlags(QtCore.Qt.Widget) self._message = 'Loading...' + self._connect_signals() + + def _connect_signals(self): + pass + def showEvent(self, event): """Event handler. @@ -428,6 +433,14 @@ class FilterOnOverlayWidget(ProgressWidget): if a model has filters set or if it requires a refresh. """ + def _connect_signals(self): + super()._connect_signals() + + common.signals.bookmarkActivated.connect(self.update) + common.signals.assetActivated.connect(self.update) + common.signals.tabChanged.connect(self.update) + common.signals.updateTopBarButtons.connect(self.update) + common.signals.filterTextChanged.connect(self.update) def paintEvent(self, event): """Event handler. @@ -831,6 +844,11 @@ def restore_selection(self): index, hint=QtWidgets.QAbstractItemView.PositionAtCenter ) + self.selectionModel().select( + index, + QtCore.QItemSelectionModel.ClearAndSelect | + QtCore.QItemSelectionModel.Rows + ) self.selectionModel().setCurrentIndex( index, QtCore.QItemSelectionModel.ClearAndSelect | @@ -841,16 +859,30 @@ def restore_selection(self): # Select the active item index = proxy.sourceModel().active_index() if index.isValid(): + self.selectionModel().select( + index, + QtCore.QItemSelectionModel.ClearAndSelect | + QtCore.QItemSelectionModel.Rows + ) self.selectionModel().setCurrentIndex( - index, QtCore.QItemSelectionModel.ClearAndSelect + index, + QtCore.QItemSelectionModel.ClearAndSelect | + QtCore.QItemSelectionModel.Rows ) self.scrollTo(index, QtWidgets.QAbstractItemView.PositionAtCenter) return # Select the first item in the list index = proxy.index(0, 0) + self.selectionModel().select( + index, + QtCore.QItemSelectionModel.ClearAndSelect | + QtCore.QItemSelectionModel.Rows + ) self.selectionModel().setCurrentIndex( - index, QtCore.QItemSelectionModel.ClearAndSelect + index, + QtCore.QItemSelectionModel.ClearAndSelect | + QtCore.QItemSelectionModel.Rows ) self.scrollTo(index, QtWidgets.QAbstractItemView.PositionAtCenter) @@ -1067,8 +1099,12 @@ def key_down(self): item in the list, we'll jump to the beginning, and vice-versa. """ - sel = self.selectionModel() - current_index = sel.currentIndex() + model = self.selectionModel() + if model.hasSelection(): + current_index = next(f for f in model.selectedIndexes()) + else: + current_index = QtCore.QModelIndex() + first_index = self.model().index(0, 0) last_index = self.model().index( self.model().rowCount() - 1, 0 @@ -1077,22 +1113,40 @@ def key_down(self): if first_index == last_index: return if not current_index.isValid(): # No selection - sel.setCurrentIndex( + model.select( + first_index, + QtCore.QItemSelectionModel.ClearAndSelect | + QtCore.QItemSelectionModel.Rows + ) + model.setCurrentIndex( first_index, - QtCore.QItemSelectionModel.ClearAndSelect + QtCore.QItemSelectionModel.ClearAndSelect | + QtCore.QItemSelectionModel.Rows ) return if current_index == last_index: # Last item is selected - sel.setCurrentIndex( + model.select( first_index, - QtCore.QItemSelectionModel.ClearAndSelect + QtCore.QItemSelectionModel.ClearAndSelect | + QtCore.QItemSelectionModel.Rows + ) + model.setCurrentIndex( + first_index, + QtCore.QItemSelectionModel.ClearAndSelect | + QtCore.QItemSelectionModel.Rows ) return - sel.setCurrentIndex( + model.select( + self.model().index(current_index.row() + 1, 0), + QtCore.QItemSelectionModel.ClearAndSelect | + QtCore.QItemSelectionModel.Rows + ) + model.setCurrentIndex( self.model().index(current_index.row() + 1, 0), - QtCore.QItemSelectionModel.ClearAndSelect + QtCore.QItemSelectionModel.ClearAndSelect | + QtCore.QItemSelectionModel.Rows ) def key_up(self): @@ -1103,8 +1157,15 @@ def key_up(self): item in the list, we'll jump to the beginning, and vice-versa. """ - sel = self.selectionModel() - current_index = sel.currentIndex() + + model = self.selectionModel() + if model.hasSelection(): + current_index = next(f for f in model.selectedIndexes()) + else: + current_index = QtCore.QModelIndex() + + self.itemDelegate().closeEditor.emit(None, QtWidgets.QAbstractItemDelegate.NoHint) + first_index = self.model().index(0, 0) last_index = self.model().index(self.model().rowCount() - 1, 0) @@ -1112,21 +1173,39 @@ def key_up(self): return if not current_index.isValid(): # No selection - sel.setCurrentIndex( + model.select( + last_index, + QtCore.QItemSelectionModel.ClearAndSelect | + QtCore.QItemSelectionModel.Rows + ) + model.setCurrentIndex( last_index, - QtCore.QItemSelectionModel.ClearAndSelect + QtCore.QItemSelectionModel.ClearAndSelect | + QtCore.QItemSelectionModel.Rows ) return if current_index == first_index: # First item is selected - sel.setCurrentIndex( + model.select( + last_index, + QtCore.QItemSelectionModel.ClearAndSelect | + QtCore.QItemSelectionModel.Rows + ) + model.setCurrentIndex( last_index, - QtCore.QItemSelectionModel.ClearAndSelect + QtCore.QItemSelectionModel.ClearAndSelect | + QtCore.QItemSelectionModel.Rows ) return - sel.setCurrentIndex( + model.select( self.model().index(current_index.row() - 1, 0), - QtCore.QItemSelectionModel.ClearAndSelect + QtCore.QItemSelectionModel.ClearAndSelect | + QtCore.QItemSelectionModel.Rows + ) + model.setCurrentIndex( + self.model().index(current_index.row() - 1, 0), + QtCore.QItemSelectionModel.ClearAndSelect | + QtCore.QItemSelectionModel.Rows ) def key_tab(self): @@ -1301,8 +1380,15 @@ def reset_row_layout(self, *args, **kwargs): # Restore the selection if row >= 0: index = proxy.index(row, 0) + self.selectionModel().select( + index, + QtCore.QItemSelectionModel.ClearAndSelect | + QtCore.QItemSelectionModel.Rows + ) self.selectionModel().setCurrentIndex( - index, QtCore.QItemSelectionModel.ClearAndSelect + index, + QtCore.QItemSelectionModel.ClearAndSelect | + QtCore.QItemSelectionModel.Rows ) self.scrollTo( index, QtWidgets.QAbstractItemView.PositionAtCenter @@ -1370,13 +1456,15 @@ def select_item(self, v, role=QtCore.Qt.DisplayRole): if v == k: index = proxy.mapFromSource(model.index(idx, 0)) - self.selectionModel().setCurrentIndex( + self.selectionModel().select( index, - QtCore.QItemSelectionModel.ClearAndSelect + QtCore.QItemSelectionModel.ClearAndSelect | + QtCore.QItemSelectionModel.Rows ) - self.selectionModel().select( + self.selectionModel().setCurrentIndex( index, - QtCore.QItemSelectionModel.ClearAndSelect + QtCore.QItemSelectionModel.ClearAndSelect | + QtCore.QItemSelectionModel.Rows ) self.scrollTo( index, @@ -1554,8 +1642,15 @@ def keyPressEvent(self, event): self.interruptRequested.emit() if self.selectionModel().hasSelection(): + self.selectionModel().select( + QtCore.QModelIndex(), + QtCore.QItemSelectionModel.ClearAndSelect | + QtCore.QItemSelectionModel.Rows + ) self.selectionModel().setCurrentIndex( - QtCore.QModelIndex(), QtCore.QItemSelectionModel.ClearAndSelect + QtCore.QModelIndex(), + QtCore.QItemSelectionModel.ClearAndSelect | + QtCore.QItemSelectionModel.Rows ) return if event.key() == QtCore.Qt.Key_Space: @@ -1625,9 +1720,15 @@ def keyPressEvent(self, event): if index.data(QtCore.Qt.DisplayRole)[ 0].lower() == self.timed_search_string.lower(): - sel.setCurrentIndex( + self.selectionModel().select( index, - QtCore.QItemSelectionModel.ClearAndSelect + QtCore.QItemSelectionModel.ClearAndSelect | + QtCore.QItemSelectionModel.Rows + ) + self.selectionModel().setCurrentIndex( + index, + QtCore.QItemSelectionModel.ClearAndSelect | + QtCore.QItemSelectionModel.Rows ) self.delay_save_selection() break @@ -1642,9 +1743,15 @@ def keyPressEvent(self, event): match = None if match: - sel.setCurrentIndex( + self.selectionModel().select( + index, + QtCore.QItemSelectionModel.ClearAndSelect | + QtCore.QItemSelectionModel.Rows + ) + self.selectionModel().setCurrentIndex( index, - QtCore.QItemSelectionModel.ClearAndSelect + QtCore.QItemSelectionModel.ClearAndSelect | + QtCore.QItemSelectionModel.Rows ) self.delay_save_selection() return @@ -1729,9 +1836,15 @@ def mousePressEvent(self, event): cursor_position = self.viewport().mapFromGlobal(common.cursor.pos()) index = self.indexAt(cursor_position) if not index.isValid(): + self.selectionModel().select( + QtCore.QModelIndex(), + QtCore.QItemSelectionModel.ClearAndSelect | + QtCore.QItemSelectionModel.Rows + ) self.selectionModel().setCurrentIndex( QtCore.QModelIndex(), - QtCore.QItemSelectionModel.ClearAndSelect + QtCore.QItemSelectionModel.ClearAndSelect | + QtCore.QItemSelectionModel.Rows ) super().mousePressEvent(event) @@ -2355,7 +2468,7 @@ def update_row(self, ref): """Queues an update request by the threads for later processing.""" if not ref(): return - if ref not in self.update_queue: + if ref not in self.update_queue.copy(): self.update_queue.append(ref) self.update_queue_timer.start(self.update_queue_timer.interval()) diff --git a/bookmarks/maya/actions.py b/bookmarks/maya/actions.py index 8085b06c..d039a1a7 100644 --- a/bookmarks/maya/actions.py +++ b/bookmarks/maya/actions.py @@ -261,8 +261,8 @@ def save_warning(*args): if workspace_info.path().lower() not in scene_file.filePath().lower(): p = workspace_info.path() common.show_message( - f'Scene is not in the current project.', - body=f'Look like "{scene_file.fileName()}" is being saved to another project:\n"{p}"\n\n' + f'Scene not part of the current project.', + body=f'"{scene_file.fileName()}" is being saved to: \n"{p}"\n\n' f'You can safely ignore this message, it\'s just a friendly reminder.', message_type=None, disable_animation=True diff --git a/bookmarks/maya/base.py b/bookmarks/maya/base.py index 93db6ec2..1b3be5e6 100644 --- a/bookmarks/maya/base.py +++ b/bookmarks/maya/base.py @@ -495,13 +495,9 @@ def report_export_progress(start, current, end, start_time): else: progress = float(_current) / float(_end) * 100 - progress = '[{}{}] {}%'.format( - '#' * int(progress), ' ' * (100 - int(progress)), int(progress) - ) - - msg = '# Exporting frame {current} of {end}\n# {progress}\n# Elapsed: {' \ + msg = '# Exporting frame {current} of {end}\n# Elapsed: {' \ 'elapsed}\n'.format( - current=current, end=end, progress=progress, elapsed=elapsed + current=current, end=end, elapsed=elapsed ) sys.stdout.write(msg) diff --git a/bookmarks/maya/export.py b/bookmarks/maya/export.py index aa0d8654..7d9add2c 100644 --- a/bookmarks/maya/export.py +++ b/bookmarks/maya/export.py @@ -244,14 +244,8 @@ def _teardown(): raise RuntimeError('Failed to prepare scene.') try: - # Our custom progress callback - perframecallback = f'"from bookmarks.maya import base;' \ - f'base.report_export_progress(' \ - f'{start_frame}, #FRAME#, {end_frame}, ' \ - f'{time.time()})"' - # Build the export command - cmd = '{f} {fr} {s} {uv} {ws} {wv} {wuvs} {wcs} {wfs} {sn} {rt} {df} {pfc} {ro}' + cmd = '{f} {fr} {s} {uv} {ws} {wv} {wuvs} {wcs} {wfs} {sn} {rt} {df} {ro}' cmd = cmd.format( f=f'-file "{destination}"', fr=f'-framerange {start_frame} {end_frame}', @@ -266,7 +260,6 @@ def _teardown(): sn='-stripNamespaces', rt=f'-root {" -root ".join(world_transforms)}', df='-dataFormat ogawa', - pfc=f'-pythonperframecallback {perframecallback}', ro='-renderableOnly' ) s = f'Alembic Export Job Arguments:\n{cmd}' diff --git a/bookmarks/maya/scripts/aka_character_caches.py b/bookmarks/maya/scripts/aka_character_caches.py index 44094444..3a3107ae 100644 --- a/bookmarks/maya/scripts/aka_character_caches.py +++ b/bookmarks/maya/scripts/aka_character_caches.py @@ -436,7 +436,7 @@ def _get_options(self): # apply_animation_exclusions # Change the value to a boolean - options['apply_animation_exclusions'] = options['apply_animation_exclusions'] == QtCore.Qt.Checked + options['apply_animation_exclusions'] = self.apply_animation_exclusions_editor.isChecked() # exclude_animation_from if options['apply_animation_exclusions']: @@ -456,6 +456,8 @@ def _get_options(self): else: options['exclude_animation_from'] = [] + print(f'Excluding animation from: {options["exclude_animation_from"]}') + # exclude_reset_pose_from _not_found = options['exclude_reset_pose_from'].split(',') _not_found = [x.strip() for x in _not_found] @@ -471,6 +473,8 @@ def _get_options(self): if x and cmds.objExists(f'{options["namespace"]}:{x}')] + print(f'Excluding reset pose from: {options["exclude_reset_pose_from"]}') + options['cut_in'] = cut_in options['cut_out'] = cut_out options['prefix'] = prefix @@ -581,6 +585,9 @@ def export(self): # Studio Library: Save the animation start pose try: + # Move the current time to cut_in + cmds.currentTime(cut_in) + mutils.savePose( os.path.normpath( f'{options["studio_library_output_folder"]}/{options["namespace"]}_animstart.pose/pose.json' @@ -641,12 +648,9 @@ def export(self): # Remove all animation from any specified excluded controllers if options['apply_animation_exclusions'] and options['exclude_animation_from']: - for obj in options['exclude_animation_from']: - attrs = cmds.listAnimatable(obj) - if not attrs: - continue - for attr in attrs: - cmds.cutKey(attr, clear=True) + for node in options['exclude_animation_from']: + print(f'Removing animation from: {node}') + cmds.cutKey(node, clear=True) # Make sure the pose is unaltered at cut_in cmds.currentTime(cut_in) @@ -878,7 +882,6 @@ def _apply_hip_pose(self): # Parent constrain the selection to the locator cmds.parentConstraint(locator, node, maintainOffset=True)[0] - def showEvent(self, event): super().showEvent(event) common.center_window(self) diff --git a/bookmarks/rsc/gui/arrow_left.png b/bookmarks/rsc/gui/arrow_left.png new file mode 100644 index 0000000000000000000000000000000000000000..a8163b1d7f33c58f01af461f0a3772aec9e62b05 GIT binary patch literal 6594 zcmeHMd0bO>wm&yS3HoGFJJPBU!)0+n!WxJ~P(g@gu}Ui{Er}sO1QJM4c5|)cg5nkw zQ9$uI+Oeeqqo|A`X{}%ck-Ahw1S&!FDMo^Y24k4tP0*QnpT0Nm&EMto3H+%@|Q=1Oq%e~1OS-C4GmfWKtW3i(8i-buQTtCpg$I=p{r$pv4_YH#;#t)2B5wa zMMbAahljBRl9c)UILSKU{LGY8#0}sdkeSLCBni{$>x2m+F^8eKRL`J`;y8>|-r>yf z)Fr}1QE0YQxH3B;N|2o-@QY&vETsEqvXO!mVLG3lnUXA)u`@Xgqh2;zlVmpr-N=%j z#9=HV4br2-m(!O>q(Zvie5R{_$#kc){N@Xo9!#%w9`ooPOb<^triUBT&z0%J_V8i5 zd(p>!7|5J7E}p$2Xz7?S^u%E#rl+T}-P|%VGUjJ^&X-6N+}!>A{74-h9A@S38)E_aO>S9KR;ti#g-|BhAQcFMHzJr0q&KPTB~l?jT_TN=NRr>})be+b z=^kG5z348hMB+F}hHUmf2nd7t=|T>J+%;EscUPuI6w{CG?aub{p38J+GnppVa7mmf zKI=DGJ=h-q%!Eq?<<@!db<>^pW;2*~C85`-S!7crq2RW2ziQD$@T&jF@pbAuK|WtR3oT3c~4;q%6U zu5F7CG~6w^O?#HO?UdA|)konqXW4$c#XYlbj0>gwI5FS$1l=j~#I&HgSIZ{5Q@!c# z=VPYs4?%MXr?YFm-^o@z4YO*`Mb5dOYunVB(a``ZT@)Z@q!2!zY7{E9S0GCjJz(uHnPnZm_d>;Vu#pL5lAk`0J*YP0iXXP#PvEtTwqLfeKBd_zs{D17 zQ?k~;l|HN13>+79O(|xw;94zKd|4B&eU`j&;Mnzh+|K>#{hRCElGOUjaG2^0PW`>2 zTYa~ly?!eRm!>J+u=C`W*LATaA;1jZUYNV^ZIOIWo<<%fjc@#r$dj5aFP%lM_wu9Q(Y+uzk3jfv|>mie*ZT;+dqqv{9O7n{yrER(qxozF)d7~%4Dl9EiH`ST* zGmx(xpv(+U(K)MjDfCMvr3l438%y9q7cxXtt(GUXUo3{eSlQ=>{78#Geli7GZ2vx*wV~4t9!>#f!FmME@YOIVhxGLF$LO;t zD#p+iCS5k!ym^P;Xp+nfN0=~mH&loEWC|SX4qj<`)rhJRvT0|xh;EH5b-5CR4k~n zE0Pa>7j0l#P{6W9D>)M?{T#)rZh%(qyB92*wURx!pPOn)Ss!nlDfoufhMN}@mpzLD z!gd1jwpw3ug4!SY8a}A2roi z>yP_lfsTl8lAM6@|3ISK0_gcHY{eF%;aqg$pS)Z?Z}f+`@@`)W;kTevy)D{+pxSFV zG^U*$#$0vgEg7L{mHN#+G)FNF$%43{GLI-OaP6WE@; z$`$YEm7s^daH8N~(rAO$+~@mAAc)hkiUO+KDMU32XOn%K3#Keahst+aHmomEti9&U5%%y~sIZxBM}8Zkt( zOzcyjqR{B=Qw#X%n$g?IRCvxKJz;)GA-14kHQ5(W#gvPTHpBJ+uiog1lPIY=`t{Mx zd(NLfKQ#EzcvwBThMHk|^G}{!yKwDpd>F6wRBd@a@)%_VH8AZB4Vvsn{_3C#aMpc; z-^a7o)yI+*6kNpDpi0k=vEypeO32| zMoLf?TM&|o8;ZbK9dmx{-}bmDnPa45@usuNs9^IqVzN(~3{EZ|B#`LI35DlnEptVGQqJ#DE5d-^fF zdWJ4y1k=uDpamSO8%O8*+}>mF%k1$nsmJE=RoTXC=L+be0s3TQK zVs%yIo*uS--FubTImi!OuCOvgS;bYzpL2lu=X&(}tpzC5S4oIxL$#+7;$vhmbEy09 z*huM`#!m=-Oo{q2o<=q`;$#=H#@x|`O!st;FTa@Z2yoRF?icN%sl356T&YScG4_J) z2c36Nb$Q5fi)SjPh0@E5flu$*XqT`VI_0L~nKI*UW zLymqUFXT`4l{Y6rQ17UmZe;*mZ> zao?%79`%4Tb`jc%+|GmQ>MJLh8!1GcuR^_>Yq&5tLz$0qLm;X)^pz#>8G3i+X@3hodO%%S7*nRsf4gax>5S)5 z0~wT}KX45`s%hC)-zRelhvtp$KNpgk@u$4_#x=g9NmJljzb8z#sX7;`{f29}I83ja zgJe6Y*Eh}NYKva<)b=zdpXA8j9z^Y|VlkvzGs z24;MSBF2`5F(7&YV$YgEy7(;>R?YX~PwpgW5S zvc9yy9w*4O!~(}L%G0JmZV8l6uxY)Ba*zh=ZLphKr$A~TpF*rB9VtisKbYKPav~ss z3g}ZE;m=`IuTEPUx+Q)+1>21({Zb%k(b6ffl?I8(2PfER4X3#x@_$B;A?Hu>VH`6tmtUVvHq-Q zZ2Y%Go`fh$52=s0$f_vhSvLdo9u9W%IBwsMCio07?39^R`G_e;b}3@+=g^GPI=DnY z#X(xAI6}HA9U^kv$~b`nWQ=Zi2< zUTk)x7>=V!zU8ucxvvJZ{dRV}bN6w)GE^I&>A)180n-rKP1}DJ;7G~v~^{K)f6|7DaW;EX3VQwuU ztQxf2Ro1|qDcD2IRpkEXu(h%?i@Llt1PZ;*H)65NCKiUEYr%Tcwcy$6PHM-4oPBr( zt~-q&_)*~0vrvV7q%^7VLvgtF2_F00*9;{loO*r*mn_phut0p9~oKYoF!L0^m+Zuhs7qh z?Jqdi;;LYgedDD)ck=eDj|gH1ZzBlPO;JdyjL@FJ14`5z=IXBmw@=qEEL3ap3zc%O z2BmRUihu)YOBMDJ(g?*Wr>xbA!o6?X1)_lOG%wFx8+GH_yEu)YxZLR0PxjRZ!%jV9Jp1+(cb{-+KMt`rhAVEf)Lk{oCWY zXPV&9x}DMSMh9GW8)ic-ayv;=W-N(PVKQ(a4^r6ls`E9SD3!w4C&`3^_N4N4MJLLYiPR=`<@O zGnGdVCmhn^W0uh(Br-9LezK#s47<Eet`ImG(C!AjRjG9~hLlD{zhM*J@hU~OY!-uU>pv81QJ zaUsuKm<7{#Rgiy+mdE8v#mx7_a>+)SNW3r$hB=eSM#_zliG`UGS)4?Y_Ijn3y$+e? zAK)86o4q0>BTt|L z{{rgo_b*WR&Lj#mh5r$pDB>nbWa&bfa7wx`S@PLhb_ljxri$l?gaiAh1i z*ZO>kXd|(*uk{oEpZZH>DR65E)BbBa#D*hY32s!194=Pws~xdiyx|p1ze$s zc)NLY5wWb|MEWas%70w}o7wDjVrcaLSZ*6kE=kJF7Rtoo$*@}g@pWRt;F-iewB^D4 z-zi#Uf7-LJ;jj@9&H?>j?}pcQE}_PCXvqnb8!sEOY{DP9m$@hJK5Iw70V4EI80bn)*fJ4OqU<>|c{9O+)JJZmO?C)x86ZAWDUw8{W z8)zsg`KZiviLtMIhYQb0$*U>5~lQ)&`V-eH-2 zdf;P*Qf>MN8uEkrSM0Xa1VNw|$a`c)*Jw>=rlmj98jmn~V)29DS`P9ajI_ppJ+2_H zeH*HnV{rWO`CzNcbQ%r$>2Rxdy4=#!5rOFx1BRRii}7{~zEtI=QS7?a@Uz_qMoJjR z3mnZ}sS1t4|2e((=~{;QSBAE=x9e2+Q_DSa7zfdtS#$8bH_d4oEYP7-V+gJX@Us+xp;m(Cd^fjEEkA&n6nyR;V#(U^o4_ElSpW>w7# zan+51v1ovP)6gdM&qJS&^EADNd@_~0Tz}l-b z`oUBhNg%X^Ai_Q5$T^={J}Dad#q&!-WLYPgW&4>qgPR#4w;gwR&zI z=0inTr2rG>UF$|FqsV$b0a7MA>%?^6Nd>wz?E#+NX)D-IDBNS`VbiIIPDFrOAt-_l zvM#zId;1oK?Z_@RIil|VdBxLlark_=%vUJD`z~DgGTymD4h5U;0(ALG=#s(*X{C{% zJ(v{#S)`Xahr8w8L(E5J>A9H`@$qRh|3dZsMX@Bo5?JthiP2p^r-D^&)G$sF_~0SU zFjx|t-cNJQou(7o3eD9l#?Rc2i0)v3c~P+LHS-O3lb!ua9k6k|4u>?6;HW%TwMv9+ znx;c+8fWt78Rn?+p_vjGm+N?-3+ai$AEIad@`??7puqsPaZ{>Fd-64^+ZkPu-&}xxax5Vmg#gVJR4JFKa5UlvLk_@cZ%$w z97fp|WUa%|Spa*86J1S zw6)-;v%v9anOr4lHL&!an|%z~UR5s^D7U~sTJ7|o<;zvI+uEH|H(D4yJ5u*MN=eI} zlDvR(44#dapSqiZ`Mg}LYVfM+YF;ZPIvXax=VkCXj~8t2)iZjQ$_(A%@e>h!m12hB zMMVLa_jo%)8ItFqK0gzabq14i7O0dc9wYw-sqCJGw&Zp&n=RE8C?e~xpletu69j`H zB^#y`^T7{&4jB9Ru6F0ar}t4sM!$>q7TBAU%RsZrbY|OEpt|?=P8S&6RSGb?q@Fcu zd1RgIzJ?SJDj0iWyY`4vj3T!mT<)ZXRb4Lv88P?|RV85l^Sumb*yCW_k)FAayJui! zn*{j_r94D`{ozZGDL>~!m094~-r8%As*{}cACWN0K^|b8|0>z~a5TvoME731ZO7UN z+QKXB!8gy?p_gp)+)KP0!QMU_k_S4*gJyOf5Nu~&cE_s7z(Y%z@Uq!cmJttwuO6Zw zIM#h()DpNM)*+c#gXw0*g3$6D#9BO_P2~gP* zRhMTWsry25NpLU<>&QXccDC;40iA2@ygZ<;ql8se!~&J6gTdS%Fh;joi^|TI*4mb> zVmLaVH@mxH#T9p~-34f#M4*b4ei!`*HGR*Ov~7{##noJL@y^-#$*QjG&~dt1+F1M| z!y@mu{BAu{I{#Rt@+dsv@cHK68;sW7a&@Q!xZ3H6uGwRn2@mdAQ*BA`e8cJrdtoja zxZzr~{9y0gI{hE{JOi7pXt5s(C1E?N<{K(hHSh$`@6LbQkbPmrh>S|Y^7;-lLRF?4 z@C?nJ*w(Vy)399CJTK2h9RsBC5)}WMA-R-u+`-$p8ZF;#dI(*Zhy3)iPO}Ir*bt-A zF2)Zq_NB|*+nSz6$r$g+%j1XRl+R0ki(qZ`(YYy<0V zDSAPd$n5O(;m9=wf^;RAozOtz9Y6yZo?l9jxr zco%6D6XdF26+)yj(d_(V1D?FZ7v3549B<40LWXgPtkcA4TK>dslFnOgk#;n-;>oFZ znEnDNuVX8-z8qd)r>?_2yp>ts3@6xiR1DcSYRlHwwqaEefG2m}VR8lTjY{26>6gR5 z*mdX#dCDDTh+x1f|GwYRrOC%CKR!$+XEhL}CVnxzfSgrtCBGU@AXgf#Ov+l{W0r;Rob-5SvldZv14mKimr^;z#PIqviWdR}FIgsMBk|)^|Wo zsYuzVU6@+C?Bgpi9^=r;UHv=W3gQq^tlZo0I5DVns>tl64xM9)!1NpL?KfqYOuOu! z=-;S)fBlK9ww#Qu?b`T}WBUOpe0=$2ZM(!=k>HMi$oox%1)dji9`?gL`ac1{ss5xz zV6F%ZCc%?4dt^(yam@x2_-LnA_+`8M8+dFgfZ8(Gl)w3=43Gc==si6<-MjMGtj@&1MXIEhqM;?&DnR_lhL#~Zl zc-h-jwsPdj0Me*k{KPHfhf~BNeE7sIG=SUpaczHBEB@^PXJmn!$IGeS%EzZ`?J&=g z?^*@s2n`5x?15m9SUCv{8t20mEvN`Wu;Gp}m{dInj=Cc_;qHDgBo#K2L~whpTspzE zk!uLG`&P9X1Su8O^ey#P1_i)DSW!3(eH6ke^Nh7_LG&Uxbxj+DZ|1c6x+BZKO)~2F z_V!PzMW@%4n~EMzFyf)efF*-;NCV;z_n`Q3hO>ruF;Q)KGw?tDsN8guQFI`8=Tv+Opv~&{A%hA zyufFD%@O^bk3!+R>#c0^F9Mfx$+{eS^lY(dMuq+D8I8(FYvaVk)m(L`ZWJ+6_g2_9 z&1m$e95x>`pc~Dzz}Q(K2&)cwrsBmIZ4b3pLKFP0sLYG*%tzz28>Khkj^? z%A}~U=P(+Tqinft{As44$LRi`7@9-8K)dG87bb^B%BKz5 z^oz|d0*(i%dCt;k{By=F$)kZ;+C@nQ6MWBkp7KUHfN%NifG z_Wcn*q6^f3J#v!nR(Y}MEfl_^2DA`Y_S>t5QdB?AGGFV66j5NdaN-~xy6`?)UTQ+o zi+wr!dBLwv S^-jb;QP734{NoD*pZpKdsH(64 literal 0 HcmV?d00001 diff --git a/bookmarks/topbar/quickswitch.py b/bookmarks/topbar/quickswitch.py index 17a5fa89..be304801 100644 --- a/bookmarks/topbar/quickswitch.py +++ b/bookmarks/topbar/quickswitch.py @@ -51,13 +51,9 @@ def add_switch_menu(self, idx, label): 'disabled': True } - active = False model = common.model(idx) for n in range(model.rowCount()): - index = QtCore.QPersistentModelIndex(model.index(n, 0)) - path = index.data(common.PathRole) - - active = active_index.isValid() and active_index.data(common.PathRole) == path + index = model.index(n, 0) pixmap, _ = images.get_thumbnail( index.data(common.ParentPathRole)[0], @@ -67,22 +63,14 @@ def add_switch_menu(self, idx, label): size=common.size(common.size_margin) * 4, fallback_thumb='icon_bw' ) - if pixmap and not pixmap.isNull(): - icon = QtGui.QIcon(pixmap) - else: - icon = item_icon - if active: + if not pixmap or pixmap.isNull(): + icon = ui.get_icon('icon_bw') + elif active_index.isValid() and active_index.data(common.PathRole) == index.data(common.PathRole): icon = on_icon + else: + icon = QtGui.QIcon(pixmap) - name = path - if len(index.data(common.ParentPathRole)) > 3: - name = '/'.join(index.data(common.ParentPathRole)[3:]) - elif len(index.data(common.ParentPathRole)) <= 3: - name = '/'.join(index.data(common.ParentPathRole)[1:]) - if index.data(common.AssetCountRole): - name = f'{name} | {index.data(common.AssetCountRole)} items' - name = name.replace('/', ' > ') - + name = index.data(QtCore.Qt.DisplayRole) self.menu[contextmenu.key()] = { 'text': name, 'icon': icon, diff --git a/bookmarks/topbar/topbar.py b/bookmarks/topbar/topbar.py index 4bb464e6..4fd9d2b2 100644 --- a/bookmarks/topbar/topbar.py +++ b/bookmarks/topbar/topbar.py @@ -4,12 +4,12 @@ from PySide2 import QtWidgets, QtGui, QtCore from . import buttons -from . import tabs from . import filters +from . import tabs from .. import common from .. import images +from .. import ui -n = (f for f in range(common.FavouriteTab + 1, 999)) BUTTONS = { common.BookmarkTab: { @@ -28,86 +28,327 @@ 'widget': tabs.FavouritesTabButton, 'hidden': False, }, - next(n): { + common.idx(reset=True, start=common.FavouriteTab + 1): { 'widget': buttons.ApplicationLauncherButton, 'hidden': False, }, - next(n): { + common.idx(): { 'widget': filters.JobsFilterButton, 'hidden': True, }, - next(n): { + common.idx(): { 'widget': filters.EntityFilterButton, 'hidden': True, }, - next(n): { + common.idx(): { 'widget': filters.TaskFilterButton, 'hidden': True, }, - next(n): { + common.idx(): { 'widget': filters.SubdirFilterButton, 'hidden': True, }, - next(n): { + common.idx(): { 'widget': filters.TypeFilterButton, 'hidden': True, }, - next(n): { + common.idx(): { 'widget': buttons.FilterButton, 'hidden': False, }, - next(n): { + common.idx(): { 'widget': buttons.RefreshButton, 'hidden': False, }, - next(n): { + common.idx(): { 'widget': buttons.ToggleSequenceButton, 'hidden': False, }, - next(n): { + common.idx(): { 'widget': buttons.ToggleArchivedButton, 'hidden': False, }, - next(n): { + common.idx(): { 'widget': buttons.ToggleFavouriteButton, 'hidden': False, }, - next(n): { + common.idx(): { 'widget': buttons.ToggleInlineIcons, 'hidden': False, }, } -class TopBarWidget(QtWidgets.QWidget): - """The bar above the stacked widget containing the main app control buttons. +class ContextStatusBar(QtWidgets.QWidget): + """The widget used to draw an informative status label below the main bar. + + The status label will display the current active context. """ def __init__(self, parent=None): - super().__init__(parent=parent) + super().__init__( + parent=parent + ) self.setAttribute(QtCore.Qt.WA_NoSystemBackground, True) self.setAttribute(QtCore.Qt.WA_TranslucentBackground, True) - self._buttons = {} + self.label_widget = None + self.note_widget = None + + self.setContextMenuPolicy(QtCore.Qt.DefaultContextMenu) self._create_ui() + self._connect_signals() def _create_ui(self): + o = common.size(common.size_indicator) * 3 + height = common.size(common.size_margin) + o + + self.setFixedHeight(height) + QtWidgets.QHBoxLayout(self) - self.layout().setContentsMargins(0, 0, 0, 0) - self.layout().setSpacing(0) self.layout().setAlignment(QtCore.Qt.AlignCenter) + self.layout().setContentsMargins(o, 0, o, 0) + self.layout().setSpacing(0) + + self.label_widget = ui.PaintedLabel( + '', + color=common.color(common.color_text), + size=common.size(common.size_font_medium) * 1.1, + parent=self + ) + self.note_widget = ui.PaintedLabel( + '', + color=common.color(common.color_blue), + size=common.size(common.size_font_medium) * 0.9, + parent=self + ) + + + self.arrow_left_button = ui.ClickableIconButton( + 'arrow_left', + (common.color(common.color_text), common.color(common.color_text)), + size=common.size(common.size_margin), + description='Previous item', + parent=self + ) + + self.arrow_right_button = ui.ClickableIconButton( + 'arrow_right', + (common.color(common.color_text), common.color(common.color_text)), + size=common.size(common.size_margin), + description='Next item', + parent=self + ) + + self.layout().addStretch() + self.layout().addWidget(self.arrow_left_button) + self.layout().addSpacing(o) + self.layout().addWidget(self.label_widget) + self.layout().addWidget(self.note_widget) + self.layout().addSpacing(o) + self.layout().addWidget(self.arrow_right_button) + self.layout().addStretch() + + def _connect_signals(self): + common.signals.bookmarkActivated.connect(self.update) + common.signals.assetActivated.connect(self.update) + common.signals.taskFolderChanged.connect(self.update) + common.signals.tabChanged.connect(self.update) + common.signals.updateTopBarButtons.connect(self.update) + + self.arrow_left_button.clicked.connect(self.arrow_left) + self.arrow_right_button.clicked.connect(self.arrow_right) + + self.label_widget.clicked.connect(self.show_quick_switch_menu) + + @QtCore.Slot() + def arrow_left(self): + """Slot responsible for activating the previous index in the current tab's parent view. + + """ + idx = common.current_tab() + if idx == common.BookmarkTab: + return + + widget = common.widget(idx - 1) + if not widget: + return + + index = widget.model().mapFromSource(widget.model().sourceModel().active_index()) + + widget.selectionModel().select( + index, + QtCore.QItemSelectionModel.ClearAndSelect | + QtCore.QItemSelectionModel.Rows + ) + widget.selectionModel().setCurrentIndex( + index, + QtCore.QItemSelectionModel.ClearAndSelect | + QtCore.QItemSelectionModel.Rows + ) + + widget.key_up() + widget.key_enter() + + @QtCore.Slot() + def arrow_right(self): + """Slot responsible for activating the next index in the current tab's parent view. + + """ + idx = common.current_tab() + if idx == common.BookmarkTab: + return + + widget = common.widget(idx - 1) + if not widget: + return + + index = widget.model().mapFromSource(widget.model().sourceModel().active_index()) + + widget.selectionModel().select( + index, + QtCore.QItemSelectionModel.ClearAndSelect | + QtCore.QItemSelectionModel.Rows + ) + widget.selectionModel().setCurrentIndex( + index, + QtCore.QItemSelectionModel.ClearAndSelect | + QtCore.QItemSelectionModel.Rows + ) + + widget.key_down() + widget.key_enter() + + def contextMenuEvent(self, event): + self.label_widget.clicked.emit() + + @QtCore.Slot() + def show_quick_switch_menu(self): + """Slot responsible for showing the quick switch menu. + + """ + idx = common.current_tab() + + from . import quickswitch + if idx == common.AssetTab: + menu = quickswitch.SwitchBookmarkMenu( + QtCore.QModelIndex(), + parent=self + ) + elif idx == common.FileTab: + menu = quickswitch.SwitchAssetMenu( + QtCore.QModelIndex(), + parent=self + ) + else: + return + + # Move the menu to the left of the label and just below it + menu.move( + self.label_widget.mapToGlobal( + QtCore.QPoint( + 0, + self.label_widget.height() + ) + ) + ) + menu.exec_() + + + @QtCore.Slot() + def update(self, *args, **kwargs): + """Update the informative labels based on the current context. + + """ + idx = common.current_tab() + + if idx == common.BookmarkTab: + self.setHidden(True) + else: + self.setHidden(False) + + display_name = '' + if idx > common.BookmarkTab: + active_index = common.active_index(idx - 1) + if active_index and active_index.isValid(): + display_name = active_index.data(QtCore.Qt.DisplayRole) + + if idx == common.FileTab: + task = common.active('task') + task = task if task else '(no asset folder selected)' + display_name = f'{display_name}/{task}' + + display_name = display_name.strip(' _-').replace('/', ' • ') + + if idx == common.FavouriteTab: + display_name = 'Favourites' + self.arrow_left_button.setHidden(True) + self.arrow_right_button.setHidden(True) + else: + self.arrow_left_button.setHidden(False) + self.arrow_right_button.setHidden(False) + + self.label_widget.setText(display_name) + + # Update note widget + source_model = common.source_model(common.current_tab()) + p = source_model.source_path() + k = source_model.task() + t = source_model.data_type() + + data = common.get_data(p, k, t) + if data and data.refresh_needed: + self.note_widget.setHidden(False) + self.note_widget.setText('(refresh needed)') + else: + self.note_widget.setHidden(True) + self.note_widget.setText('') + + + + +class TopBarWidget(QtWidgets.QWidget): + """The bar above the stacked widget containing the main app control buttons. + + """ + + def __init__(self, parent=None): + super().__init__(parent=parent) + + self._buttons = {} + + self._create_ui() + self._connect_signals() + + def _create_ui(self): o = common.size(common.size_indicator) * 3 height = common.size(common.size_margin) + o - self.setFixedHeight(height) + QtWidgets.QVBoxLayout(self) + + self.layout().setContentsMargins(0, 0, 0, 0) + self.layout().setSpacing(0) + self.layout().setAlignment(QtCore.Qt.AlignTop) + self.setAttribute(QtCore.Qt.WA_NoBackground, True) + self.setAttribute(QtCore.Qt.WA_TranslucentBackground, True) + + self.setSizePolicy( + QtWidgets.QSizePolicy.MinimumExpanding, + QtWidgets.QSizePolicy.Maximum + ) + # Buttons bar widget = QtWidgets.QWidget() + QtWidgets.QHBoxLayout(widget) + widget.layout().setContentsMargins(0, 0, o, 0) widget.layout().setSpacing(o) - widget.setAttribute(QtCore.Qt.WA_NoBackground, True) + widget.setFixedHeight(height) + + widget.setAttribute(QtCore.Qt.WA_NoSystemBackground, True) widget.setAttribute(QtCore.Qt.WA_TranslucentBackground, True) for idx in BUTTONS: @@ -117,30 +358,21 @@ def _create_ui(self): if idx > common.FavouriteTab: widget.layout().addWidget(self._buttons[idx], 0) else: - self.layout().addWidget(self._buttons[idx], 1) + widget.layout().addWidget(self._buttons[idx], 1) if idx == common.FavouriteTab: - self.layout().addStretch() + widget.layout().addStretch() self.layout().addWidget(widget) + widget = ContextStatusBar(parent=self) + self.layout().addWidget(widget, 1) + widget.setHidden(True) + + def _connect_signals(self): + pass + def button(self, idx): if idx not in self._buttons: raise ValueError('Button does not exist') return self._buttons[idx] - - def paintEvent(self, event): - """`TopBarWidget`' paint event.""" - painter = QtGui.QPainter() - painter.begin(self) - painter.setPen(QtCore.Qt.NoPen) - - pixmap = images.rsc_pixmap( - 'gradient', None, self.height() - ) - t = QtGui.QTransform() - t.rotate(90) - pixmap = pixmap.transformed(t) - painter.setOpacity(0.8) - painter.drawPixmap(self.rect(), pixmap, pixmap.rect()) - painter.end() From 913605c8c1756be4f5beed226c96d742b21cb8d2 Mon Sep 17 00:00:00 2001 From: Gergely Wootsch Date: Sun, 5 Nov 2023 23:10:24 +0100 Subject: [PATCH 32/32] Add job editor to replace the bookmarker module --- bookmarks/actions.py | 8 +- bookmarks/bookmarker/__init__.py | 23 - bookmarks/bookmarker/bookmark_editor.py | 375 ----- bookmarks/bookmarker/job_editor.py | 485 ------ bookmarks/bookmarker/main.py | 396 ----- bookmarks/bookmarker/server_editor.py | 410 ----- bookmarks/common/__init__.py | 2 +- bookmarks/common/settings.py | 9 +- bookmarks/common/ui.py | 7 +- bookmarks/contextmenu.py | 2 +- bookmarks/editor/base.py | 379 +++-- bookmarks/editor/jobs.py | 261 +++ bookmarks/editor/jobs_widgets.py | 1497 +++++++++++++++++ bookmarks/items/bookmark_items.py | 2 +- bookmarks/rsc/gui/branch_closed.png | Bin 3182 -> 6259 bytes bookmarks/rsc/gui/branch_open.png | Bin 3212 -> 6240 bytes bookmarks/rsc/stylesheet.qss | 102 +- bookmarks/shortcuts.py | 2 +- bookmarks/templates.py | 41 +- bookmarks/topbar/quickswitch.py | 2 +- bookmarks/ui.py | 137 +- .../bookmarks/bookmarker/bookmark_editor.rst | 10 - .../bookmarks/bookmarker/server_editor.rst | 10 - .../{session_lock.rst => active_mode.rst} | 4 +- .../{topbar/sgfilter.rst => editor/jobs.rst} | 4 +- .../main.rst => editor/jobs_widgets.rst} | 6 +- .../akaconvert.rst} | 6 +- ...oth_cache.rst => aka_character_caches.rst} | 4 +- .../maya/scripts/aka_cloth_cache_animated.rst | 10 - 29 files changed, 2147 insertions(+), 2047 deletions(-) delete mode 100644 bookmarks/bookmarker/__init__.py delete mode 100644 bookmarks/bookmarker/bookmark_editor.py delete mode 100644 bookmarks/bookmarker/job_editor.py delete mode 100644 bookmarks/bookmarker/main.py delete mode 100644 bookmarks/bookmarker/server_editor.py create mode 100644 bookmarks/editor/jobs.py create mode 100644 bookmarks/editor/jobs_widgets.py delete mode 100644 docs/src/modules/bookmarks/bookmarker/bookmark_editor.rst delete mode 100644 docs/src/modules/bookmarks/bookmarker/server_editor.rst rename docs/src/modules/bookmarks/common/{session_lock.rst => active_mode.rst} (86%) rename docs/src/modules/bookmarks/{topbar/sgfilter.rst => editor/jobs.rst} (87%) rename docs/src/modules/bookmarks/{bookmarker/main.rst => editor/jobs_widgets.rst} (80%) rename docs/src/modules/bookmarks/{bookmarker/job_editor.rst => external/akaconvert.rst} (79%) rename docs/src/modules/bookmarks/maya/scripts/{aka_cloth_cache.rst => aka_character_caches.rst} (81%) delete mode 100644 docs/src/modules/bookmarks/maya/scripts/aka_cloth_cache_animated.rst diff --git a/bookmarks/actions.py b/bookmarks/actions.py index 79dfa1e0..fcb2ddb8 100644 --- a/bookmarks/actions.py +++ b/bookmarks/actions.py @@ -728,11 +728,11 @@ def reset_row_size(): @common.error @common.debug -def show_bookmarker(): - """Shows :class:`~bookmarks.bookmarker.main.BookmarkerWidget`. +def show_job_editor(): + """Shows :class:`~bookmarks.editor.jobs.JobsEditor` widget. """ - from .bookmarker import main as editor + from .editor import jobs as editor widget = editor.show() return widget @@ -868,7 +868,7 @@ def add_item(): """ idx = common.current_tab() if idx == common.BookmarkTab: - show_bookmarker() + show_job_editor() elif idx == common.AssetTab: show_add_asset() elif idx == common.FileTab: diff --git a/bookmarks/bookmarker/__init__.py b/bookmarks/bookmarker/__init__.py deleted file mode 100644 index a76ca2df..00000000 --- a/bookmarks/bookmarker/__init__.py +++ /dev/null @@ -1,23 +0,0 @@ -"""This module defines the widgets needed to allow the user to select and create -bookmark items. - -This requires specifying servers, creating job folders, and letting the user pick -existing folders to be used as bookmark items. - -See :class:`~bookmarks.bookmarker.main.BookmarkerWidget`, -:class:`~bookmarks.bookmarker.server_editor.ServerItemEditor`, -:class:`~bookmarks.bookmarker.job_editor.JobItemEditor`, -and :class:`~bookmarks.bookmarker.bookmark_editor.BookmarkItemEditor` for more -information. - - -To show the main editor call: - -.. code-block:: python - :linenos: - - import bookmarks.actions - bookmarks.actions.show_bookmarker() - - -""" diff --git a/bookmarks/bookmarker/bookmark_editor.py b/bookmarks/bookmarker/bookmark_editor.py deleted file mode 100644 index 4483b36d..00000000 --- a/bookmarks/bookmarker/bookmark_editor.py +++ /dev/null @@ -1,375 +0,0 @@ -"""Editor widget used by :class:`~bookmarks.bookmarker.main.BookmarkerWidget` to save -bookmark items to the user settings file. - - -""" -import functools -import json -import os - -from PySide2 import QtCore, QtWidgets - -from .. import actions -from .. import common -from .. import contextmenu -from .. import log -from .. import shortcuts -from .. import ui - - -class BookmarkEditorContextMenu(contextmenu.BaseContextMenu): - """Context menu associated with :class:`BookmarkItemEditor`. - - """ - - def setup(self): - """Creates the context menu. - - """ - self.add_menu() - self.separator() - if isinstance( - self.index, QtWidgets.QListWidgetItem - ) and self.index.flags() & QtCore.Qt.ItemIsEnabled: - self.bookmark_properties_menu() - self.reveal_menu() - self.copy_json_menu() - self.separator() - self.refresh_menu() - - def add_menu(self): - """Add bookmark item action.""" - self.menu[contextmenu.key()] = { - 'text': 'Pick a new bookmark item...', - 'action': self.parent().add, - 'icon': ui.get_icon('add', color=common.color(common.color_green)) - } - - def reveal_menu(self): - """Reveal bookmark item action.""" - self.menu[contextmenu.key()] = { - 'text': 'Reveal', - 'action': functools.partial( - actions.reveal, f'{self.index.data(QtCore.Qt.UserRole)}/.' - ), - 'icon': ui.get_icon('folder') - } - - def refresh_menu(self): - """Refresh bookmark item list action.""" - self.menu[contextmenu.key()] = { - 'text': 'Refresh', - 'action': self.parent().init_data, - 'icon': ui.get_icon('refresh') - } - - def bookmark_properties_menu(self): - """Show the bookmark item property editor.""" - server = self.parent().window().server() - job = self.parent().window().job() - root = self.index.data(QtCore.Qt.DisplayRole) - - self.menu[contextmenu.key()] = { - 'text': 'Edit Properties...', - 'action': functools.partial(actions.edit_bookmark, server, job, root), - 'icon': ui.get_icon('settings') - } - - def copy_json_menu(self): - """Copy bookmark item as JSON action.""" - server = self.parent().window().server() - job = self.parent().window().job() - root = self.index.data(QtCore.Qt.DisplayRole) - - d = { - f'{server}/{job}/{root}': { - 'server': server, - 'job': job, - 'root': root - } - } - s = json.dumps(d) - - self.menu[contextmenu.key()] = { - 'text': 'Copy as JSON', - 'action': functools.partial( - QtWidgets.QApplication.clipboard().setText, s - ), - 'icon': ui.get_icon('copy') - } - - -class BookmarkItemEditor(ui.ListWidget): - """List widget containing a job's available bookmark items. - - """ - loaded = QtCore.Signal() - - def __init__(self, parent=None): - super().__init__( - default_icon='folder', - parent=parent - ) - - self._interrupt_requested = False - - self.setWindowTitle('Bookmark Item Editor') - self.setObjectName('BookmarkEditor') - - self.setAttribute(QtCore.Qt.WA_NoSystemBackground) - self.setAttribute(QtCore.Qt.WA_TranslucentBackground) - self.viewport().setAttribute(QtCore.Qt.WA_NoSystemBackground) - self.viewport().setAttribute(QtCore.Qt.WA_TranslucentBackground) - - self.setSizePolicy( - QtWidgets.QSizePolicy.MinimumExpanding, - QtWidgets.QSizePolicy.MinimumExpanding - ) - self.setMinimumWidth(common.size(common.size_width) * 0.2) - - self._connect_signals() - self._init_shortcuts() - - def _init_shortcuts(self): - """Initialize shortcuts.""" - shortcuts.add_shortcuts(self, shortcuts.BookmarkEditorShortcuts) - connect = functools.partial( - shortcuts.connect, shortcuts.BookmarkEditorShortcuts - ) - connect(shortcuts.AddItem, self.add) - - def _connect_signals(self): - """Connect signals.""" - super()._connect_signals() - - self.itemActivated.connect(self.toggle_item_state) - self.itemChanged.connect( - lambda item: self.add_remove_bookmark( - item.checkState(), - item.data(QtCore.Qt.DisplayRole) - ) - ) - - common.signals.serversChanged.connect(self.init_data) - - @QtCore.Slot(QtWidgets.QListWidgetItem) - def add_remove_bookmark(self, state, v): - """Slot used to add or remove a bookmark item. - - Args: - state (QtCore.Qt.CheckState): A check state. - v (QtWidgets.QListWidgetItem): The item that was just toggled. - - """ - if state == QtCore.Qt.Checked: - try: - actions.add_bookmark( - self.window().server(), - self.window().job(), - v - ) - except: - log.error('Could not add bookmark') - elif state == QtCore.Qt.Unchecked: - try: - actions.remove_bookmark( - self.window().server(), - self.window().job(), - v - ) - except: - log.error('Could not remove bookmark') - else: - raise ValueError('Invalid check state.') - - @QtCore.Slot(QtWidgets.QListWidgetItem) - def toggle_item_state(self, item): - """Slot used to toggle the check state of an item.""" - if item.checkState() == QtCore.Qt.Checked: - item.setCheckState(QtCore.Qt.Unchecked) - elif item.checkState() == QtCore.Qt.Unchecked: - item.setCheckState(QtCore.Qt.Checked) - self.add_remove_bookmark( - item.checkState(), - item.data(QtCore.Qt.DisplayRole) - ) - - @common.debug - @common.error - @QtCore.Slot() - def add(self, *args, **kwargs): - """Pick and add a folder as a new bookmark item. - - """ - if not self.window().server() or not self.window().job(): - return - - path = QtWidgets.QFileDialog.getExistingDirectory( - self, - 'Pick a new bookmark folder', - self.window().job_path(), - QtWidgets.QFileDialog.ShowDirsOnly | - QtWidgets.QFileDialog.DontResolveSymlinks - ) - - if not path: - return - - if self.window().job_path() not in path: - raise RuntimeError('Bookmark item must be inside the current job folder.') - dir = QtCore.QDir(path) - if not dir.exists() and not dir.mkdir(common.bookmark_cache_dir): - raise RuntimeError('Could not create bookmark') - - name = path[len(self.window().job_path()) + 1:] - - for n in range(self.count()): - item = self.item(n) - if item.data(QtCore.Qt.DisplayRole) == name: - common.show_message( - f'Cannot add "{name}" as a bookmark', - body='"{name}" is already a bookmark item.', - message_type='error' - ) - return - - # Add link - if not common.add_link(self.window().job_path(), name, section='links/root'): - log.error('Could not add link') - - # Add the QListWidgetItem - item = QtWidgets.QListWidgetItem() - item.setFlags( - QtCore.Qt.ItemIsEnabled | - QtCore.Qt.ItemIsSelectable | - QtCore.Qt.ItemIsUserCheckable - ) - item.setCheckState(QtCore.Qt.Unchecked) - item.setData(QtCore.Qt.DisplayRole, name) - item.setData(QtCore.Qt.UserRole, path) - size = QtCore.QSize( - 0, - common.size(common.size_margin) * 2 - ) - item.setSizeHint(size) - self.update_state(item) - self.insertItem(self.count(), item) - self.setCurrentItem(item) - - def contextMenuEvent(self, event): - """Context menu event.""" - item = self.itemAt(event.pos()) - menu = BookmarkEditorContextMenu(item, parent=self) - pos = event.pos() - pos = self.mapToGlobal(pos) - menu.move(pos) - menu.exec_() - - @QtCore.Slot() - def init_data(self): - """Initializes data. - - """ - self.clear() - - if not self.window().job(): - return - - max_recursion = common.settings.value('settings/job_scan_depth') - max_recursion = 3 if not max_recursion else max_recursion - - it = self.item_generator(self.window().job_path(), max_recursion=max_recursion) - items = sorted(set({f for f in it})) - - for name, path in items: - self.progressUpdate.emit(f'Parsing {path}...') - - item = QtWidgets.QListWidgetItem() - item.setFlags( - QtCore.Qt.ItemIsEnabled | - QtCore.Qt.ItemIsSelectable | - QtCore.Qt.ItemIsUserCheckable - ) - item.setData(QtCore.Qt.DisplayRole, name) - item.setData(QtCore.Qt.UserRole, path) - item.setData(QtCore.Qt.StatusTipRole, path) - item.setData(QtCore.Qt.WhatsThisRole, path) - item.setData(QtCore.Qt.ToolTipRole, path) - item.setSizeHint(QtCore.QSize(0, common.size(common.size_margin) * 2)) - - self.update_state(item) - self.insertItem(self.count(), item) - - self.progressUpdate.emit('') - - @QtCore.Slot(QtWidgets.QListWidgetItem) - def update_state(self, item): - """Checks if the item is part of the current bookmark set and sets the item's - check state and icon accordingly. - - Args: - item(QtWidgets.QListWidgetItem): The item to verify. - - """ - self.blockSignals(True) - if item.data(QtCore.Qt.UserRole) in list(common.bookmarks): - item.setCheckState(QtCore.Qt.Checked) - else: - item.setCheckState(QtCore.Qt.Unchecked) - self.blockSignals(False) - - def item_generator(self, path, recursion=0, max_recursion=3): - """Recursive scanning function for finding bookmark folders - inside the given path. - - """ - if self._interrupt_requested: - return - - # If links exist, return items stored in the link file and nothing else - if recursion == 0: - links = common.get_links(path, section='links/root') - for v in links: - if self._interrupt_requested: - return - yield v, f'{path}/{v}' - - # Otherwise parse the folder - recursion += 1 - if recursion > max_recursion: - return - - # Let unreadable paths fail silently - try: - it = os.scandir(path) - except: - return - - for entry in it: - if not entry.is_dir(): - continue - - # yield the match - path = entry.path.replace('\\', '/') - self.progressUpdate.emit(f'Scanning {path}. Please wait...') - - if entry.name == common.bookmark_cache_dir: - _path = '/'.join(path.split('/')[:-1]) - _name = _path[len(self.window().job_path()) + 1:] - yield _name, _path - - for _name, _path in self.item_generator( - path, - recursion=recursion, - max_recursion=max_recursion - ): - if self._interrupt_requested: - return - yield _name, _path - - def keyPressEvent(self, event): - """Key press event handler. - - """ - if event.key() == QtCore.Qt.Key_Escape: - self._interrupt_requested = True diff --git a/bookmarks/bookmarker/job_editor.py b/bookmarks/bookmarker/job_editor.py deleted file mode 100644 index f412f139..00000000 --- a/bookmarks/bookmarker/job_editor.py +++ /dev/null @@ -1,485 +0,0 @@ -"""Editor widget used by :class:`~bookmarks.bookmarker.main.BookmarkerWidget` to add and -select jobs found inside a server. - -The module also defines :class:`AddJobWidget`, an editor used to create new jobs inside -a server. - -""" -import functools -import os - -from PySide2 import QtCore, QtGui, QtWidgets - -from .. import actions -from .. import common -from .. import contextmenu -from .. import images -from .. import log -from .. import shortcuts -from .. import templates -from .. import ui -from ..editor import base - - -def get_job_icon(path): - """Checks the given job folder for the presence of a thumbnail image file. - - """ - for entry in os.scandir(path): - if 'thumbnail' not in entry.name.lower(): - continue - pixmap = images.ImageCache.get_pixmap( - QtCore.QFileInfo(entry.path).filePath(), - common.thumbnail_size - ) - if not pixmap or pixmap.isNull(): - continue - return QtGui.QIcon(pixmap) - - path = common.rsc( - f'{common.GuiResource}/asset_item.{common.thumbnail_format}' - ) - pixmap = images.ImageCache.get_pixmap(path, common.thumbnail_size) - return QtGui.QIcon(pixmap) - - -class AddJobWidget(base.BasePropertyEditor): - """A custom `BasePropertyEditor` used to add new jobs on a server. - - """ - buttons = ('Create Job', 'Cancel') - - #: UI layout definition - sections = { - 0: { - 'name': 'Add Job', - 'icon': '', - 'color': common.color(common.color_dark_background), - 'groups': { - 0: { - 0: { - 'name': 'Name', - 'key': None, - 'validator': base.job_name_validator, - 'widget': ui.LineEdit, - 'placeholder': 'Name, e.g. `MY_NEW_JOB`', - 'description': 'The job\'s name, e.g. `MY_NEW_JOB`.', - }, - }, - 1: { - 0: { - 'name': 'Template', - 'key': None, - 'validator': None, - 'widget': functools.partial( - templates.TemplatesWidget, templates.JobTemplateMode - ), - 'placeholder': None, - 'description': 'Select a folder template to create this asset.', - }, - }, - }, - }, - } - - def __init__(self, server, parent=None): - super().__init__( - server, - None, - None, - asset=None, - db_table=None, - buttons=self.buttons, - parent=parent - ) - - self.jobs = None - self.setWindowTitle(f'{self.server}: Add Job') - - common.signals.templateExpanded.connect(self.close) - common.signals.jobAdded.connect(self.close) - common.signals.serversChanged.connect(self.close) - - def db_source(self): - """A file path to use as the source of database values. - - Returns: - str: The database source file. - - """ - return None - - def init_data(self): - """Initialize data. - - """ - self.jobs = [] - items = [] - - it = self.parent().job_editor.item_generator(self.server, emit_progress=False) - for name, path in it: - items.append(name) - - completer = QtWidgets.QCompleter(items, parent=self) - completer.setCaseSensitivity(QtCore.Qt.CaseInsensitive) - common.set_stylesheet(completer.popup()) - self.name_editor.setCompleter(completer) - self.name_editor.setFocus(QtCore.Qt.MouseFocusReason) - - @common.error - @common.debug - def save_changes(self): - """Saves changes. - - """ - text = self.name_editor.text() - if not text: - raise ValueError('Must enter a name to create a job.') - - text = text.replace('\\', '/') - root = self.server - - if '/' in self.name_editor.text(): - _name = text.split('/')[-1] - _root = text[:-len(_name) - 1] - name = _name - root = f'{self.server}/{_root}'.rstrip('/') - - if not QtCore.QFileInfo(root).exists(): - if not QtCore.QDir(root).mkpath('.'): - raise RuntimeError('Error creating folders.') - else: - name = text - - # Create template and signal - self.template_editor.template_list_widget.create(name, root) - path = f'{root}/{name}' - - # Save link - if '/' in text: - if not common.add_link(root, name, section='links/job'): - log.error('Could not add link') - - file_info = QtCore.QFileInfo(path) - if not file_info.exists(): - raise RuntimeError('Could not find the added job.') - - try: - path += f'/thumbnail.{common.thumbnail_format}' - self.thumbnail_editor.save_image(destination=path) - except: - pass - - common.signals.jobAdded.emit(file_info.filePath()) - common.show_message(f'{name} was successfully created.', message_type='success') - return True - - -class JobContextMenu(contextmenu.BaseContextMenu): - """Context menu associated with :class:`JobItemEditor`. - - """ - - def setup(self): - """Creates the context menu. - - """ - self.add_menu() - self.separator() - self.reveal_menu() - self.separator() - self.refresh_menu() - - def add_menu(self): - """Add job item action. - - """ - self.menu['Add Job...'] = { - 'action': self.parent().add, - 'icon': ui.get_icon('add', color=common.color(common.color_green)) - } - - def reveal_menu(self): - """Reveal job item action. - - """ - self.menu['Reveal...'] = { - 'action': lambda: actions.reveal( - f'{self.index.data(QtCore.Qt.UserRole)}/.' - ), - 'icon': ui.get_icon('folder') - } - - def refresh_menu(self): - """Refresh job list action. - - """ - self.menu['Refresh'] = { - 'action': self.parent().init_data, - 'icon': ui.get_icon('refresh') - } - - -class JobItemEditor(ui.ListViewWidget): - """Simple list widget used to add and remove servers to/from the local - common. - - """ - - def __init__(self, parent=None): - super().__init__( - default_icon='asset', - parent=parent - ) - - self._interrupt_requested = False - - self.setWindowTitle('Job Editor') - self.setObjectName('JobEditor') - - self.setAttribute(QtCore.Qt.WA_NoSystemBackground) - self.setAttribute(QtCore.Qt.WA_TranslucentBackground) - self.viewport().setAttribute(QtCore.Qt.WA_NoSystemBackground) - self.viewport().setAttribute(QtCore.Qt.WA_TranslucentBackground) - - self.setSizePolicy( - QtWidgets.QSizePolicy.MinimumExpanding, - QtWidgets.QSizePolicy.MinimumExpanding - ) - self.setMinimumWidth(common.size(common.size_width) * 0.2) - - self._connect_signals() - self._init_shortcuts() - - def _init_shortcuts(self): - """Initialize shortcuts. - - """ - shortcuts.add_shortcuts(self, shortcuts.BookmarkEditorShortcuts) - connect = functools.partial( - shortcuts.connect, shortcuts.BookmarkEditorShortcuts - ) - connect(shortcuts.AddItem, self.add) - - def _connect_signals(self): - """Connect signals. - - """ - super()._connect_signals() - - self.selectionModel().selectionChanged.connect( - functools.partial(common.save_selection, self) - ) - - common.signals.bookmarkAdded.connect(self.update_text) - common.signals.bookmarkRemoved.connect(self.update_text) - - common.signals.jobAdded.connect(self.init_data) - common.signals.jobAdded.connect( - lambda v: common.select_index(self, v, role=QtCore.Qt.UserRole) - ) - common.signals.serversChanged.connect(self.init_data) - - def item_generator(self, source, emit_progress=True): - """Scans the given source, usually a server, to find job items. - - """ - if emit_progress: - self.progressUpdate.emit('') - - # Parse source otherwise - for entry in os.scandir(source): - if self._interrupt_requested: - if emit_progress: - self.progressUpdate.emit('') - return - - if not entry.is_dir(): - continue - - file_info = QtCore.QFileInfo(entry.path) - if emit_progress: - self.progressUpdate.emit(f'Scanning: {file_info.filePath()}') - - if file_info.isHidden(): - continue - if not file_info.isReadable(): - continue - # Test access - try: - next(os.scandir(file_info.filePath())) - except: - continue - - # Use paths in the link file, if available - links = common.get_links(file_info.filePath(), section='links/job') - if links: - for link in links: - _file_info = QtCore.QFileInfo(f'{file_info.filePath()}/{link}') - _name = _file_info.filePath()[len(source) + 1:] - yield _name, _file_info.filePath() - else: - yield entry.name, file_info.filePath() - - if emit_progress: - self.progressUpdate.emit('') - - @QtCore.Slot() - def init_data(self, *args, **kwargs): - """Load job item data. - - """ - selected_index = common.get_selected_index(self) - selected_name = selected_index.data( - QtCore.Qt.DisplayRole - ) if selected_index.isValid() else None - - self.selectionModel().blockSignals(True) - - self.model().sourceModel().clear() - self._interrupt_requested = False - - if ( - not self.window().server() or - not QtCore.QFileInfo(self.window().server()).exists() - ): - self.selectionModel().blockSignals(False) - return - - for name, path in self.item_generator(self.window().server()): - item = QtGui.QStandardItem() - - item.setFlags( - QtCore.Qt.ItemIsEnabled | - QtCore.Qt.ItemIsSelectable - ) - - _name = ( - name. - replace('_', ' '). - replace(' ', ' '). - strip(). - replace('/', ' | '). - strip() - ) - - item.setData(_name, role=QtCore.Qt.DisplayRole) - item.setData(path, role=QtCore.Qt.UserRole) - item.setData(name, role=QtCore.Qt.UserRole + 1) - item.setData(path, role=QtCore.Qt.StatusTipRole) - item.setData(path, role=QtCore.Qt.WhatsThisRole) - item.setData(path, role=QtCore.Qt.ToolTipRole) - - size = QtCore.QSize(0, common.size(common.size_margin) * 2) - item.setSizeHint(size) - - _icon = get_job_icon(path) - item.setData( - _icon if _icon else None, - role=QtCore.Qt.DecorationRole - ) - - self.addItem(item) - - self.update_text() - self.progressUpdate.emit('Loading jobs...') - - if selected_name: - for idx in range(self.model().rowCount()): - index = self.model().index(idx, 0) - if index.data(QtCore.Qt.DisplayRole) == selected_name: - self.selectionModel().select( - index, QtCore.QItemSelectionModel.ClearAndSelect - ) - break - - self.selectionModel().blockSignals(False) - - common.restore_selection(self) - self.progressUpdate.emit('') - - @QtCore.Slot() - def update_text(self): - """Checks each job item to see if they have active bookmark items, and marks - them visually as active. - - """ - if not self.model(): - return - - active_jobs = set([f['job'] for f in common.bookmarks.values()]) - suffix = ' (Active)' - - for n in range(self.model().rowCount()): - index = self.model().index(n, 0) - if not index.isValid(): - continue - source_index = self.model().mapToSource(index) - if not source_index.isValid(): - continue - item = self.model().sourceModel().itemFromIndex(source_index) - if not item: - continue - - if item.data(QtCore.Qt.UserRole + 1) in active_jobs: - item.setData( - common.color(common.color_green), - role=QtCore.Qt.ForegroundRole - ) - if suffix not in item.data(QtCore.Qt.DisplayRole): - item.setData( - f'{item.data(QtCore.Qt.DisplayRole)}{suffix}', - role=QtCore.Qt.DisplayRole - ) - continue - - item.setData( - common.color(common.color_text), - role=QtCore.Qt.ForegroundRole - ) - name = item.data(QtCore.Qt.DisplayRole).replace(suffix, '') - item.setData( - name, - role=QtCore.Qt.DisplayRole - ) - - @QtCore.Slot() - def add(self): - """Opens the widget used to create new job items. - - """ - if not self.window().server(): - return - - widget = AddJobWidget(self.window().server(), parent=self.window()) - widget.open() - - def set_filter(self, v): - """Set a search filter. - - Args: - v (str): The search filter. - - """ - self.selectionModel().blockSignals(True) - self.model().setFilterWildcard(v) - common.restore_selection(self) - self.selectionModel().blockSignals(False) - - def keyPressEvent(self, event): - """Key press event handler. - - """ - if event.key() == QtCore.Qt.Key_Escape: - self._interrupt_requested = True - - def contextMenuEvent(self, event): - """Context menu event handler. - - """ - item = self.indexAt(event.pos()) - menu = JobContextMenu(item, parent=self) - pos = event.pos() - pos = self.mapToGlobal(pos) - menu.move(pos) - menu.exec_() diff --git a/bookmarks/bookmarker/main.py b/bookmarks/bookmarker/main.py deleted file mode 100644 index c5ed2f2b..00000000 --- a/bookmarks/bookmarker/main.py +++ /dev/null @@ -1,396 +0,0 @@ -""":class:`BookmarkerWidget`, the main editor widget. - -The editor is made up of :class:`~bookmarks.bookmarker.server_editor.ServerItemEditor`, -:class:`~bookmarks.bookmarker.job_editor.JobItemEditor` and -:class:`~bookmarks.bookmarker.bookmark_editor.BookmarkItemEditor`, and defines -functionality needed saver and remove bookmark items to and from the user settings file. - -""" - -from PySide2 import QtCore, QtWidgets - -from . import bookmark_editor -from . import job_editor -from . import server_editor -from .. import actions -from .. import common -from .. import images -from .. import ui - -HINT = 'Activate or disable existing bookmark items, or create new ones using the ' \ - 'options below.' - - -def close(): - """Closes the :class:`BookmarkerWidget` editor. - - """ - if common.bookmarker_widget is None: - return - try: - common.bookmarker_widget.close() - common.bookmarker_widget.deleteLater() - except: - pass - common.bookmarker_widget = None - - -def show(): - """Shows the :class:`BookmarkerWidget` editor. - - """ - if not common.bookmarker_widget: - common.bookmarker_widget = BookmarkerWidget() - - common.restore_window_geometry(common.bookmarker_widget) - common.restore_window_state(common.bookmarker_widget) - - -class BookmarkerWidget(QtWidgets.QDialog): - """The main editor used to add or remove bookmark items, jobs and servers. - - """ - - def __init__(self, parent=None): - super().__init__( - parent=parent, - f=QtCore.Qt.CustomizeWindowHint | - QtCore.Qt.WindowTitleHint | - QtCore.Qt.WindowCloseButtonHint | - QtCore.Qt.WindowMaximizeButtonHint - ) - - self.server_editor = None - self.server_add_button = None - self.job_editor = None - self.job_add_button = None - self.bookmark_editor = None - self.bookmark_add_button = None - self.default_bookmarks_button = None - self.prune_bookmarks_button = None - - self.setObjectName('AddRemoveBookmarkItemsWidget') - self.setWindowTitle('Manage Bookmarks Items') - - self._create_ui() - self._connect_signals() - - def _create_ui(self): - """Create ui.""" - common.set_stylesheet(self) - QtWidgets.QVBoxLayout(self) - - o = common.size(common.size_indicator * 1.5) - self.layout().setContentsMargins(0, 0, 0, o) - self.layout().setSpacing(0) - - h = common.size(common.size_row_height) * 0.8 - - # ===================================================== - - _o = common.size(common.size_margin) - main_row = ui.add_row(None, height=None, parent=self, cls=QtWidgets.QSplitter) - main_row.setObjectName('mainRow') - main_row.setSizePolicy( - QtWidgets.QSizePolicy.MinimumExpanding, - QtWidgets.QSizePolicy.MinimumExpanding - ) - - main_row.layout().setSpacing(o) - main_row.layout().setContentsMargins(o, 0, o, 0) - - # ===================================================== - - pixmap = images.rsc_pixmap( - 'icon', - color=None, - size=common.size(common.size_row_height * 4), - opacity=0.8, - ) - label = QtWidgets.QLabel(parent=self) - label.setPixmap(pixmap) - main_row.layout().addWidget(label) - - # ===================================================== - - row = ui.add_row(None, vertical=True, height=None, parent=main_row) - row.layout().setSpacing(o * 0.5) - row.layout().setContentsMargins(0, o, 0, o) - - _grp = ui.get_group( - parent=row, margin=common.size( - common.size_indicator - ) * 1.5 - ) - _grp.setSizePolicy( - QtWidgets.QSizePolicy.MinimumExpanding, - QtWidgets.QSizePolicy.MinimumExpanding - ) - _row = ui.add_row(None, height=None, parent=_grp) - _row.setSizePolicy( - QtWidgets.QSizePolicy.MinimumExpanding, - QtWidgets.QSizePolicy.Maximum - ) - grp = ui.get_group( - parent=_grp, margin=common.size( - common.size_indicator - ) * 0.66 - ) - grp.setSizePolicy( - QtWidgets.QSizePolicy.MinimumExpanding, - QtWidgets.QSizePolicy.MinimumExpanding - ) - - label = ui.PaintedLabel( - 'Servers', - color=common.color(common.color_secondary_text) - ) - self.server_editor = server_editor.ServerItemEditor(parent=grp) - self.server_add_button = ui.ClickableIconButton( - 'add', - (common.color(common.color_green), - common.color(common.color_selected_text)), - h, - description='Click to add a new server', - state=True, - parent=self - ) - _row.layout().addWidget(label, 0) - _row.layout().addStretch(1) - _row.layout().addWidget(self.server_add_button) - grp.layout().addWidget(self.server_editor, 1) - - # ===================================================== - - row = ui.add_row(None, vertical=True, height=None, parent=main_row) - row.layout().setSpacing(o * 0.5) - row.layout().setContentsMargins(0, o, 0, o) - - _grp = ui.get_group( - parent=row, margin=common.size( - common.size_indicator - ) * 1.5 - ) - _grp.setSizePolicy( - QtWidgets.QSizePolicy.MinimumExpanding, - QtWidgets.QSizePolicy.MinimumExpanding - ) - _row = ui.add_row(None, height=None, parent=_grp) - _row.setSizePolicy( - QtWidgets.QSizePolicy.MinimumExpanding, - QtWidgets.QSizePolicy.Maximum - ) - - self.job_filter_widget = ui.LineEdit(parent=self) - self.job_filter_widget.setAlignment( - QtCore.Qt.AlignVCenter | QtCore.Qt.AlignRight - ) - self.job_filter_widget.setPlaceholderText('Search...') - _grp.layout().addWidget(self.job_filter_widget) - - grp = ui.get_group( - parent=_grp, margin=common.size( - common.size_indicator - ) * 0.66 - ) - grp.setSizePolicy( - QtWidgets.QSizePolicy.MinimumExpanding, - QtWidgets.QSizePolicy.MinimumExpanding - ) - - label = ui.PaintedLabel( - 'Jobs', - color=common.color(common.color_secondary_text) - ) - - self.job_editor = job_editor.JobItemEditor(parent=self) - self.job_add_button = ui.ClickableIconButton( - 'add', - (common.color(common.color_green), - common.color(common.color_selected_text)), - h, - description='Click to create a new job', - state=True, - parent=self - ) - - _row.layout().addWidget(label, 0) - _row.layout().addStretch(1) - _row.layout().addWidget(self.job_add_button) - - grp.layout().addWidget(self.job_editor, 1) - - # ===================================================== - - row = ui.add_row(None, vertical=True, height=None, parent=main_row) - row.layout().setSpacing(o * 0.5) - row.layout().setContentsMargins(0, o, 0, o) - - _grp = ui.get_group( - parent=row, margin=common.size( - common.size_indicator - ) * 1.5 - ) - _grp.setSizePolicy( - QtWidgets.QSizePolicy.MinimumExpanding, - QtWidgets.QSizePolicy.MinimumExpanding - ) - _row = ui.add_row(None, height=None, parent=_grp) - _row.layout().setAlignment(QtCore.Qt.AlignCenter) - _row.setSizePolicy( - QtWidgets.QSizePolicy.MinimumExpanding, - QtWidgets.QSizePolicy.Maximum - ) - grp = ui.get_group( - parent=_grp, margin=common.size( - common.size_indicator - ) * 0.66 - ) - grp.setSizePolicy( - QtWidgets.QSizePolicy.MinimumExpanding, - QtWidgets.QSizePolicy.MinimumExpanding - ) - - label = ui.PaintedLabel( - 'Root folders', - color=common.color(common.color_secondary_text) - ) - - self.bookmark_editor = bookmark_editor.BookmarkItemEditor(parent=self) - self.bookmark_add_button = ui.ClickableIconButton( - 'add', - (common.color(common.color_green), - common.color(common.color_selected_text)), - h, - description='Click to select a folder and use it as a bookmark item.', - state=True, - parent=self - ) - - _row.layout().addWidget(label, 0) - _row.layout().addStretch(1) - _row.layout().addWidget(self.bookmark_add_button) - grp.layout().addWidget(self.bookmark_editor, 1) - - self.layout().addSpacing(o * 2) - - row = ui.add_row(None, height=None, parent=self) - row.setSizePolicy( - QtWidgets.QSizePolicy.MinimumExpanding, - QtWidgets.QSizePolicy.Maximum, - ) - row.layout().setContentsMargins(o, 0, o, 0) - self.done_button = ui.PaintedButton( - 'Done', - parent=self - ) - - self.default_bookmarks_button = ui.PaintedButton( - 'Show default_bookmark_items.json' - ) - row.layout().addWidget(self.default_bookmarks_button, 0) - self.prune_bookmarks_button = ui.PaintedButton( - 'Remove invalid' - ) - row.layout().addWidget(self.prune_bookmarks_button, 0) - row.layout().addWidget(self.done_button, 1) - - def _connect_signals(self): - """Connect signals.""" - self.server_editor.selectionModel().selectionChanged.connect( - self.bookmark_editor.clear - ) - self.server_editor.selectionModel().selectionChanged.connect( - self.job_editor.init_data - ) - self.job_editor.selectionModel().selectionChanged.connect( - self.bookmark_editor.init_data - ) - - self.server_add_button.clicked.connect(self.server_editor.add) - self.job_add_button.clicked.connect(self.job_editor.add) - self.bookmark_add_button.clicked.connect(self.bookmark_editor.add) - - self.done_button.clicked.connect(self.close) - - self.default_bookmarks_button.clicked.connect( - actions.reveal_default_bookmarks_json - ) - self.prune_bookmarks_button.clicked.connect( - actions.prune_bookmarks - ) - - self.job_filter_widget.textChanged.connect(self.job_editor.set_filter) - - def server(self): - """Get the selected server. - - Returns: - str: The selected server. - - """ - index = common.get_selected_index(self.server_editor) - if not index.isValid(): - return None - return index.data(QtCore.Qt.DisplayRole) - - def job(self): - """Get the selected job. - - Returns: - str: The selected job. - - """ - index = common.get_selected_index(self.job_editor) - if not index.isValid(): - return None - return index.data(QtCore.Qt.UserRole + 1) - - def job_path(self): - """Get the selected job path. - - Returns: - str: The selected job path. - - """ - index = common.get_selected_index(self.job_editor) - if not index.isValid(): - return None - return index.data(QtCore.Qt.UserRole) - - def init_data(self): - """Initializes data. - - """ - self.server_editor.init_data() - - def changeEvent(self, event): - """Change event handler.""" - if event.type() == QtCore.QEvent.WindowStateChange: - common.save_window_state(self) - - def hideEvent(self, event): - """Hide event handler.""" - common.save_window_state(self) - super().hideEvent(event) - - def closeEvent(self, event): - """Close event handler.""" - common.save_window_state(self) - super().closeEvent(event) - - def showEvent(self, event): - """Show event handler. - - """ - QtCore.QTimer.singleShot(100, self.init_data) - super().showEvent(event) - - def sizeHint(self): - """Returns a size hint. - - """ - return QtCore.QSize( - common.size(common.size_width), - common.size(common.size_height) * 1.33 - ) diff --git a/bookmarks/bookmarker/server_editor.py b/bookmarks/bookmarker/server_editor.py deleted file mode 100644 index 8b58c185..00000000 --- a/bookmarks/bookmarker/server_editor.py +++ /dev/null @@ -1,410 +0,0 @@ -"""Editor widget used by -:class:`~bookmarks.bookmarker.main.BookmarkerWidget` -to select and save a new server item. - -""" -import functools - -from PySide2 import QtCore, QtGui, QtWidgets - -from .. import actions -from .. import common -from .. import contextmenu -from .. import images -from .. import shortcuts -from .. import ui - - -class AddServerEditor(QtWidgets.QDialog): - """Dialog used to add a new server to user settings file. - - """ - - def __init__(self, parent=None): - super().__init__(parent=parent) - self.ok_button = None - self.pick_button = None - self.editor = None - - self.setWindowTitle('Add New Server') - - self._create_ui() - self._connect_signals() - self._add_completer() - - def _create_ui(self): - """Create ui.""" - if not self.parent(): - common.set_stylesheet(self) - - QtWidgets.QVBoxLayout(self) - - o = common.size(common.size_margin) - self.layout().setSpacing(o) - self.layout().setContentsMargins(o, o, o, o) - - self.ok_button = ui.PaintedButton('Done', parent=self) - self.ok_button.setFixedHeight(common.size(common.size_row_height)) - self.pick_button = ui.PaintedButton('Pick', parent=self) - - self.editor = ui.LineEdit(parent=self) - self.editor.setPlaceholderText( - 'Enter the path to a server, e.g. \'//my_server/jobs\'' - ) - self.setFocusProxy(self.editor) - self.editor.setFocusPolicy(QtCore.Qt.StrongFocus) - - row = ui.add_row(None, parent=self) - row.layout().addWidget(self.editor, 1) - row.layout().addWidget(self.pick_button, 0) - - row = ui.add_row(None, parent=self) - row.layout().addWidget(self.ok_button, 1) - - def _add_completer(self): - """Add and populate a QCompleter with mounted drive names. - - """ - items = [] - for info in QtCore.QStorageInfo.mountedVolumes(): - if info.isValid(): - items.append(info.rootPath()) - items += common.servers.values() - - completer = QtWidgets.QCompleter(items, parent=self) - completer.setCaseSensitivity(QtCore.Qt.CaseInsensitive) - common.set_stylesheet(completer.popup()) - self.editor.setCompleter(completer) - - def _connect_signals(self): - """Connect signals.""" - self.ok_button.clicked.connect( - lambda: self.done(QtWidgets.QDialog.Accepted) - ) - self.pick_button.clicked.connect(self.pick) - self.editor.textChanged.connect( - lambda: self.editor.setStyleSheet( - f'color: {common.rgb(common.color_green)};' - ) - ) - - @QtCore.Slot() - def pick(self): - """Get an existing directory to use as a server. - - """ - _dir = QtWidgets.QFileDialog.getExistingDirectory(parent=self) - if not _dir: - return - - file_info = QtCore.QFileInfo(_dir) - if file_info.exists(): - self.editor.setText(file_info.absoluteFilePath()) - - @common.error - @common.debug - def done(self, result): - """Finalize action. - - """ - if result == QtWidgets.QDialog.Rejected: - super().done(result) - return - - if not self.text(): - return - - v = self.text() - file_info = QtCore.QFileInfo(v) - - if not file_info.exists() or not file_info.isReadable() or v in \ - common.servers: - # Indicate the selected item is invalid and keep the editor open - self.editor.setStyleSheet( - 'color: {0}; border-color: {0}'.format( - common.rgb(common.color_red) - ) - ) - self.editor.blockSignals(True) - self.editor.setText(v) - self.editor.blockSignals(False) - return - - actions.add_server(v) - super().done(QtWidgets.QDialog.Accepted) - - def text(self): - """Sanitize text. - - Returns: - str: The sanitized text. - - """ - v = self.editor.text() - return common.strip(v) if v else '' - - def showEvent(self, event): - """Show event handler. - - """ - self.editor.setFocus() - common.center_window(self) - - def sizeHint(self): - """Returns a size hint. - - """ - return QtCore.QSize( - common.size(common.size_width), - common.size(common.size_row_height) * 2 - ) - - -class ServerContextMenu(contextmenu.BaseContextMenu): - """Context menu associated with :class:`ServerItemEditor`. - - """ - - def setup(self): - """Creates the context menu. - - """ - self.add_menu() - self.separator() - if isinstance( - self.index, QtWidgets.QListWidgetItem - ) and self.index.flags() & QtCore.Qt.ItemIsEnabled: - self.reveal_menu() - self.remove_menu() - elif isinstance( - self.index, - QtWidgets.QListWidgetItem - ) and not self.index.flags() & QtCore.Qt.ItemIsEnabled: - self.remove_menu() - self.separator() - self.refresh_menu() - - def add_menu(self): - """Add server action. - - """ - self.menu['Add New Server...'] = { - 'action': self.parent().add, - 'icon': ui.get_icon('add', color=common.color(common.color_green)) - } - - def reveal_menu(self): - """Reveal server item action. - - """ - self.menu['Reveal...'] = { - 'action': lambda: actions.reveal(f'{self.index.text()}/.'), - 'icon': ui.get_icon('folder'), - } - - def remove_menu(self): - """Remove server item action. - - """ - self.menu['Remove'] = { - 'action': self.parent().remove, - 'icon': ui.get_icon('close', color=common.color(common.color_red)) - } - - def refresh_menu(self): - """Refresh server list action. - - """ - self.menu['Refresh'] = { - 'action': self.parent().init_data, - 'icon': ui.get_icon('refresh') - } - - -class ServerItemEditor(ui.ListWidget): - """List widget used to add and remove servers to and from the local - user settings. - - """ - - def __init__(self, parent=None): - super().__init__( - default_icon='server', - parent=parent - ) - - self.setItemDelegate(ui.ListWidgetDelegate(parent=self)) - self.setWindowTitle('Server Editor') - self.setObjectName('ServerEditor') - - self.setAttribute(QtCore.Qt.WA_NoSystemBackground) - self.setAttribute(QtCore.Qt.WA_TranslucentBackground) - self.viewport().setAttribute(QtCore.Qt.WA_NoSystemBackground) - self.viewport().setAttribute(QtCore.Qt.WA_TranslucentBackground) - - self.setSizePolicy( - QtWidgets.QSizePolicy.MinimumExpanding, - QtWidgets.QSizePolicy.MinimumExpanding - ) - self.setMinimumWidth(common.size(common.size_width) * 0.2) - - self._connect_signals() - self._init_shortcuts() - - def _init_shortcuts(self): - """Initializes shortcuts. - - """ - shortcuts.add_shortcuts(self, shortcuts.BookmarkEditorShortcuts) - connect = functools.partial( - shortcuts.connect, shortcuts.BookmarkEditorShortcuts - ) - connect(shortcuts.AddItem, self.add) - connect(shortcuts.RemoveItem, self.remove) - - def _connect_signals(self): - """Connects signals. - - """ - super()._connect_signals() - - self.selectionModel().selectionChanged.connect( - functools.partial(common.save_selection, self) - ) - - common.signals.serversChanged.connect(self.init_data) - common.signals.serverAdded.connect( - functools.partial(common.select_index, self) - ) - - @common.debug - @common.error - @QtCore.Slot() - def remove(self, *args, **kwargs): - """Remove a server item. - - """ - index = common.get_selected_index(self) - if not index.isValid(): - return - - v = index.data(QtCore.Qt.DisplayRole) - v = common.strip(v) - actions.remove_server(v) - - @common.debug - @common.error - @QtCore.Slot() - def add(self, *args, **kwargs): - """Add a server item. - - """ - w = AddServerEditor(parent=self.window()) - pos = self.mapToGlobal(self.window().rect().topLeft()) - w.move(pos) - if w.exec_() == QtWidgets.QDialog.Accepted: - self.init_data() - - def contextMenuEvent(self, event): - """Context menu event handler. - - """ - item = self.itemAt(event.pos()) - menu = ServerContextMenu(item, parent=self) - pos = event.pos() - pos = self.mapToGlobal(pos) - menu.move(pos) - menu.exec_() - - @common.debug - @common.error - @QtCore.Slot() - def init_data(self, *args, **kwargs): - """Load data. - - """ - selected_index = common.get_selected_index(self) - selected_name = selected_index.data( - QtCore.Qt.DisplayRole - ) if selected_index.isValid() else None - - self.selectionModel().blockSignals(True) - self.clear() - - for path in common.servers: - item = QtWidgets.QListWidgetItem() - item.setData(QtCore.Qt.DisplayRole, path) - item.setData(QtCore.Qt.UserRole, path) - item.setData(QtCore.Qt.UserRole + 1, path) - item.setData(QtCore.Qt.StatusTipRole, path) - item.setData(QtCore.Qt.WhatsThisRole, path) - item.setData(QtCore.Qt.ToolTipRole, path) - - size = QtCore.QSize( - 0, - common.size(common.size_margin) * 2 - ) - item.setSizeHint(size) - self.validate_item(item) - self.insertItem(self.count(), item) - - self.progressUpdate.emit('') - - if selected_name: - for idx in range(self.model().rowCount()): - index = self.model().index(idx, 0) - if index.data(QtCore.Qt.DisplayRole) == selected_name: - self.selectionModel().select( - index, QtCore.QItemSelectionModel.ClearAndSelect - ) - - self.selectionModel().blockSignals(False) - common.restore_selection(self) - - @QtCore.Slot(QtWidgets.QListWidgetItem) - def validate_item(self, item): - """Check if the given server item is valid. - - """ - selected_index = common.get_selected_index(self) - - self.blockSignals(True) - - pixmap = images.rsc_pixmap( - 'server', common.color(common.color_text), - common.size(common.size_row_height) * 0.8 - ) - pixmap_selected = images.rsc_pixmap( - 'server', common.color(common.color_selected_text), - common.size(common.size_row_height) * 0.8 - ) - pixmap_disabled = images.rsc_pixmap( - 'close', common.color(common.color_red), - common.size(common.size_row_height) * 0.8 - ) - icon = QtGui.QIcon() - - file_info = QtCore.QFileInfo(item.text()) - if file_info.exists() and file_info.isReadable() and file_info.isWritable(): - icon.addPixmap(pixmap, QtGui.QIcon.Normal) - icon.addPixmap(pixmap_selected, QtGui.QIcon.Selected) - icon.addPixmap(pixmap_selected, QtGui.QIcon.Active) - icon.addPixmap(pixmap_disabled, QtGui.QIcon.Disabled) - item.setFlags( - QtCore.Qt.ItemIsEnabled | - QtCore.Qt.ItemIsSelectable - ) - valid = True - else: - icon.addPixmap(pixmap_disabled, QtGui.QIcon.Normal) - icon.addPixmap(pixmap_disabled, QtGui.QIcon.Selected) - icon.addPixmap(pixmap_disabled, QtGui.QIcon.Active) - icon.addPixmap(pixmap_disabled, QtGui.QIcon.Disabled) - valid = False - - item.setData(QtCore.Qt.DecorationRole, icon) - self.blockSignals(False) - - index = self.indexFromItem(item) - if not valid and selected_index == index: - self.selectionModel().clearSelection() diff --git a/bookmarks/common/__init__.py b/bookmarks/common/__init__.py index f208879a..e2eb0423 100644 --- a/bookmarks/common/__init__.py +++ b/bookmarks/common/__init__.py @@ -99,7 +99,7 @@ launcher_widget = None message_widget = None preference_editor_widget = None -bookmarker_widget = None +job_editor = None bookmark_property_editor = None asset_property_editor = None file_saver_widget = None diff --git a/bookmarks/common/settings.py b/bookmarks/common/settings.py index 7b193a0a..df7d592e 100644 --- a/bookmarks/common/settings.py +++ b/bookmarks/common/settings.py @@ -30,7 +30,6 @@ 'user/favourites', ), 'settings': ( - 'settings/job_scan_depth', 'settings/ui_scale', 'settings/show_menu_icons', 'settings/paint_thumbnail_bg', @@ -43,6 +42,9 @@ 'settings/bin_rvpush', 'settings/bin_oiiotool', ), + 'jobs': ( + 'jobs/scandepth', + ), 'filters': ( 'filters/active', 'filters/archived', @@ -83,11 +85,6 @@ 'file_saver/template', 'file_saver/user', ), - 'bookmarker': ( - 'bookmarker/server', - 'bookmarker/job', - 'bookmarker/root', - ), 'ffmpeg': ( 'ffmpeg/preset', 'ffmpeg/size', diff --git a/bookmarks/common/ui.py b/bookmarks/common/ui.py index dfab7cd3..2247a65f 100644 --- a/bookmarks/common/ui.py +++ b/bookmarks/common/ui.py @@ -366,7 +366,7 @@ def show_message(title, body='', disable_animation=False, icon='icon', message_t body (str): The body of the message box. disable_animation (bool): Whether to show the message box without animation. icon (str): The icon to use. - message_type (str): The message type. + message_type (str): The message type. One of 'info', 'success' or 'error'. buttons (list): The buttons to show. modal (bool): Whether the message box should be modal. parent (QWidget): The parent widget. @@ -500,6 +500,11 @@ def select_index(widget, v, *args, role=QtCore.Qt.DisplayRole, **kwargs): QtCore.QItemSelectionModel.ClearAndSelect | QtCore.QItemSelectionModel.Rows ) + widget.selectionModel().setCurrentIndex( + index, + QtCore.QItemSelectionModel.ClearAndSelect | + QtCore.QItemSelectionModel.Rows + ) widget.scrollTo( index, QtWidgets.QAbstractItemView.PositionAtCenter diff --git a/bookmarks/contextmenu.py b/bookmarks/contextmenu.py index 34fc39f5..5d482ec6 100644 --- a/bookmarks/contextmenu.py +++ b/bookmarks/contextmenu.py @@ -950,7 +950,7 @@ def bookmark_editor_menu(self): self.menu[key()] = { 'text': 'Manage Bookmark Items...', 'icon': icon, - 'action': actions.show_bookmarker, + 'action': actions.show_job_editor, 'shortcut': shortcuts.get( shortcuts.MainWidgetShortcuts, shortcuts.AddItem diff --git a/bookmarks/editor/base.py b/bookmarks/editor/base.py index fafb9798..77a0d868 100644 --- a/bookmarks/editor/base.py +++ b/bookmarks/editor/base.py @@ -1,14 +1,19 @@ -"""Contains :class:`.BasePropertyEditor` and its required attributes and methods. +"""Contains :class:`.BasePropertyEditor` the property editor base class used +across the application. -The property editor's layout is defined by a previously specified SECTIONS +The editor is designed to interact with the bookmark item database and user +settings file but can be used without either as a general dialog for user actions. + +The editor's layout is defined by SECTIONS dictionary. This contains the sections, rows and editor widget definitions - plus linkage information needed to associate the widget with a bookmark database columns or user setting keys. :class:`BasePropertyEditor` is relatively flexible and has a number of abstract methods that need implementing in subclasses depending on the desired -functionality. See, :meth:`.BasePropertyEditor.db_source`, -:meth:`.BasePropertyEditor.init_data` and :meth:`.BasePropertyEditor.save_changes`. +functionality. In all subclasses, the :meth:`.BasePropertyEditor.db_source`, +:meth:`.BasePropertyEditor.init_data` and :meth:`.BasePropertyEditor.save_changes` +methods must be implemented. """ import datetime @@ -95,7 +100,7 @@ def add_section(icon, label, parent, color=None): class BasePropertyEditor(QtWidgets.QDialog): - """Base class for constructing a property editor widget. + """Base class for constructing a property editor widgets. Args: server (str or None): `server` path segment. @@ -103,8 +108,8 @@ class BasePropertyEditor(QtWidgets.QDialog): root (str or None): `root` path segment. asset (str or None): `asset` path segment. db_table (str or None): - An optional name of a bookmark database table. When not `None`, the editor - will load and save data to the database. Defaults to `None`. + An optional name of a bookmark database table. When set, the editor + will load and save data to and from the bookmark item database. Defaults to `None`. buttons (tuple): Button labels. Defaults to `('Save', 'Cancel')`. alignment (int): Text alignment. Defaults to `QtCore.Qt.AlignRight`. fallback_thumb (str): An image name. Defaults to `'placeholder'`. @@ -129,8 +134,9 @@ def __init__( alignment=QtCore.Qt.AlignRight, fallback_thumb='placeholder', hide_thumbnail_editor=False, + section_buttons=True, + frameless=False, parent=None, - section_buttons=True ): super().__init__( parent=parent, @@ -143,13 +149,30 @@ def __init__( ) ) + common.check_type(server, (str, None)) + common.check_type(job, (str, None)) + common.check_type(root, (str, None)) + common.check_type(asset, (str, None)) + common.check_type(db_table, (str, None)) + common.check_type(buttons, (tuple, list)) + common.check_type(alignment, int) + common.check_type(fallback_thumb, str) + common.check_type(hide_thumbnail_editor, bool) + common.check_type(section_buttons, bool) + common.check_type(frameless, bool) + common.check_type(parent, (QtWidgets.QWidget, None)) + + if len(buttons) > 2: + raise ValueError('`buttons` must be a tuple of 1 or 2 items.') + self._fallback_thumb = fallback_thumb self._alignment = alignment - self._section_widgets = [] self._buttons = buttons self._db_table = db_table + self._frameless = frameless self.section_buttons = section_buttons + self._section_widgets = [] self.server = server self.job = job @@ -181,6 +204,23 @@ def __init__( else: self.setWindowTitle(f'{server}/{job}/{root}/{asset}') + if self._frameless: + self.setAttribute(QtCore.Qt.WA_TranslucentBackground) + self.setAttribute(QtCore.Qt.WA_NoSystemBackground) + + self.setWindowFlags( + QtCore.Qt.Dialog | + QtCore.Qt.FramelessWindowHint + ) + + # Shadow effect + self.effect = QtWidgets.QGraphicsDropShadowEffect(self) + self.effect.setBlurRadius(common.size(common.size_margin) * 2) + self.effect.setXOffset(0) + self.effect.setYOffset(0) + self.effect.setColor(QtGui.QColor(0, 0, 0, 200)) + self.setGraphicsEffect(self.effect) + self._create_ui() self._connect_signals() @@ -227,7 +267,7 @@ def _create_ui(self): QtWidgets.QVBoxLayout(self.section_headers_widget) self.section_headers_widget.layout().setContentsMargins(0, 0, 0, 0) self.section_headers_widget.layout().setSpacing( - common.size(common.size_indicator) + common.size(common.size_indicator) * 2 ) parent = QtWidgets.QWidget(parent=self) @@ -266,13 +306,96 @@ def _create_ui(self): parent.layout().addStretch(1) self._add_buttons() + def _connect_signals(self): + self.cancel_button.clicked.connect( + lambda: self.done(QtWidgets.QDialog.Rejected) + ) + self.save_button.clicked.connect( + lambda: self.done(QtWidgets.QDialog.Accepted) + ) + + common.signals.databaseValueUpdated.connect(self.update_changed_database_value) + + def _connect_data_changed_signals(self, key, _type, editor): + """Utility method for connecting an editor's change signal to `data_changed`. + + `data_changed` will save the changed current value internally. This data + later can be used, for instance, to save the changed values to the + database. + + """ + if hasattr(editor, 'dataUpdated'): + editor.dataUpdated.connect( + functools.partial( + self.data_changed, + key, + _type, + editor + ) + ) + elif hasattr(editor, 'textChanged'): + editor.textChanged.connect( + functools.partial( + self.data_changed, + key, + _type, + editor + ) + ) + elif hasattr(editor, 'currentTextChanged'): + editor.currentTextChanged.connect( + functools.partial( + self.data_changed, + key, + _type, + editor + ) + ) + elif hasattr(editor, 'stateChanged'): + editor.stateChanged.connect( + functools.partial( + self.data_changed, + key, + _type, + editor + ) + ) + + def _connect_settings_save_signals(self, keys): + """Utility method for connecting editor signals to save their current + value in the user setting file. + + Args: + keys (tuple): A tuple of user setting keys. + + """ + for k in keys: + _k = k.replace('/', '_') + if not hasattr(self, f'{_k}_editor'): + print(f'No editor found for {k}') + continue + + editor = getattr(self, f'{_k}_editor') + + if hasattr(editor, 'currentTextChanged'): + signal = getattr(editor, 'currentTextChanged') + elif hasattr(editor, 'textChanged'): + signal = getattr(editor, 'textChanged') + elif hasattr(editor, 'stateChanged'): + signal = getattr(editor, 'stateChanged') + else: + continue + + func = functools.partial(common.settings.setValue, k) + signal.connect(func) + def _create_sections(self): """Translates the section data into a UI layout. """ parent = self.scroll_area.widget() for section in self.sections.values(): - grp = add_section( + section_widget = add_section( section['icon'], section['name'], parent, @@ -280,18 +403,27 @@ def _create_sections(self): ) if self.section_buttons: - self.add_section_header_button(section['name'], grp) + self.add_section_header_button(section['name'], section_widget) for item in section['groups'].values(): - _grp = ui.get_group(parent=grp) + group_widget = ui.get_group(parent=section_widget) for v in item.values(): - self._add_row(v, grp, _grp) + self._add_row(v, group_widget) + + def _add_row(self, v, group_widget): + """Utility method for translating a row item in the section data to a UI layout. + + Args: + v (dict): The row data. + group_widget.parent() (QWidget): The parent group widget. + group_widget (QWidget): The child group widget. - def _add_row(self, v, grp, _grp): - row = ui.add_row(v['name'], parent=_grp, height=None) + """ + row = ui.add_row(v['name'], parent=group_widget, height=None) k = v['key'] _k = k.replace('/', '_') if k else k + name = v['name'] _name = name.lower() if name else name _name = re.sub(r'\W+', '_', _name) if _name else _name @@ -303,12 +435,13 @@ def _add_row(self, v, grp, _grp): if 'widget' in v and v['widget']: if 'no_group' in v and v['no_group']: - editor = v['widget'](parent=grp) - grp.layout().insertWidget(1, editor, 1) + editor = v['widget'](parent=group_widget.parent()) + group_widget.parent().layout().insertWidget(1, editor, 1) else: editor = v['widget'](parent=row) if isinstance(editor, QtWidgets.QCheckBox): # We don't want checkboxes to fully extend across a row + editor.setSizePolicy( QtWidgets.QSizePolicy.Maximum, QtWidgets.QSizePolicy.Maximum, @@ -370,7 +503,7 @@ def _add_row(self, v, grp, _grp): if 'help' in v and v['help']: ui.add_description( - v['help'], label=None, parent=_grp + v['help'], label=None, parent=group_widget ) if 'button' in v and v['button']: @@ -405,34 +538,6 @@ def _add_row(self, v, grp, _grp): ) row.layout().addWidget(button2, 0) - def add_section_header_button(self, name, widget): - """Add a header button to help reveal the given section widget. - - """ - if not name: - return - button = QtWidgets.QPushButton( - name, - parent=self.section_headers_widget - ) - button.setFocusPolicy(QtCore.Qt.NoFocus) - - font, _ = common.font_db.bold_font(common.size(common.size_font_small)) - button.setStyleSheet( - 'outline: none;' - 'border: none;' - f'color: {common.rgb(common.color_light_background)};' - 'text-align: left;' - 'padding: 0px;' - 'margin: 0px;' - f'font-size: {common.size(common.size_font_small)}px;' - f'font-family: "{font.family()}"' - ) - self.section_headers_widget.layout().addWidget(button) - button.clicked.connect( - functools.partial(self.scroll_to_section, widget) - ) - def _add_buttons(self): if not self._buttons: return @@ -441,9 +546,15 @@ def _add_buttons(self): self.save_button = ui.PaintedButton( self._buttons[0], parent=self ) - self.cancel_button = ui.PaintedButton( - self._buttons[1], parent=self - ) + if len(self._buttons) > 1: + self.cancel_button = ui.PaintedButton( + self._buttons[1], parent=self + ) + else: + self.cancel_button = ui.PaintedButton( + 'Close', parent=self + ) + self.cancel_button.setHidden(True) row = ui.add_row( None, height=h * 2, parent=self.right_row @@ -454,6 +565,19 @@ def _add_buttons(self): row.layout().addWidget(self.cancel_button, 0) row.layout().addSpacing(common.size(common.size_margin)) + def add_section_header_button(self, name, widget): + """Add a header button to help reveal the given section widget. + + """ + if not name: + return + button = ui.PaintedLabel(name, parent=self.section_headers_widget) + + self.section_headers_widget.layout().addWidget(button) + button.clicked.connect( + functools.partial(self.scroll_to_section, widget) + ) + @QtCore.Slot(QtWidgets.QWidget) def scroll_to_section(self, widget): """Slot used to scroll to a section when a section header is clicked. @@ -464,89 +588,6 @@ def scroll_to_section(self, widget): point.y() + self.scroll_area.verticalScrollBar().value() ) - def _connect_data_changed_signals(self, key, _type, editor): - """Utility method for connecting an editor's change signal to `data_changed`. - - `data_changed` will save the changed current value internally. This data - later can be used, for instance, to save the changed values to the - database. - - """ - if hasattr(editor, 'dataUpdated'): - editor.dataUpdated.connect( - functools.partial( - self.data_changed, - key, - _type, - editor - ) - ) - elif hasattr(editor, 'textChanged'): - editor.textChanged.connect( - functools.partial( - self.data_changed, - key, - _type, - editor - ) - ) - elif hasattr(editor, 'currentTextChanged'): - editor.currentTextChanged.connect( - functools.partial( - self.data_changed, - key, - _type, - editor - ) - ) - elif hasattr(editor, 'stateChanged'): - editor.stateChanged.connect( - functools.partial( - self.data_changed, - key, - _type, - editor - ) - ) - - def _connect_signals(self): - self.cancel_button.clicked.connect( - lambda: self.done(QtWidgets.QDialog.Rejected) - ) - self.save_button.clicked.connect( - lambda: self.done(QtWidgets.QDialog.Accepted) - ) - - common.signals.databaseValueUpdated.connect(self.update_changed_database_value) - - def _connect_settings_save_signals(self, keys): - """Utility method for connecting editor signals to save their current - value in the user setting file. - - Args: - keys (tuple): A tuple of user setting keys. - - """ - for k in keys: - _k = k.replace('/', '_') - if not hasattr(self, f'{_k}_editor'): - print(f'No editor found for {k}') - continue - - editor = getattr(self, f'{_k}_editor') - - if hasattr(editor, 'currentTextChanged'): - signal = getattr(editor, 'currentTextChanged') - elif hasattr(editor, 'textChanged'): - signal = getattr(editor, 'textChanged') - elif hasattr(editor, 'stateChanged'): - signal = getattr(editor, 'stateChanged') - else: - continue - - func = functools.partial(common.settings.setValue, k) - signal.connect(func) - def load_saved_user_settings(self, keys): """Utility method will load user setting values and apply them to the corresponding editors. @@ -759,29 +800,6 @@ def data_changed(self, key, _type, editor, v): f'color: {common.rgb(common.color_text)};' ) - def db_source(self): - """A file path to use as the source of database values. - - Returns: - str: The database source file. - - """ - raise NotImplementedError('Abstract method must be implemented by subclass.') - - @QtCore.Slot() - def init_data(self): - """Initializes data. - - """ - raise NotImplementedError('Abstract method must be implemented by subclass.') - - @QtCore.Slot() - def save_changes(self): - """Perform save actions and/or data saving. - - """ - raise NotImplementedError('Abstract method must be implemented by subclass.') - @QtCore.Slot() def done(self, result): """Finish editing the item. @@ -803,6 +821,18 @@ def done(self, result): return super().done(result) + def paintEvent(self, event): + if self._frameless: + painter = QtGui.QPainter(self) + painter.setPen(QtCore.Qt.NoPen) + painter.setBrush(common.color(common.color_background)) + + o = common.size(common.size_margin) * 2 + painter.drawRect(self.rect().adjusted(o, o, -o, -o)) + painter.end() + + super().paintEvent(event) + def changeEvent(self, event): """Change event handler. @@ -842,6 +872,7 @@ def sizeHint(self): common.size(common.size_height) * 1.5 ) + @QtCore.Slot(str) @QtCore.Slot(str) @QtCore.Slot(str) @QtCore.Slot(object) @@ -855,6 +886,12 @@ def update_changed_database_value(self, table, source, key, value): value (object): The new database value. """ + try: + self.db_source() + except NotImplementedError: + print('Skipping database updates, no db source set.') + return + if source != self.db_source(): return @@ -897,3 +934,33 @@ def url2_button_clicked(self): if not v: return QtGui.QDesktopServices.openUrl(v) + + def db_source(self): + """A file path to use as the source of database values. + + When returns `None`, the editor will not load or save data to the + database. + + Returns: + str: A file or folder path. + + """ + raise NotImplementedError('Abstract method must be implemented by subclass.') + + @QtCore.Slot() + def init_data(self): + """Implement this method to initialize the editor's data. + + """ + raise NotImplementedError('Abstract method must be implemented by subclass.') + + @QtCore.Slot() + def save_changes(self): + """Implement to execute any actions and/or save data before closing the + editor. + + For editors that interact with the user database, this method should call + :meth:`save_changed_data_to_db` to save the changed data to the database. + + """ + raise NotImplementedError('Abstract method must be implemented by subclass.') diff --git a/bookmarks/editor/jobs.py b/bookmarks/editor/jobs.py new file mode 100644 index 00000000..8ef2b2ed --- /dev/null +++ b/bookmarks/editor/jobs.py @@ -0,0 +1,261 @@ +"""Widgets to create jobs and edit bookmark items. + +Job is a pseudo-entity in the Bookmarks as it is simply a folder on a server that contains +a bookmarks items. This module allows adding a server path to the user settings, +create new jobs using templates and pick folders from within the job to use as +bookmark items. + +To show the main editor call: + +.. code-block:: python + :linenos: + + from bookmarks import actions + bookmarks.actions.show_job_editor() + + +""" + +from PySide2 import QtCore + +from . import base +from . import jobs_widgets +from .. import common + + +def close(): + """Closes the :class:`JobsEditor` editor. + + """ + if common.job_editor is None: + return + try: + common.job_editor.close() + common.job_editor.deleteLater() + except: + pass + common.jobs_editor = None + + +def show(): + """Shows the :class:`JobsEditor` editor. + + """ + close() + common.job_editor = JobsEditor() + + common.restore_window_geometry(common.job_editor) + common.restore_window_state(common.job_editor) + + return common.bookmark_property_editor + + +class JobsEditor(base.BasePropertyEditor): + """The :class:`JobsEditor` class provides a widget for creating jobs and + the bookmark items within the job. + + """ + #: UI Layout definition + + sections = { + 0: { + 'name': 'Edit Jobs', + 'icon': 'icon_bw', + 'color': common.color(common.color_green), + 'groups': { + 0: { + 0: { + 'name': None, + 'key': 'server_btn', + 'validator': None, + 'widget': None, + 'placeholder': None, + 'description': 'Edit the list of servers Bookmarks should read jobs from.', + # 'help': f'Edit the list of servers Bookmarks should read jobs from. The server should be ' + # f'a directory where jobs are kept, e.g. ' + # 'Z:/ or' + # '//server/jobs.'.format(c=common.rgb( + # common.color_green) + # ), + 'button': 'Add server...' + }, + 1: { + 'name': None, + 'key': 'server', + 'validator': None, + 'widget': jobs_widgets.ServersWidget, + 'placeholder': None, + 'description': 'List of servers added to the user preferences.', + }, + }, + 1: { + 0: { + 'name': None, + 'key': 'job_btn', + 'validator': None, + 'widget': None, + 'placeholder': None, + 'description': 'Add jobs and bookmark items', + # 'help': 'Add jobs to the current server. A job is a folder on a server that contains ' + # 'bookmark items. To mark a folder as a bookmark item, right-click on a job and ' + # 'select "Add bookmark item...".', + 'button': 'Add job...' + }, + 1: { + 'name': None, + 'key': 'job', + 'validator': None, + 'widget': jobs_widgets.JobsView, + 'placeholder': None, + 'description': 'The list of jobs in the current server', + }, + }, + 2: { + 0: { + 'name': 'Bookmark item search depth', + 'key': 'jobs/scandepth', + 'validator': None, + 'widget': jobs_widgets.ScanDepthComboBox, + 'placeholder': None, + 'description': 'Set the maximum folder depth to parse. Parsing large ' + 'project folders will take a long time. This setting ' + 'will limit the number of sub-directories the editor ' + 'parses when looking for bookmark items.', + }, + } + } + } + } + + def __init__(self, parent=None): + super().__init__( + None, + None, + None, + buttons=('Close',), + fallback_thumb='icon_bw', + hide_thumbnail_editor=True, + section_buttons=False, + parent=parent + ) + + self.setAttribute(QtCore.Qt.WA_DeleteOnClose) + self.setWindowTitle('Edit Jobs') + + def init_data(self): + self.load_saved_user_settings(common.SECTIONS['jobs']) + self._connect_settings_save_signals(common.SECTIONS['jobs']) + + self.server_editor.selectionModel().selectionChanged.connect(self.server_selection_changed) + self.server_editor.model().modelAboutToBeReset.connect(self.server_selection_changed) + self.server_editor.model().modelReset.connect(self.server_selection_changed) + + self.server_editor.progressUpdate.connect(self.show_message) + self.job_editor.progressUpdate.connect(self.show_message) + + self.server_editor.init_data() + + @QtCore.Slot(str) + @QtCore.Slot(str) + def show_message(self, title, body): + """Shows a progress message as the models are loading. + + """ + if not title: + common.close_message() + return + + try: + common.message_widget.set_labels(title, body) + return + except: + pass + + common.show_message( + title, + body=body, + disable_animation=True, + message_type=None, + buttons=[], + ) + + def save_changes(self): + return True + + def get_args(self): + """Returns the server, job and root arguments based on the current + bookmark item selection. + + Returns: + tuple: The server, job and root arguments. + + """ + editor = self.job_editor + model = editor.model() + server = model.root_node.name + + if not server or server == 'server': + return None, None, None + + if not editor.selectionModel().hasSelection(): + return None, None, None + + index = next(f for f in editor.selectionModel().selectedIndexes()) + node = index.internalPointer() + + if not node: + return None, None, None + + if not isinstance(node, jobs_widgets.BookmarkItemNode): + return None, None, None + + job = node.parent.name[len(server) + 1:] + root = node.name[len(server) + len(job) + 2:] + return server, job, root + + @QtCore.Slot() + def server_selection_changed(self, *args, **kwargs): + """Slot -> called when the server editor's selection changes. + + """ + model = self.server_editor.selectionModel() + + if not model.hasSelection(): + return self.job_editor.model().init_data('server') + + index = next(f for f in model.selectedIndexes()) + + if not index.isValid(): + return self.job_editor.model().init_data('server') + + v = index.data(QtCore.Qt.UserRole) + if not v: + return self.job_editor.model().init_data('server') + + self.job_editor.model().init_data(v) + + @QtCore.Slot() + def server_btn_button_clicked(self): + """Slot -> Called when the server editor's 'Add' button is clicked. + + """ + self.server_editor.add() + + @QtCore.Slot() + def job_btn_button_clicked(self): + """Slot -> Called when the server editor's 'Add' button is clicked. + + """ + self.job_editor.add() + + def hideEvent(self, event): + self._disconnect_signals() + + def _disconnect_signals(self): + self.server_editor.selectionModel().selectionChanged.disconnect() + self.server_editor.model().modelAboutToBeReset.disconnect() + self.server_editor.model().modelReset.disconnect() + + self.job_editor.selectionModel().selectionChanged.disconnect() + self.job_editor.model().modelAboutToBeReset.disconnect() + self.job_editor.model().modelReset.disconnect() diff --git a/bookmarks/editor/jobs_widgets.py b/bookmarks/editor/jobs_widgets.py new file mode 100644 index 00000000..558a48c7 --- /dev/null +++ b/bookmarks/editor/jobs_widgets.py @@ -0,0 +1,1497 @@ +"""The widgets needed by the jobs editor. + +""" +import functools +import json +import os + +from PySide2 import QtCore, QtGui, QtWidgets + +from . import base +from .. import actions +from .. import common +from .. import contextmenu +from .. import images +from .. import shortcuts +from .. import templates +from .. import ui + +cache = common.DataDict() + + +class ScanDepthComboBox(QtWidgets.QComboBox): + + def __init__(self, parent=None): + super().__init__(parent=parent) + self.setView(QtWidgets.QListView()) + self.init_data() + + def init_data(self): + """Initializes data. + + """ + self.blockSignals(True) + for k in range(1, 5): + self.addItem(str(f'{k}'), userData=k) + self.blockSignals(False) + + +class AddServerDialog(QtWidgets.QDialog): + """Dialog used to add a new server to user settings file. + + """ + + def __init__(self, parent=None): + super().__init__(parent=parent) + + self.ok_button = None + self.pick_button = None + self.editor = None + + self.setWindowTitle('Add new server') + + self.setAttribute(QtCore.Qt.WA_TranslucentBackground) + self.setAttribute(QtCore.Qt.WA_NoSystemBackground) + + self.setWindowFlags( + QtCore.Qt.Dialog | + QtCore.Qt.FramelessWindowHint + ) + + # Shadow effect + self.effect = QtWidgets.QGraphicsDropShadowEffect(self) + self.effect.setBlurRadius(common.size(common.size_margin) * 2) + self.effect.setXOffset(0) + self.effect.setYOffset(0) + self.effect.setColor(QtGui.QColor(0, 0, 0, 200)) + self.setGraphicsEffect(self.effect) + + self._create_ui() + self._connect_signals() + self._add_completer() + + def _create_ui(self): + if not self.parent(): + common.set_stylesheet(self) + + QtWidgets.QVBoxLayout(self) + + o = common.size(common.size_margin) + _o = common.size(common.size_margin) * 3 + self.layout().setSpacing(o) + self.layout().setContentsMargins(_o, _o, _o, _o) + + self.ok_button = ui.PaintedButton('Add', parent=self) + self.ok_button.setFixedHeight( + common.size(common.size_row_height) * 0.8 + ) + self.cancel_button = ui.PaintedButton('Cancel', parent=self) + self.cancel_button.setFixedHeight( + common.size(common.size_row_height) * 0.8 + ) + self.pick_button = ui.PaintedButton('Pick', parent=self) + + self.editor = ui.LineEdit(parent=self) + self.editor.setPlaceholderText( + 'Enter the path to a server, e.g. \'//my_server/jobs\'' + ) + self.setFocusProxy(self.editor) + self.editor.setFocusPolicy(QtCore.Qt.StrongFocus) + + row = ui.add_row(None, parent=self) + row.layout().addWidget(self.editor, 1) + row.layout().addWidget(self.pick_button, 0) + + row = ui.add_row(None, parent=self) + row.layout().addWidget(self.ok_button, 1) + row.layout().addWidget(self.cancel_button, 0) + + def paintEvent(self, event): + painter = QtGui.QPainter(self) + painter.setPen(QtCore.Qt.NoPen) + painter.setBrush(common.color(common.color_background)) + + o = common.size(common.size_margin) * 2 + painter.drawRect(self.rect().adjusted(o, o, -o, -o)) + painter.end() + + def _connect_signals(self): + """Connect signals.""" + self.ok_button.clicked.connect( + lambda: self.done(QtWidgets.QDialog.Accepted) + ) + self.cancel_button.clicked.connect( + lambda: self.done(QtWidgets.QDialog.Rejected) + ) + self.pick_button.clicked.connect(self.pick) + self.editor.textChanged.connect( + lambda: self.editor.setStyleSheet( + f'color: {common.rgb(common.color_green)};' + ) + ) + + def _add_completer(self): + """Add and populate a QCompleter with mounted drive names. + + """ + items = [] + for info in QtCore.QStorageInfo.mountedVolumes(): + if info.isValid(): + items.append(info.rootPath()) + items += common.servers.values() + + completer = QtWidgets.QCompleter(items, parent=self) + completer.setCaseSensitivity(QtCore.Qt.CaseInsensitive) + common.set_stylesheet(completer.popup()) + self.editor.setCompleter(completer) + + @QtCore.Slot() + def pick(self): + """Get an existing directory to use as a server. + + """ + _dir = QtWidgets.QFileDialog.getExistingDirectory(parent=self) + if not _dir: + return + + file_info = QtCore.QFileInfo(_dir) + if file_info.exists(): + self.editor.setText(file_info.absoluteFilePath()) + + @common.error + @common.debug + def done(self, result): + """Finalize action. + + """ + if result == QtWidgets.QDialog.Rejected: + super().done(result) + return + + if not self.text(): + raise RuntimeError('No server path specified.') + + v = self.text() + file_info = QtCore.QFileInfo(v) + + if not file_info.exists(): + self._apply_invalid_style() + raise RuntimeError(f'{file_info.filePath()} path does not exist.') + + if not file_info.isReadable(): + self._apply_invalid_style() + raise RuntimeError(f'{file_info.filePath()} path is not readable.') + + if v in common.servers: + self._apply_invalid_style() + raise RuntimeError(f'{file_info.filePath()} path already added to the server list.') + + actions.add_server(v) + super().done(QtWidgets.QDialog.Accepted) + + def _apply_invalid_style(self): + # Indicate the selected item is invalid and keep the editor open + self.editor.setStyleSheet( + 'color: {0}; border-color: {0}'.format( + common.rgb(common.color_red) + ) + ) + self.editor.blockSignals(True) + self.editor.setText(self.text()) + self.editor.blockSignals(False) + + def text(self): + """Sanitize text. + + Returns: + str: The sanitized text. + + """ + v = self.editor.text() + return common.strip(v) if v else '' + + def showEvent(self, event): + """Show event handler. + + """ + common.center_to_parent(self, self.parent().window()) + self.editor.setFocus() + + def sizeHint(self): + """Returns a size hint. + + """ + return QtCore.QSize( + common.size(common.size_width), + common.size(common.size_row_height) * 2 + ) + + +class ServersWidgetContextMenu(contextmenu.BaseContextMenu): + """Context menu associated with :class:`ServersWidget`. + + """ + + def setup(self): + """Creates the context menu. + + """ + self.add_menu() + self.separator() + if isinstance( + self.index, QtWidgets.QListWidgetItem + ) and self.index.flags() & QtCore.Qt.ItemIsEnabled: + self.reveal_menu() + self.remove_menu() + elif isinstance( + self.index, + QtWidgets.QListWidgetItem + ) and not self.index.flags() & QtCore.Qt.ItemIsEnabled: + self.remove_menu() + self.separator() + self.refresh_menu() + + def add_menu(self): + """Add server action. + + """ + self.menu[contextmenu.key()] = { + 'text': 'Add server...', + 'action': self.parent().add, + 'icon': ui.get_icon('add', color=common.color(common.color_green)) + } + + def reveal_menu(self): + """Reveal server item action. + + """ + self.menu['Reveal...'] = { + 'action': lambda: actions.reveal(f'{self.index.text()}/.'), + 'icon': ui.get_icon('folder'), + } + + def remove_menu(self): + """Remove server item action. + + """ + self.menu['Remove'] = { + 'action': self.parent().remove, + 'icon': ui.get_icon('close', color=common.color(common.color_red)) + } + + def refresh_menu(self): + """Refresh server list action. + + """ + self.menu['Refresh'] = { + 'action': self.parent().init_data, + 'icon': ui.get_icon('refresh') + } + + +class ServersWidget(ui.ListWidget): + """List widget used to add and remove servers to and from the local + user settings. + + """ + progressUpdate = QtCore.Signal(str, str) + + def __init__(self, parent=None): + super().__init__( + default_icon='server', + parent=parent + ) + + self.setAttribute(QtCore.Qt.WA_NoSystemBackground) + self.setAttribute(QtCore.Qt.WA_TranslucentBackground) + + self._connect_signals() + self._init_shortcuts() + + def _init_shortcuts(self): + """Initializes shortcuts. + + """ + shortcuts.add_shortcuts(self, shortcuts.JobEditorShortcuts) + connect = functools.partial( + shortcuts.connect, shortcuts.JobEditorShortcuts + ) + connect(shortcuts.AddItem, self.add) + connect(shortcuts.RemoveItem, self.remove) + + def _connect_signals(self): + """Connects signals. + + """ + super()._connect_signals() + + self.selectionModel().selectionChanged.connect( + functools.partial(common.save_selection, self) + ) + + common.signals.serversChanged.connect(self.init_data) + common.signals.serverAdded.connect( + functools.partial(common.select_index, self) + ) + + @common.debug + @common.error + @QtCore.Slot() + def remove(self, *args, **kwargs): + """Remove a server item. + + """ + index = common.get_selected_index(self) + if not index.isValid(): + return + + v = index.data(QtCore.Qt.DisplayRole) + v = common.strip(v) + actions.remove_server(v) + + @common.debug + @common.error + @QtCore.Slot() + def add(self, *args, **kwargs): + """Add a server item. + + """ + w = AddServerDialog(parent=self) + w.accepted.connect(self.init_data) + w.open() + + def contextMenuEvent(self, event): + """Context menu event handler. + + """ + item = self.itemAt(event.pos()) + menu = ServersWidgetContextMenu(item, parent=self) + pos = event.pos() + pos = self.mapToGlobal(pos) + menu.move(pos) + menu.exec_() + + @common.debug + @common.error + @QtCore.Slot() + def init_data(self, *args, **kwargs): + """Load data. + + """ + common.save_selection(self) + + self.selectionModel().clearSelection() + self.selectionModel().blockSignals(True) + + self.clear() + + size = QtCore.QSize( + 0, + common.size(common.size_row_height) * 0.8 + ) + + for path in sorted(common.servers, key=lambda x: x.lower()): + self.progressUpdate.emit('Loading servers', f'Loading {path}...') + + item = QtWidgets.QListWidgetItem() + item.setData(QtCore.Qt.DisplayRole, path) + item.setData(QtCore.Qt.UserRole, path) + item.setData(QtCore.Qt.StatusTipRole, path) + item.setData(QtCore.Qt.WhatsThisRole, path) + item.setData(QtCore.Qt.ToolTipRole, path) + item.setSizeHint(size) + + self.validate_item(item) + self.insertItem(self.count(), item) + + self.progressUpdate.emit('', '') + self.selectionModel().blockSignals(False) + common.restore_selection(self) + + @QtCore.Slot(QtWidgets.QListWidgetItem) + def validate_item(self, item): + """Check if the given server item is valid. + + """ + selected_index = common.get_selected_index(self) + + pixmap = images.rsc_pixmap( + 'server', common.color(common.color_text), + common.size(common.size_row_height) * 0.8 + ) + pixmap_selected = images.rsc_pixmap( + 'server', common.color(common.color_green), + common.size(common.size_row_height) * 0.8 + ) + pixmap_disabled = images.rsc_pixmap( + 'close', common.color(common.color_red), + common.size(common.size_row_height) * 0.8 + ) + icon = QtGui.QIcon() + + file_info = QtCore.QFileInfo(item.text()) + if file_info.exists() and file_info.isReadable(): + icon.addPixmap(pixmap, QtGui.QIcon.Normal) + icon.addPixmap(pixmap_selected, QtGui.QIcon.Selected) + icon.addPixmap(pixmap_selected, QtGui.QIcon.Active) + icon.addPixmap(pixmap_disabled, QtGui.QIcon.Disabled) + item.setFlags( + QtCore.Qt.ItemIsEnabled | + QtCore.Qt.ItemIsSelectable + ) + valid = True + else: + icon.addPixmap(pixmap_disabled, QtGui.QIcon.Normal) + icon.addPixmap(pixmap_disabled, QtGui.QIcon.Selected) + icon.addPixmap(pixmap_disabled, QtGui.QIcon.Active) + icon.addPixmap(pixmap_disabled, QtGui.QIcon.Disabled) + valid = False + + item.setData(QtCore.Qt.DecorationRole, icon) + + index = self.indexFromItem(item) + if not valid and selected_index == index: + self.selectionModel().clearSelection() + + +class Node(QtCore.QObject): + + def __contains__(self, v): + if not isinstance(v, str): + return False + return [f for f in self.children if f.name.lower() == v.lower()] != [] + + def __init__(self, name): + self.name = name + self.children = [] + self.parent = None + + self.fetched_children = False + + def add_child(self, child): + child.parent = self + + children = self.children + children.append(child) + self.children = sorted(children, key=lambda x: x.name.lower()) + + def child(self, row): + if row < 0 or row >= len(self.children): + return None + return self.children[row] + + def child_count(self): + return len(self.children) + + def row(self): + if self.parent: + return self.parent.children.index(self) + return 0 + + +class ServerNode(Node): + pass + + +class JobNode(Node): + pass + + +class BookmarkItemNode(Node): + pass + + +class AddJobDialog(base.BasePropertyEditor): + """A custom `BasePropertyEditor` used to add new jobs on a server. + + """ + + #: UI layout definition + sections = { + 0: { + 'name': 'Add Job', + 'icon': '', + 'color': common.color(common.color_dark_background), + 'groups': { + 0: { + 0: { + 'name': 'Name', + 'key': None, + 'validator': base.job_name_validator, + 'widget': ui.LineEdit, + 'placeholder': 'Name, e.g. `MY_NEW_JOB`', + 'description': 'The job\'s name, e.g. `MY_NEW_JOB`.', + }, + }, + 1: { + 0: { + 'name': 'Template', + 'key': None, + 'validator': None, + 'widget': functools.partial( + templates.TemplatesWidget, templates.JobTemplateMode + ), + 'placeholder': None, + 'description': 'Select a folder template to create this asset.', + }, + }, + }, + }, + } + + def __init__(self, server, parent=None): + super().__init__( + server, + None, + None, + asset=None, + db_table=None, + buttons=('Add job', 'Cancel'), + hide_thumbnail_editor=False, + section_buttons=False, + frameless=True, + fallback_thumb='placeholder', + parent=parent + ) + + self.setWindowTitle(f'{self.server}: Add job') + + o = common.size(common.size_margin) * 2 + self.layout().setContentsMargins(o, o, o, o) + + self.scroll_area.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + self.scroll_area.setVerticalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + + common.signals.templateExpanded.connect(self.close) + common.signals.jobAdded.connect(self.close) + common.signals.serversChanged.connect(self.close) + + def db_source(self): + """A file path to use as the source of database values. + + Returns: + str: The database source file. + + """ + return None + + def init_data(self): + """Initialize data. + + """ + pass + + @common.error + @common.debug + def save_changes(self): + """Verify user options and create a new job item. + + """ + + value = self.name_editor.text() + if not value: + raise ValueError('Must enter a name to create a job.') + + # Check if there's a thumbnail set + if not self.thumbnail_editor.image() or self.thumbnail_editor.image().isNull(): + if common.show_message( + 'No thumbnail set for new job', + 'Are you sure want to continue? You can add a thumbnail after the job was created by ' + 'saving a `thumbnail.png` file to the job\'s folder.', + buttons=[common.YesButton, common.CancelButton], + message_type=None, + modal=True + ) == QtWidgets.QDialog.Rejected: + return False + + value = value.replace('\\', '/') + server = self.server + + if QtCore.QFileInfo(f'{server}/{value}').exists(): + raise RuntimeError(f'{server}/{value} already exists.') + + # Assets with relative paths + if '/' in self.name_editor.text(): + segments = value.split('/') + name = segments.pop() + + # Create the folder structure + rel_path = '/'.join(segments) + + _dir = QtCore.QDir(f'{server}/{rel_path}') + if not _dir.exists() and not _dir.mkpath('.'): + raise RuntimeError(f'Could not create {_dir.path()}') + + # Get the folder the template will be expanded into and + # create it if it doesn't exist + asset_root = f'{server}/{segments.pop(0)}' + _dir = QtCore.QDir(asset_root) + if not _dir.exists() and not _dir.mkpath('.'): + raise RuntimeError(f'Could not create {_dir.path()}') + + # Expand the template into the asset folder + self.template_editor.template_list_widget.create(name, f'{server}/{rel_path}') + path = f'{server}/{rel_path}/{name}' + + # Add the link to the first folder of the asset structure + if not common.add_link( + asset_root, + f'{"/".join(segments)}/{name}'.strip('/'), + section='links/job' + ): + raise RuntimeError(f'Could not add link to {server}/{segments[0]}') + else: + name = value + + # Expand the template into the asset folder + self.template_editor.template_list_widget.create(name, server) + path = f'{server}/{name}' + + # Verify the job was created + file_info = QtCore.QFileInfo(path) + if not file_info.exists(): + raise RuntimeError(f'Could not find {path}') + + # Create a thumbnail + try: + path += f'/thumbnail.{common.thumbnail_format}' + self.thumbnail_editor.save_image(destination=path) + except: + pass + + # Let the outside world know + common.signals.jobAdded.emit(file_info.filePath()) + + common.show_message( + 'Success', + f'{name} was successfully created at\n{file_info.filePath()}', + message_type='success' + ) + + return True + + def showEvent(self, event): + """Show event handler. + + """ + common.center_to_parent(self, self.parent().window()) + self.name_editor.setFocus() + + def sizeHint(self): + """Returns a size hint. + + """ + return QtCore.QSize( + common.size(common.size_width) * 1.4, + common.size(common.size_height) * 1.0 + ) + + +class JobsViewContextMenu(contextmenu.BaseContextMenu): + """Context menu associated with :class:`BookmarkItemEditor`. + + """ + + def setup(self): + """Creates the context menu. + + """ + self.add_menu() + self.separator() + self.bookmark_properties_menu() + self.separator() + self.reveal_menu() + self.copy_json_menu() + self.show_links_menu() + self.separator() + self.collapse_menu() + self.separator() + self.refresh_menu() + self.separator() + self.prune_bookmarks_menu() + self.reveal_default_bookmarks_menu() + + def collapse_menu(self): + """Menu used to collapse items. + + """ + self.menu[contextmenu.key()] = { + 'text': 'Collapse all', + 'action': self.parent().collapseAll, + } + + def add_menu(self): + """Menu used to mark a folder as a bookmark item. + + """ + self.menu[contextmenu.key()] = { + 'text': 'Add bookmark item...', + 'action': self.parent().add_bookmark_item, + 'icon': ui.get_icon('add', color=common.color(common.color_green)) + } + + def reveal_menu(self): + """Reveal bookmark item action.""" + self.menu[contextmenu.key()] = { + 'text': 'Reveal', + 'action': functools.partial( + actions.reveal, f'{self.index.data(QtCore.Qt.UserRole)}/.' + ), + 'icon': ui.get_icon('folder') + } + + def refresh_menu(self): + """Forces a model data refresh. + + """ + model = self.parent().model() + self.menu[contextmenu.key()] = { + 'text': 'Refresh', + 'action': lambda: model.init_data(model.root_node.name, force=True), + 'icon': ui.get_icon('refresh') + } + + def bookmark_properties_menu(self): + """Show the bookmark item property editor. + + """ + server, job, root = self.parent().window().get_args() + if not all((server, job, root)): + return + + self.menu[contextmenu.key()] = { + 'text': 'Edit bookmark item properties...', + 'action': functools.partial(actions.edit_bookmark, server, job, root), + 'icon': ui.get_icon('settings') + } + + @QtCore.Slot() + @common.error + @common.debug + def copy_json_menu(self): + """Copy bookmark item as JSON action. + + """ + server, job, root = self.parent().window().get_args() + if not all((server, job, root)): + return + + if not QtCore.QFileInfo(f'{server}/{job}/{root}').exists(): + raise RuntimeError(f'{server}/{job}/{root} does not exist.') + + d = { + f'{server}/{job}/{root}': { + 'server': server, + 'job': job, + 'root': root + } + } + s = json.dumps( + d, + indent=4, + ) + + def show_json(s): + """Shows a popup with the bookmark item as json text. + + """ + w = QtWidgets.QDialog(parent=self.parent()) + w.setMinimumWidth(common.size(common.size_width)) + w.setMinimumHeight(common.size(common.size_height * 0.5)) + b = QtWidgets.QTextBrowser(parent=w) + b.setText(s) + QtWidgets.QVBoxLayout(w) + w.layout().addWidget(b) + w.open() + + QtWidgets.QApplication.clipboard().setText(s) + + self.menu[contextmenu.key()] = { + 'text': 'Item as json...', + 'action': functools.partial(show_json, s), + 'icon': ui.get_icon('copy') + } + + @QtCore.Slot() + @common.debug + @common.error + def show_links_menu(self): + """Show the links associated with the selected item. + + """ + if not self.parent().selectionModel().hasSelection(): + return + + index = next(iter(self.parent().selectionModel().selectedIndexes())) + if not index.isValid(): + return + + server = self.parent().model().root_node.name + if server == 'server': + return + + path = index.data(QtCore.Qt.UserRole) + + v = common.get_links( + f'{server}/{path[len(server) + 1:].split("/")[0]}', + section='links/job' + ) + + def _show_links(s): + """Shows a popup with the bookmark item as json text. + + """ + w = QtWidgets.QDialog(parent=self.parent()) + w.setMinimumWidth(common.size(common.size_width)) + w.setMinimumHeight(common.size(common.size_height * 0.5)) + b = QtWidgets.QTextBrowser(parent=w) + b.setText(s) + QtWidgets.QVBoxLayout(w) + w.layout().addWidget(b) + w.open() + + QtWidgets.QApplication.clipboard().setText(s) + + self.menu[contextmenu.key()] = { + 'text': 'Show links...', + 'action': functools.partial(_show_links, '\n'.join(v)), + 'icon': ui.get_icon('copy') + } + + @QtCore.Slot() + @common.debug + @common.error + def prune_bookmarks_menu(self): + """Prune bookmarks. + + """ + + def prune(): + """Prune bookmarks. + + """ + actions.prune_bookmarks() + server = self.parent().model().root_node.name + if server == 'server': + return + self.parent().model().init_data(server, force=True) + + self.menu[contextmenu.key()] = { + 'text': 'Prune bookmark items', + 'action': prune, + } + + @QtCore.Slot() + @common.debug + @common.error + def reveal_default_bookmarks_menu(self): + self.menu[contextmenu.key()] = { + 'text': 'Reveal default bookmarks', + 'action': actions.reveal_default_bookmarks_json, + } + + +class JobsModel(QtCore.QAbstractItemModel): + progressUpdate = QtCore.Signal(str, str) + + def __init__(self, parent=None): + super().__init__(parent=parent) + self.root_node = ServerNode('server') + + def index(self, row, column, parent=QtCore.QModelIndex()): + if not self.hasIndex(row, column, parent): + return QtCore.QModelIndex() + + if not parent.isValid(): + parent_node = self.root_node + else: + parent_node = parent.internalPointer() + + child_node = parent_node.child(row) + if child_node: + return self.createIndex(row, column, child_node) + else: + return QtCore.QModelIndex() + + def parent(self, index): + if not index.isValid(): + return QtCore.QModelIndex() + + node = index.internalPointer() + if not node: + return QtCore.QModelIndex() + + if node.parent == self.root_node: + return QtCore.QModelIndex() + + return self.createIndex(node.parent.row(), 0, node.parent) + + def rowCount(self, parent=QtCore.QModelIndex()): + if parent.column() > 0: + return 0 + + if not parent.isValid(): + parent_node = self.root_node + else: + parent_node = parent.internalPointer() + + return parent_node.child_count() + + def columnCount(self, parent=QtCore.QModelIndex()): + return 2 + + def headerData(self, section, orientation, role): + if role == QtCore.Qt.DisplayRole: + if section == 0: + return 'Job' + elif section == 1: + return 'Status' + + if role == QtCore.Qt.SizeHintRole: + return QtCore.QSize( + 0, + common.size(common.size_row_height) * 0.8 + ) + + if role == QtCore.Qt.TextAlignmentRole: + return QtCore.Qt.AlignCenter + + return None + + def data(self, index, role, parent=QtCore.QModelIndex()): + if not index.isValid(): + return None + node = index.internalPointer() + if not node: + return None + + if role == QtCore.Qt.SizeHintRole: + if isinstance(node, BookmarkItemNode): + return QtCore.QSize( + super().parent().width() * 0.5, + common.size(common.size_row_height) * 0.8 + ) + elif isinstance(node, JobNode): + return QtCore.QSize( + super().parent().width() * 0.5, + common.size(common.size_row_height) * 0.8 + ) + else: + return QtCore.QSize( + super().parent().width() * 0.5, + common.size(common.size_row_height) + ) + + if index.column() == 0: + if role == QtCore.Qt.DisplayRole: + return node.name[len(node.parent.name):].strip('/') + + if role == QtCore.Qt.UserRole: + return index.internalPointer().name + + if role == QtCore.Qt.ToolTipRole: + return index.internalPointer().name + + if role == QtCore.Qt.WhatsThisRole: + return index.internalPointer().name + + if role == QtCore.Qt.StatusTipRole: + return index.internalPointer().name + + if role == QtCore.Qt.ForegroundRole: + if isinstance(node, JobNode) and self.hasChildren(index): + return common.color(common.color_text) + elif isinstance(node, JobNode) and not self.hasChildren(index): + return common.color(common.color_disabled_text) + elif isinstance(node, BookmarkItemNode) and node.name in common.bookmarks: + return common.color(common.color_text) + elif isinstance(node, BookmarkItemNode) and node.name not in common.bookmarks: + return common.color(common.color_disabled_text) + + if role == QtCore.Qt.DecorationRole: + if isinstance(node, ServerNode): + return ui.get_icon('server') + elif isinstance(node, JobNode) and self.hasChildren(index): + return ui.get_icon('asset', color=common.color(common.color_disabled_text)) + elif isinstance(node, JobNode) and not self.hasChildren(index): + return ui.get_icon('asset', color=common.color(common.color_dark_background)) + elif isinstance(node, BookmarkItemNode) and node.name in common.bookmarks: + return ui.get_icon('bookmark', color=common.color(common.color_green)) + elif isinstance(node, BookmarkItemNode): + return ui.get_icon('bookmark', color=common.color(common.color_disabled_text)) + + if index.column() == 1: + + if isinstance(node, JobNode): + if role == QtCore.Qt.DecorationRole: + return ui.get_icon('add_circle', color=common.color(common.color_green)) + if role == QtCore.Qt.DisplayRole: + return 'Add bookmark item' + if role == QtCore.Qt.ForegroundRole: + return common.color(common.color_green) + if role == QtCore.Qt.WhatsThisRole: + return 'Click to add a new bookmark item' + if role == QtCore.Qt.ToolTipRole: + return 'Click to add a new bookmark item' + if role == QtCore.Qt.StatusTipRole: + return 'Click to add a new bookmark item' + + if isinstance(node, BookmarkItemNode): + if role == QtCore.Qt.DisplayRole: + if node.name in common.bookmarks: + return 'active' + else: + return 'inactive' + if role == QtCore.Qt.ForegroundRole: + if node.name in common.bookmarks: + return common.color(common.color_green) + else: + return common.color(common.color_disabled_text) + if role == QtCore.Qt.DecorationRole: + if node.name in common.bookmarks: + return ui.get_icon('check', color=common.color(common.color_green)) + else: + return ui.get_icon('close', color=common.color(common.color_red)) + if role == QtCore.Qt.WhatsThisRole: + return f'Bookmark item: {node.name}' + if role == QtCore.Qt.ToolTipRole: + return f'Bookmark item: {node.name}' + if role == QtCore.Qt.StatusTipRole: + return f'Bookmark item: {node.name}' + + return None + + def canFetchMore(self, index): + """Returns True if the parent node has not been expanded yet. + + """ + if not index.isValid(): + return False + + node = index.internalPointer() + + if isinstance(node.parent, JobNode): + return False + + if not node.fetched_children: + return True + + return False + + def fetchMore(self, index): + """Fetches children for the given parent node. + + """ + node = index.internalPointer() + + if node.fetched_children: + self.progressUpdate.emit('', '') + return + + self.layoutAboutToBeChanged.emit() + + recursion = common.settings.value('jobs/scandepth') + recursion = int(recursion) if recursion is not None else 2 + + for path in self.bookmark_item_generator(node.name, max_recursion=recursion): + if path in node: + continue + node.add_child(BookmarkItemNode(path)) + self.layoutChanged.emit() + + node.fetched_children = True + self.progressUpdate.emit('', '') + + def hasChildren(self, index): + if not index.isValid(): + return True + + node = index.internalPointer() + + if isinstance(node.parent, JobNode): + return False + + if not node.fetched_children: + return True + + return node.child_count() > 0 + + def flags(self, index): + if not index.isValid(): + return QtCore.Qt.NoItemFlags + if index.column() == 0: + return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable + if index.column() == 1: + return QtCore.Qt.ItemIsEnabled + + return QtCore.Qt.ItemIsEnabled | QtCore.Qt.ItemIsSelectable + + @QtCore.Slot(str) + def init_data(self, server, force=False): + self.progressUpdate.emit('', '') + + self.beginResetModel() + + if not force and server in cache: + self.root_node = cache[server] + self.endResetModel() + self.progressUpdate.emit('', '') + return + + self.root_node = ServerNode(server) + + if server == 'server': + self.endResetModel() + self.progressUpdate.emit('', '') + return + + for job in self.item_generator(): + node = JobNode(job) + self.root_node.add_child(node) + + self.endResetModel() + + cache[server] = self.root_node + + self.progressUpdate.emit('', '') + + def item_generator(self): + """Scans the current server to find job items. + + """ + server = self.root_node.name + if not server: + return + if server.endswith(':'): + server = f'{server}/' + + # Parse source otherwise + for entry in os.scandir(server): + if not entry.is_dir(): + continue + if entry.name.startswith('.'): + continue + if entry.name.startswith('$'): + continue + + file_info = QtCore.QFileInfo(entry.path) + + if file_info.isHidden(): + continue + if not file_info.isReadable(): + continue + + # Test access + try: + next(os.scandir(file_info.filePath())) + except: + continue + + # Use paths in the link file, if available + links = common.get_links(file_info.filePath(), section='links/job') + if links: + for link in links: + _file_info = QtCore.QFileInfo(f'{file_info.filePath()}/{link}') + yield _file_info.filePath() + else: + yield file_info.filePath() + + def bookmark_item_generator(self, path, recursion=0, max_recursion=2): + """Recursive scanning function for finding bookmark folders + inside the given path. + + """ + # If links exist, return items stored in the link file and nothing else + if recursion == 0: + links = common.get_links(path, section='links/root') + for v in links: + yield f'{path}/{v}' + + # Otherwise parse the folder + recursion += 1 + if recursion > max_recursion: + return + + # Let unreadable paths fail silently + try: + it = os.scandir(path) + except: + return + + for entry in it: + + if not entry.is_dir(): + continue + if entry.name.startswith('.'): + continue + if entry.name.startswith('$'): + continue + + self.progressUpdate.emit(f'Scanning...', entry.name) + + file_info = QtCore.QFileInfo(entry.path) + if file_info.isHidden(): + continue + if not file_info.isReadable(): + continue + + # yield the match + path = entry.path.replace('\\', '/') + + if entry.name == common.bookmark_cache_dir: + _path = '/'.join(path.split('/')[:-1]) + yield _path + + yield from self.bookmark_item_generator(path, recursion=recursion, max_recursion=max_recursion) + + +class JobsView(QtWidgets.QTreeView): + progressUpdate = QtCore.Signal(str, str) + + def __init__(self, parent=None): + super().__init__(parent=parent) + + self.setModel(JobsModel(parent=self)) + + self.setRootIsDecorated(True) + self.setIndentation(common.size(common.size_margin)) + self.setUniformRowHeights(False) + + self.setItemDelegate(ui.ListWidgetDelegate(parent=self)) + + self.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers) + self.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection) + self.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows) + + self.setAttribute(QtCore.Qt.WA_NoSystemBackground) + self.setAttribute(QtCore.Qt.WA_TranslucentBackground) + + self.installEventFilter(self) + + # Hide the top header + header = QtWidgets.QHeaderView(QtCore.Qt.Horizontal, parent=self) + + header.setSectionsMovable(False) + header.setSectionsClickable(False) + header.setStretchLastSection(True) + header.setSectionResizeMode(QtWidgets.QHeaderView.Fixed) + + header.setMinimumSectionSize(common.size(common.size_width) * 0.2) + + self.setHeader(header) + self.header().hide() + + self.setSizePolicy( + QtWidgets.QSizePolicy.MinimumExpanding, + QtWidgets.QSizePolicy.MinimumExpanding + ) + + # Disable the horizontal scrollbar + self.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAlwaysOff) + + self._connect_signals() + + def _connect_signals(self): + self.model().modelReset.connect(self.expand_active_items) + + self.model().progressUpdate.connect(self.progressUpdate) + + self.expanded.connect(self.resize_columns) + self.collapsed.connect(self.resize_columns) + self.model().layoutChanged.connect(self.resize_columns) + self.model().modelReset.connect(self.resize_columns) + + self.doubleClicked.connect(self.toggle_bookmark_item) + + common.signals.jobAdded.connect(self.job_added) + + def contextMenuEvent(self, event): + """Context menu event. + + """ + index = self.indexAt(event.pos()) + menu = JobsViewContextMenu(index, parent=self) + pos = event.pos() + pos = self.mapToGlobal(pos) + menu.move(pos) + menu.exec_() + + def eventFilter(self, widget, event): + """Event filter handler. + + """ + if widget is not self: + return False + if event.type() == QtCore.QEvent.Paint: + ui.paint_background_icon('asset', widget) + return True + return False + + @QtCore.Slot() + def resize_columns(self, *args, **kwargs): + """Resize the columns to fit the data. + + """ + self.header().resizeSections(QtWidgets.QHeaderView.ResizeToContents) + self.header().resizeSection( + 0, + self.width() - self.header().sectionSize(1) - common.size(common.size_margin) + ) + + @QtCore.Slot(str) + def job_added(self, path): + """Slot -> Shows a recently added job in the view. + + Args: + path (str): The path to the job. + + """ + if not path: + return + if not QtCore.QFileInfo(path).exists(): + return + + path = QtCore.QFileInfo(path).filePath() + model = self.model() + + # Reload the data + model.init_data(model.root_node.name, force=True) + for node in model.root_node.children: + if node.name.lower() == path.lower(): + index = model.createIndex(node.row(), 0, node) + self.setExpanded(index, True) + self.selectionModel().select( + index, + QtCore.QItemSelectionModel.ClearAndSelect | QtCore.QItemSelectionModel.Rows + ) + self.selectionModel().setCurrentIndex( + index, + QtCore.QItemSelectionModel.ClearAndSelect | QtCore.QItemSelectionModel.Rows + ) + return + + @QtCore.Slot() + def expand_active_items(self): + """Slot -> Expands all job items that have active bookmark items + + """ + for v in common.bookmarks: + for node in self.model().root_node.children: + if node.name.lower() in v.lower(): + index = self.model().createIndex(node.row(), 0, node) + self.setExpanded(index, True) + break + + @common.debug + @common.error + @QtCore.Slot() + def add_bookmark_item(self, *args, **kwargs): + """Pick and add a folder as a new bookmark item. + + """ + if self.model().root_node.name == 'server': + raise RuntimeError('No server selected.') + + if not self.selectionModel().hasSelection(): + raise RuntimeError('Must select a job first.') + + index = next(f for f in self.selectionModel().selectedIndexes()) + if not index.isValid(): + return + + if not QtCore.QFileInfo(index.data(QtCore.Qt.UserRole)).exists(): + raise RuntimeError(f'{index.data(QtCore.Qt.UserRole)} does not exist.') + + if not self.isExpanded(index): + self.setExpanded(index, True) + + path = QtWidgets.QFileDialog.getExistingDirectory( + self, + 'Select a folder to use as a bookmark item', + index.data(QtCore.Qt.UserRole), + QtWidgets.QFileDialog.ShowDirsOnly | + QtWidgets.QFileDialog.DontResolveSymlinks + ) + + if not path: + return + + if index.data(QtCore.Qt.UserRole).lower() not in path.lower(): + raise RuntimeError('Bookmark item must be inside the selected job folder.') + + node = index.internalPointer() + + if path in node: + common.show_message( + f'Error', + body=f'Cannot select {path.split("/")[-1]} because it is already a bookmark item.', + message_type='error' + ) + return + + name = path[len(node.name):].strip('/') + + # Add link + if not common.add_link(node.name, name, section='links/root'): + raise RuntimeError('Failed to add link.') + + self.model().layoutAboutToBeChanged.emit() + if path not in node: + node.add_child(BookmarkItemNode(path)) + + self.model().layoutChanged.emit() + + @common.debug + @common.error + @QtCore.Slot() + def add(self, *args, **kwargs): + """Add a server item. + + """ + server = self.model().root_node.name + if server == 'server': + raise RuntimeError('No server selected.') + + w = AddJobDialog(server, parent=self) + w.open() + + @QtCore.Slot() + @common.debug + @common.error + def toggle_bookmark_item(self, *args, **kwargs): + server, job, root = self.parent().window().get_args() + if not all((server, job, root)): + return + + if f'{server}/{job}/{root}' not in common.bookmarks: + actions.add_bookmark(server, job, root) + else: + actions.remove_bookmark(server, job, root) + + if self.selectionModel().hasSelection(): + index = next(f for f in self.selectionModel().selectedIndexes()) + self.update(index) + + def mouseReleaseEvent(self, event): + """Mouse release event. + + """ + index = self.indexAt(event.pos()) + if index.isValid() and index.column() == 1: + node = index.internalPointer() + if isinstance(node, JobNode): + event.accept() + self.add_bookmark_item(index) + return + if isinstance(node, BookmarkItemNode): + event.accept() + self.toggle_bookmark_item() + return + + super().mouseReleaseEvent(event) + + def sizeHint(self): + return QtCore.QSize( + common.size(common.size_width), + common.size(common.size_height) * 0.8 + ) diff --git a/bookmarks/items/bookmark_items.py b/bookmarks/items/bookmark_items.py index e6255743..789a7174 100644 --- a/bookmarks/items/bookmark_items.py +++ b/bookmarks/items/bookmark_items.py @@ -390,4 +390,4 @@ def get_hint_string(self): """Returns an informative hint text. """ - return 'Right-click and select \'Manage Bookmark Items\' to add items' + return 'Right-click and select \'Edit Jobs\' to add bookmark items' diff --git a/bookmarks/rsc/gui/branch_closed.png b/bookmarks/rsc/gui/branch_closed.png index 7d219db634c7b4180496f89f5bbecf8ce24c4eb8..62f8ce9183fe2d6f07c44119d081660a5e6b9235 100644 GIT binary patch literal 6259 zcmeHMdsvfamOo!exj1S7XS^3;jNlA}TnL0jZVD2DfL5{M)Kvn6%L++Mq6Bm#7Ash^ zYN%WUfnCOSo$-RDR;pGqwQ5ta>Z*&iB2Wm-##!FA}!%Z8zR1cKw5&5NLhnC0y zoJ+Ekk`2iT@v<~cZh$gfld1|Z=4ufQ5E^0BD$`b~4D3`@X0|$v(|4?e!_H0*<17hE z5F}`$R9V@v1v=HDg2be>f|Y5~bWTJ#JJcvcf?SnB$u{QZsP!^q7>DMSp-j|#4x5G; zR)%qA6Nl{NgazyN( z)1Na$tMqBQY^@<%qh=FEWvXViA&i5e4(E`oeL<_%kJJe(#y2Xpd|`lqnDo2&8QXBM$6~4e~vor>cK1=g=s9#L~4+mJ=goGD9elnKa+!rqNhPi7n zjgf-cInJCatAd(3L!_WjxdUi(sOHiSZCkRXu zhRB3LGNI5d8sz4~<2?bIWFECIn=A|ly!91lnP$-h91cAzs7yjq# zvTI#KWU<+LT&(<&9kEEYYDCJ(X473DQ>Kyb4&$VeWmTnfM%39qQGuVA z!7y!JmI_J#!Ez77^qLGqo>Hfpor%@@r|rc5d%^3KYyNTQfvLg5AaM|OQ))&CPbn2k zc~Ye?ofjm@5UMi7>4HFk>Urufh5lpviLvpXx8E-beWa{uSxR-L3eQSD=a(n*^E&bW ztizAVyu6qG|B~U86LPr2`2V~4=((3pQ(Q<|g-0|IUtA72cyYa`)L17SULMn4bKVEw zu|p9(JIPq{eaCkPYc8)obH9^vq81fUac&9qyt#3U$1Ps`hlfLj6@TFK*2IHNuu^Qy2Q?Bsj>R~S5H~;t6-zaLY+<)AWXn)Pj>Kkmrq zMczV>dulU(!r;}>H}b9VOjz{s95`;a#jI($)!DwE8==rW_w(-#T~qSCOz!w0^+XTDo_sqr+*F-TmK-S`zK@hT`Tk zbD2}0&L2xX@`1{Zyt3o6f)W>_@Zox9q05G8q3bHYD%*Q;=#TkQn0UIVxayM~Pe%_0 zJcXmaJ* zrUsYfN*v5&LQ{T5Kghd$Ai2h6R8zhV2S}g%E~Eqp@?#~GB{ej*^6bEE>YcU(Xmq74 zIVUEES=^RgW0pXQ2Dz`{aB~TCu~A+ZTwogKNm;HV??7kUP`GJrej2-R0&7ghgSqDU zF|1Xo*C)CztAdmmiJT=cVF+=d{*@OrF)RTFmy*daO?tM(JYUWdlA*7cXWQ*i>p>iD zN9aO^Y3V0*wnw+8IqSwjsJYmw>8Z$itf6>f?+qVIr@kxPYjj(0yJB$dz0nO=T_c;a zwBdS-t-CK2jlP8?4fk?wyL&TM!{#zIoDd*IeKvq1Q-|0G&1G_xI;KO*^l^l~D zn@t=>{Ui!FU2nRYVwmt-j|I^no(5ejX`4u}NC+fF$AG)O#Ave{7zG3r9|x9`^xPZ< zcqK4@>j7mRpq0{d(O^+WM|w~KHwuo8qvkR#OeQSbO56u0g5@%8cajOca$+}S2BZ`c ze^=NP6nhZ(X;%oTq2Wj+B2Vz8Wxf(^RLJFtwP#U&pBKmyn3x zLRAH=ia`~Pn#+WmO|)tbUL7>*6;zcImGd~z7LXVw#)4%cEuiNRTEHqK6Cp%M!A-){ zwUG(?iilYvf(i&z7bdu`l&Ic`2g_4hRf4KAT18N_p)0DYiE6wLq&%bZ!koWmlf2$? zg)3u8UjFFtBbp(S0UyyNa>1MvNDNoX!1TQ*DUlZwa%idp81W;TeH0VgW|NA}@&o~k zQ1v3!GU7&yEAfCX?Q~bDq@(h}DjX+s_wWnmExIK%9w3MycGsdHl12PA&x1&BB9wrM zUeXVJq28N7_PfDt1v-m-g8@%TMCZ+5x=#2$oDQZVWyd25N`7Y)T02kcclbDFyI^bk zbJ&XIadt^Vv1ySzMB=G<(I{mrPE^~rUWl?Ba)Ykr%g4^taP!yqyKCba&|FdB^u7c^??QJSWp$5}m@2s)6 z+_Szknt!LiaBaVPBUgek9<6nh*%~mG|GWlp?2Zw!)gz;dZ`ax9-?=k%1hd}rF$Dn^ z2W)kh%_CL)?M6$A73(ApXQzYsL4iHCNfZZzL3oIZ)4UZAl*dkPG#`v@io3h!%XHegZ9E zOR{np`46QIoIHYUThYD3zt9L2=s$3jcC%R1ASJxK z@Q~vh55%7$_+{ABp)`Ihs{Te)y<~eVC9Hn9iE?TD+qidVvMWVk%024Q8ga;>)Zk|8 zi7~gwunsZGBc_r~NC)HOhWnC)*NC3KP8pcc>OG!=yO(zJCfU#=tmH~CJ*2~uvms?S zJ-NdblIc{n(G2)6I>4z_2x}zK`y%XB8n)RLCeqo~YzNb!VU-(9JVT-%ji}MYxd1od z#U^5i@r3Yb6}r55h^YM7&?Tk~c*nq~B)k^u)! z)0TKNchHt@c&cqBo+^>7l~!TCMlqcL-aIUt%m*X;8%_3SCS0S75s&AngvMLl;K?pR zkrfG%bRr3^(CFYN51uc9`dNhO{1&JWq=oHJ??(&oLj81FsD%0{v~cY(?i#mGs%)1p zMD4Sj&~y0rf=@33hVJZYW(@*y?@H6E3a|u(IUX6p4ct zNi&s_k-4|K^VzwN%ObSi5@_o^%qd0V9n8kw( z^kgt8%Wsq8Jup# z`dul*`gr0C=}$XtE3kSlzJ*=s_|ST~_@l$IyB!1$#vg7*4?K$rbzu#5G(0nh>JZ0}28`6&MwE6D+Yhm!0!>@DQTM%pZ zEpxC}Ptoq_Y%ZFmSYKu9;I0U89G>*q2;>wrYs0MHq4gQo)Ne|;eeahK?RRP!?{{Cj=JpOZwiq0xeG=8`sMf6Z~Q;&&m7Mp>?^KP-}J6aCjS;t%uS3wFel~B F{|45Rgt`C# literal 3182 zcmeHJ>sJ$J8h<810ty?eJ>F1)GJ;f!h!z$h0Tq-13~W;6F2N$8ZV94tQG_V&b}3lx z1VrS%9H3kTbkW!aWYCs6B9yiP?8?m&xkNw&28e{P6ZRk258d4l-8pC8_xD`Sb9rap zC&in8&~W*NzmwwUO@TTJZh09zEOKg8E3}UQN3$K6kZJ6B3 z)%@y}TZQ@&f^C@P%uLMhQ)D{(G&yH_hjOM8coTfTygrYS+4+Y{E526;wXkr%MuL(yjBk)|{t4E~75+mhKB?+GdwAJ6rgJb;^G zG3@*e@WAvlY5gsQAd)*RuC+qrEu=zY()Tp-EUW3>00mN|V{rw@{* zPH7JR;yRxX`}865r+jf<)`(1tM>O~BFD`!n?v9v>=_U;Wu zgpwKe-VNGz`zh=w|03*oG87USF|OC z)a$*zjn`trECXr+(8|I2^`RR!XUTUElh8^Jm~TcUXE`T5Gqjdnm> zrA^i7M07U;+P!D9C9~4*k0>h!u}Z0FCd^Cw_W4@!7!cXP!4oVml)Y4@!*KQp z5;HVAQ(Kvmlsj>#7qe6J}sJt#Bfo z0^&!1ugMm)?>6lr$%hTl(P^Wa45qxM)CMa-_w5!Fh$tB#leUw6;dkPpChhNeY8hT{ zlf1x`6#qmil0N0`Tv)>(qDK-Y``jlJ@w$>;SMc4^L`N{tkg*FWhs6bxxWf-B%fLwqlzqMX?R(>NH+>w&yZx8sbyUO zSj-_=F4W8g&Wo#8W8+zo4v6Tje$q>}^R7q1bIkU%iM1NoZL0p06SV66%Is(Uc&3n3 zpIURp2|oO_o&P5T{~!3KYgJUn?sfGw$Gf>bG4#=h+etOm>3PsSYNmg$w4`bQi5MtE zeFU6`9W5kBVjE+o@=!jOowrvp+n=5(WA`avDcH{2jj*;-o`i0w!SrJ}E9%@_-3+TB zgF;J%oJV{OShw2=w1C+6!jB8jnuHz9_wBP30hlI$s|O#?1t`6KSyH=**$O#NiXQic zYoK<<*H*8B@3 zO{m7!S)2pgCaUEuS*Du4hN??>^|@+9R72nI{o44R=b>wb9K{m!5^x5WKoiO-=E;}z z@{4)Tm%xIy9{o$uw2U49k`2qgDB+C;%`*v69iHQUao9mc!&$R<^G6)`$Jb8ei?S7q z1Mg|T`&caAWn(*MCw1f`B^tp~ZmweAAV(7}b+jGM0!hGxGyVz2Ya2~objR1?qee5k zf%uf3mpaf)8+yQTn-t)z&v*z#eP@e1W-D4zj-qq_Vghbw6F<^E{!+;7zI0ECi1v;> zQH1r=Lza#G58}WDSt{1HHGT(KhqZ+YbbiAls5bQn`|}x%$XI@Dkob<4#$5R>Rr^Gj F{|U8;X&C?j diff --git a/bookmarks/rsc/gui/branch_open.png b/bookmarks/rsc/gui/branch_open.png index 64a166dacfe0e2f30e69c7a1f34d81f7ece9f84b..b76a26bd1b3cff450840ff0f8c802ba9aac17e80 100644 GIT binary patch literal 6240 zcmeHMc~n!^zTOGZLcwaF4izXE0;z?N0SqAwL4{((fJF+Zh=e3SBqTA3KoChBm_)P< zA_Wz1U$3%$GaH^AiOkKio)79nMj}^TS20 zVw2gDwfrQ(x(q3QQ$}b6HzS$r&BHPM9evXokRXLG=QyUP2t_hRx*yKS%YfI2nS^sR zLgdMQxYdY5$7uFO$F*W9-_hHhOyrWu6h|*_cP^Pq_Kc^xIa0|~4-%P5B6}0bGzOK% zpm;jYeQ?m7l$XE=5By-x865fHlH_s;gG5S8OLI^2a2HDxNfd8yZ-j$NB|-#IrVzEkZ_2$dU8?a7fjNi6{~0_$rNzwPK-I3LQgr z9!3>xHiIRS$vGk}pB3nbgE6}c1Uv?XOYx@CytqV80^O5H=XrV(IaE(R(UVVI<>f_P zMfKuxU(OE{b9W(?eL0^8=kq;iJWp>LpUB~O@riV*Cza^U<#LD|3YX4LAk(>Y8vo_| zFsT48EspTt=0O%5(h`hy0vRlpVs1rj;_sZh5(*rRUBKXQk?!`xagnm}dAK>V;58Ze z*CH^{rX}$q=|3p92__RK$kRAd{^~@St$*51q`xO##!3Ch(*L#M{XgliZ@+&>`dnJM zNgPokAMTYT91;u(*;GtLA^qQa8SnjW19}OEyBc!&YCna8S35Fa1gWKPzx{4G`zHXb zLs)^UBhpVizPI(ubIIFoKJERDv-`b;`TKZ#?H677>d_7R;>GuPU$1H^SpU$)siylc ztmWpVfnS-OKX}w}(c5o!`{aqN8Kpi4^F-(m#kbU7KmIJdd!Klz>}*TMvE*}pLuK4U z6T0clxC`zsvO`1h*V+uz{@h!XFBIiwK(1(Nw*;Uy41j;ZmJR&HIJvvxZRQpgj(v?fv4C0f5a<` z2e&6M54>GgAy_|pm6e5G+R>KgQtJNn+^OcqXZkeOY(n|a&9JWnQ$vA-f7|maUHe@8^t1ms6wCykEghO7 zh0XXrQh~n8x6bS9yqBiE7#h8cA1NQ2>i6&`oM`Z5pKobgHFn6u|8cH#=K_Tiq~SsJ z=k=c$YKk%qYeFXD)V+E&*eXvi>a~^b9IQK_WRLdwwr!7oIJMaurAcAm9&2G`F|`wJ zwMTP{YC|wp&bb}aN+)gd(%7atXT`6+ZH5{hBYK4crooGDH(f_)UT7GMH{0_IXKnUf zpP94Qh=^fec~)D0SbW+Ci*~Sm zf4`tC3-?>K?DAc+oz|Lz_F1&v@`CNpwjPgis7y)UHvx19V4Hkgu<*X3r#<;{L9lw} z+X+mSisc<*aznUQitzil(0_?3wC)RC;;Rqvz;F zff~=eGKQ|yvOwa6r#^<`FFM;T;}o=8(%c1)r;o+n#L_ML{+ud7xT+&{W}1*9-?ZT% zyZj;$8XufC>*Y&W+Tq_QJeqm4-I7ivpzQ9pKA91Zx1V0B`3hM%(cK}(Vl@`NAM?s9 zw1oLmqV3dyy4}xGLP#q9<&c4?5K>>@35JZF!%3^zH?JbD~%~G_60M(UxTf}XI>8d9fjf4nM`B0ei z)X3?Moe81nb>X{*PT`N(SME931z2k3mJF1o?!xu^sgqByMLj(N+3$DFtWz%}T~%e% zE#CVmb4a=Nw^eOyVJ2Q1tj-h+2e~z{0!l}HV7qNGeBWLG)9XiV(?=Egel^BeGKl}v z0UrZiTc4X;E~WX3CIi$D*wCQr>WH_kmEt-!S54V~qp;ah4wdth6#$QtX z(`rCY-+sk^*~8RKp|17lp4yRnGl^>EmJICKT=rqYE&WU}e&it2df>k5q~wv|fVb8M zuGrCbOOBsVV)Z~Vn~NXWmSOMOcVAT~d&Ep1>Hj&0(Aiyma-wm>f)I+PHJ>!})nBfz z;E-En(*n{Z5BBZbMZQM|-#8ZA{HCwEq4%xMyn(jyx|4Nbqse+-Wy)}NmH!iA^||_- zrxkc*&DfSmO*6{2zf-A8QhV9XcIPiB^eG#fdKy!^dkbdrvFa|Y+4avaSL?;I!iD7r zS`O7%qs*{X;W-A4ZYW&H^IeH0FG9I;Cl^{N-CJC4oX%72HIKHaSH?}soM~itHpVJY z^PQDpZ+}~HS8cU5g0`g0yphA4Xt>c>oDb1-4Dj)*}R=-oUbw;TBtqs&MWhh8n+EU*S&Z69w$j#`;DnH|md%>gpDMce&dzXL zoFn>)M(40>_C$7JUXf8*LA_3yY-MQB4IL7$IgyPJ?mwPmXmh)IY%$by=J|p=Jr=e9 zxj7l}k{DGpRb_=Ge_05SfG_4WtwJ}Zjh*o#K$^jQw zHD&UD=3-Y%_8x20{+16+9!|HUxIH>$zD($jHYROHO2VekpaP+bY5d>kvetf9V!rIG zziIr}bEzJC12Nt4-(cgAtkO8UY%RLiW2_>Vnml?$Ud-3rgOYZbBng`aY|UeinJ;sD z*94GZz;PUUenF5?Ko=gRc>vK}PR0O)$3R?Bpr%ha$Ib>-n{jcHQIq=P1kmY;CDS*X zqTjX%2+Pfv+4-5IiOfOjYK+z4B_=;n7*GSdq2sVA(>`d`Hdu$K+`r2Afn6 zQ5yGr)%DXR6_3h5+$E@jH8(5=aUF1Y*d*$G=qC(gb!~;wZCIh8eCTusNTeXkZbqbb z*pfkX)g`ieN_mI-mHYd{7NY`+1!Ye{MmMUbOOPcgoiwZ}sEoJwFDvLbOtxF@m0`gh z_^2XwPi@lf248x#={wZ5&OJ7&{xAwEz!@80rEIbQJMvXKa+uFB;BH`mbG-u&1YqQp znf?F}5Ex)l?;yf~z5vXlvi^ma76FM3wkjlt*_{JaTk}-gAnYv+=*}_koLBGgWDU^R z=c(8pGyN=cP;H5=V&yPn_5l@P1)A2V%MGyx5(`|Mjg_(*jRGZjbYzPz*R||{A~s_| z6+B$rD;fL@KS8>>M2&Z7Y1An*hqE=u=qQyd!>a6!U-5AZ-er%v)H_s$0*#+es(GJ2 zo|_C69SPL@7o>DSfeuIX63%2j7CLvqN+NTZcKJZ%1Q$JGv;cH#tRx&7OQF#kMSw;N zb3mtJB@sEy6L~;ovj`Q87$Fjil@OuvQ)skCO*H6oJ)om_T;F;tCEW%E5*^X*Fdz>Y zkPEJ_5*j_A5swx%>vHFr4trn1-Et4>-StvKd-CSaU>Rk`VSC1kA8uFxw?4A&-3=2g z&F8Eh-ef%$1Kj>Rb06HCtsD0Mk1hk5p6pepV{y0$7D4I+~WMxX_V{!5_|NC0t-Gz2JALEtaZ2oQ?~v^XaO zm}3q)^NJCmEo599X$1b@*`{w#29-(3Sru;fv)Lu1KgL)omF-JuP`vO5PM`m$gQO)f zH*&d?^$Ff6)q>;5u{8%@cT_*`MMqIBNxM7I)*LQ4mHnp`!;4uxpA2pdn~5-dGH4H< zVS0FCq*GA92e!3`dL~}oK~Rjtqi8d$=hWblt6dAnR|8G!hTCJccxK7qH>}w7mfYzW zb#75+T=@_wR)0aY7z?U4!E>y$*0reiuR24B`_#Ept&Qi$U+hu`!zbKxL8Q4G3k2lX zYcO|;vPGIgGjqw^QaRPlw9p*L z(rYd^$C`P$V5uc(V%HqQ4a<_!lFGHHEJzGPx!(i+33q-QUN86_9-jB-`Fx+}dw|S< z9e(EXZRSG|WX{~|^AiMNz$FGUo(Gn%Nl&N2ViLW1Z!83RPNGlvs&T>|5G2KIV#NhU zg~TNuis3+siHWqZ$ne<1hoU*OsF=`#Pit);$c)7F`Egg$wU4i)!^b2iP==ATm_0 z%%=ozQ@-d5me*(|JD7vpV%#Lf0_{!7;|GnY&MlLw0KUJI=IN+RS1|k&U(Bgbqpe)y ziL3|}tSuV8N{ZYf%l@UD6#281qKV-JOvmTEu}Pw~tUR%@ts$MpsUQ788f}i*Hu``f zrHc4Nuc{++Wr7sCM8sskwXw7onO5;)W?IC56OvzKm z348d~?z36pUK7;hlNazft0d1B+1Jam9#c})0VGgFmrQtk1RqPOYdofJ{_BJ-MU;~u zlal!{~stirf8_x3iLu)9aq!`##& z;zX&@);uXd{?76+s zq~u)LI!)AAp_y#0cT>KaUUuN^b|^6DJrHQRHC2Oge$o-f-Ppf!5bJu*kpVJCQj3Y~ zL_~S(aXF1{Shh7&I9>oy37xOmG~pr;_y_cc4TR$ zsuItp__yk|L5_ z1#0@uD_^MWS0}kj@s&3=T(-;#97_C?*?G@A2eB0bC*5MXe9{M_h@-XOabc-~V|EVg z1DAC3XD_PRoVG-FL5U0LLJc+SoLw-2cRoW1aKr5IL*eQrD7>1a zFJ?~yg4P6Yu?&2Z)0#Tqs4@7R$1Hk5pwHxM+9VL1FN8SC-#qDI4BkIOX&y@r2 zBpKElpb_ERgMNtK@N!@8vb%&5v?1esLclu?R`2T`Nbg&Z(W4KIpLYit(vcOu-kb;r zZ?s~wIgqL2L8wLOE*rb+%3QwfSwg_x_r?LJ#b^uw!sdXb04SRSyj(HMVhw&^HJD0g zyrJ-O2Zs{(IYVqLfN67Z1`pYyFc@V6(XwZT0 z!$n$ne76wv)TrnFAeaRX$vUWC6cwqcf%?okFcmy>_tqUSHTZR2pF0L@