From 5782adf97e9d681de17ec5bdbd86a99a0dacbab6 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 30 Apr 2021 14:43:44 +0200 Subject: [PATCH 01/14] SyncServer - add saving of priority value --- .../modules/sync_server/sync_server_module.py | 32 ++++++++++++++++--- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/openpype/modules/sync_server/sync_server_module.py b/openpype/modules/sync_server/sync_server_module.py index a434af9fea..e411366fd9 100644 --- a/openpype/modules/sync_server/sync_server_module.py +++ b/openpype/modules/sync_server/sync_server_module.py @@ -749,7 +749,7 @@ class SyncServerModule(PypeModule, ITrayModule): return SyncStatus.DO_NOTHING def update_db(self, collection, new_file_id, file, representation, - site, error=None, progress=None): + site, error=None, progress=None, priority=None): """ Update 'provider' portion of records in DB with success (file_id) or error (exception) @@ -763,12 +763,16 @@ class SyncServerModule(PypeModule, ITrayModule): site (string): label ('gdrive', 'S3') error (string): exception message progress (float): 0-1 of progress of upload/download + priority (int): 0-100 set priority Returns: None """ representation_id = representation.get("_id") - file_id = file.get("_id") + file_id = None + if file: + file_id = file.get("_id") + query = { "_id": representation_id } @@ -780,6 +784,8 @@ class SyncServerModule(PypeModule, ITrayModule): update["$unset"] = self._get_error_dict("", "", "") elif progress is not None: update["$set"] = self._get_progress_dict(progress) + elif priority is not None: + update["$set"] = self._get_priority_dict(priority, file_id) else: tries = self._get_tries_count(file, site) tries += 1 @@ -787,9 +793,10 @@ class SyncServerModule(PypeModule, ITrayModule): update["$set"] = self._get_error_dict(error, tries) arr_filter = [ - {'s.name': site}, - {'f._id': ObjectId(file_id)} + {'s.name': site} ] + if file_id: + arr_filter['f._id'] = ObjectId(file_id) self.connection.database[collection].update_one( query, @@ -798,7 +805,7 @@ class SyncServerModule(PypeModule, ITrayModule): array_filters=arr_filter ) - if progress is not None: + if progress is not None or priority is not None: return status = 'failed' @@ -1192,6 +1199,21 @@ class SyncServerModule(PypeModule, ITrayModule): val = {"files.$[f].sites.$[s].progress": progress} return val + def _get_priority_dict(self, priority, file_id): + """ + Provide priority metadata to be stored in Db. + Used during upload/download for GUI to show. + Args: + priority: (int) - priority for file(s) + Returns: + (dictionary) + """ + if file_id: + str_key = "files.$[f].sites.$[s].priority" + else: + str_key = "files.$[].sites.$[s].priority" + return {str_key: priority} + def _get_retries_arr(self, project_name): """ Returns array with allowed values in 'tries' field. If repre From 81baab037213021cd123b1a2622972a87e6c4f76 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 30 Apr 2021 15:50:40 +0200 Subject: [PATCH 02/14] SyncServer - Pause button shouldn't be default action --- openpype/modules/sync_server/tray/app.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/openpype/modules/sync_server/tray/app.py b/openpype/modules/sync_server/tray/app.py index 2538675c51..ef19d6dbb0 100644 --- a/openpype/modules/sync_server/tray/app.py +++ b/openpype/modules/sync_server/tray/app.py @@ -85,6 +85,8 @@ class SyncServerWindow(QtWidgets.QDialog): self.projects.current_project)) self.pause_btn.clicked.connect(self._pause) + self.pause_btn.setAutoDefault(False) + self.pause_btn.setDefault(False) repres.message_generated.connect(self._update_message) def _pause(self): From ad87c65b75cc0026cbf551758018f8b9faa7f9c8 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Fri, 30 Apr 2021 18:40:40 +0200 Subject: [PATCH 03/14] SyncServer - implemented priority in SummaryView SummaryDetail not implemented Still some kinks to iron out --- openpype/modules/sync_server/tray/lib.py | 57 ++++++++++++++++++- openpype/modules/sync_server/tray/models.py | 58 ++++++++++++++++++-- openpype/modules/sync_server/tray/widgets.py | 26 +++++++++ 3 files changed, 134 insertions(+), 7 deletions(-) diff --git a/openpype/modules/sync_server/tray/lib.py b/openpype/modules/sync_server/tray/lib.py index 04bd1f568e..5e59d653ac 100644 --- a/openpype/modules/sync_server/tray/lib.py +++ b/openpype/modules/sync_server/tray/lib.py @@ -1,9 +1,11 @@ -from Qt import QtCore +from Qt import QtCore, QtWidgets, QtGui +from Qt.QtCore import Qt import attr import abc import six from openpype.lib import PypeLogger +from avalon.vendor import qtawesome log = PypeLogger().get_logger("SyncServer") @@ -164,3 +166,56 @@ def get_item_by_id(model, object_id): index = model.get_index(object_id) item = model.data(index, FullItemRole) return item + + +class PriorityDelegate(QtWidgets.QStyledItemDelegate): + + def paint(self, painter, option, index): + super(PriorityDelegate, self).paint(painter, option, index) + + if option.widget.selectionModel().isSelected(index) or \ + option.state & QtWidgets.QStyle.State_MouseOver: + edit_icon = qtawesome.icon("fa.edit", color="white") + state = QtGui.QIcon.On + mode = QtGui.QIcon.Selected + + icon_side = 16 + icon_rect = QtCore.QRect( + option.rect.left() + option.rect.width() - icon_side - 4, + option.rect.top() + ((option.rect.height() - icon_side) / 2), + icon_side, + icon_side + ) + + edit_icon.paint( + painter, icon_rect, + QtCore.Qt.AlignRight, mode, state + ) + + def createEditor(self, parent, option, index): + editor = PriorityLineEdit( + parent, + option.widget.selectionModel().selectedRows()) + editor.setValidator(QtGui.QIntValidator(0, 1000, self)) + editor.setFocus(True) + return editor + + def setModelData(self, editor, model, index): + for index in editor.selected_idxs: + model.set_priority_data(index, editor.text()) + + +class PriorityLineEdit(QtWidgets.QLineEdit): + """Special LineEdit to consume Enter and store selected indexes""" + def __init__(self, parent=None, selected_idxs=None): + self.selected_idxs = selected_idxs + super(PriorityLineEdit, self).__init__(parent) + + def keyPressEvent(self, event): + result = super(PriorityLineEdit, self).keyPressEvent(event) + if ( + event.key() in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter) + ): + return event.accept() + + return result diff --git a/openpype/modules/sync_server/tray/models.py b/openpype/modules/sync_server/tray/models.py index 8fdd9487a4..9fcaefbe6e 100644 --- a/openpype/modules/sync_server/tray/models.py +++ b/openpype/modules/sync_server/tray/models.py @@ -6,6 +6,7 @@ from Qt import QtCore from Qt.QtCore import Qt from avalon.tools.delegates import pretty_timestamp +from avalon.vendor import qtawesome from openpype.lib import PypeLogger @@ -41,6 +42,8 @@ class _SyncRepresentationModel(QtCore.QAbstractTableModel): PAGE_SIZE = 20 # default page size to query for REFRESH_SEC = 5000 # in seconds, requery DB for new status + DEFAULT_PRIORITY = 50 + @property def dbcon(self): """ @@ -79,6 +82,14 @@ class _SyncRepresentationModel(QtCore.QAbstractTableModel): return self.COLUMN_LABELS[section][0] # return name def get_column(self, index): + """ + Returns info about column + + Args: + index (QModelIndex) + Returns: + (tuple): (COLUMN_NAME: COLUMN_LABEL) + """ return self.COLUMN_LABELS[index] def get_header_index(self, value): @@ -109,7 +120,8 @@ class _SyncRepresentationModel(QtCore.QAbstractTableModel): than single page of records) """ if self.sync_server.is_paused() or \ - self.sync_server.is_project_paused(self.project): + self.sync_server.is_project_paused(self.project) or \ + self.is_editing: return self.refresh_started.emit() self.beginResetModel() @@ -191,7 +203,7 @@ class _SyncRepresentationModel(QtCore.QAbstractTableModel): self.sort = {self.SORT_BY_COLUMN[index]: order} # reset # add last one for key, val in backup_sort.items(): - if key != '_id': + if key != '_id' and key != self.SORT_BY_COLUMN[index]: self.sort[key] = val break # add default one @@ -363,7 +375,7 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel): "updated_dt_remote", # remote created_dt "files_count", # count of files "files_size", # file size of all files - "context.asset", # priority TODO + "priority", # priority "status" # status ] @@ -374,6 +386,8 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel): 'representation': lib.MultiSelectFilter('representation') } + EDITABLE_COLUMNS = ["priority"] + refresh_started = QtCore.Signal() refresh_finished = QtCore.Signal() @@ -413,6 +427,9 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel): self._word_filter = None self._column_filtering = {} + self.edit_icon = qtawesome.icon("fa.edit", color="white") + self.is_editing = False + self._word_filter = None self._initialized = False @@ -472,7 +489,7 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel): return item.status == lib.STATUS[2] and \ item.remote_progress < 1 - if role == Qt.DisplayRole: + if role in (Qt.DisplayRole, Qt.EditRole): # because of ImageDelegate if header_value in ['remote_site', 'local_site']: return "" @@ -549,7 +566,7 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel): avg_progress_remote, repre.get("files_count", 1), lib.pretty_size(repre.get("files_size", 0)), - 1, + repre.get("priority"), lib.STATUS[repre.get("status", -1)], files[0].get('path') ) @@ -668,6 +685,10 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel): '$cond': [{'$size': "$order_local.paused"}, 1, 0]}, + 'priority': { + '$cond': [{'$size': '$order_local.priority'}, + {'$first': '$order_local.priority'}, + self.DEFAULT_PRIORITY]}, }}, {'$group': { '_id': '$_id', @@ -690,7 +711,8 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel): 'failed_local_tries': {'$sum': '$failed_local_tries'}, 'paused_remote': {'$sum': '$paused_remote'}, 'paused_local': {'$sum': '$paused_local'}, - 'updated_dt_local': {'$max': "$updated_dt_local"} + 'updated_dt_local': {'$max': "$updated_dt_local"}, + 'priority': {'$max': "$priority"}, }}, {"$project": self.projection} ] @@ -772,6 +794,7 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel): 'updated_dt_local': 1, 'paused_remote': 1, 'paused_local': 1, + 'priority': 1, 'status': { '$switch': { 'branches': [ @@ -818,6 +841,27 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel): } } + def set_priority_data(self, index, value): + """ + Sets 'priority' flag and value on active site for selected reprs. + + Args: + index (QItemIndex): selected index from View + value (int): priority value + + Updates DB + """ + repre_id = self.data(index, Qt.UserRole) + + representation = list(self.dbcon.find({"type": "representation", + "_id": repre_id})) + if representation: + self.sync_server.update_db(self.project, None, None, + representation.pop(), self.active_site, + priority=value) + self.is_editing = False + self.refresh(None, self._rec_loaded) + class SyncRepresentationDetailModel(_SyncRepresentationModel): """ @@ -901,6 +945,8 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel): self._initialized = False self._column_filtering = {} + self.is_editing = False + self.sync_server = sync_server # TODO think about admin mode # this is for regular user, always only single local and single remote diff --git a/openpype/modules/sync_server/tray/widgets.py b/openpype/modules/sync_server/tray/widgets.py index 106fc4b8a8..838b325c73 100644 --- a/openpype/modules/sync_server/tray/widgets.py +++ b/openpype/modules/sync_server/tray/widgets.py @@ -171,6 +171,13 @@ class _SyncRepresentationWidget(QtWidgets.QWidget): """ Opens representation dialog with all files after doubleclick """ + # priority editing + column_name = self.model.get_column(index.column()) + if column_name[0] in self.model.EDITABLE_COLUMNS: + self.model.is_editing = True + self.table_view.openPersistentEditor(index) + return + _id = self.model.data(index, Qt.UserRole) detail_window = SyncServerDetailWindow( self.sync_server, _id, self.model.project) @@ -253,6 +260,11 @@ class _SyncRepresentationWidget(QtWidgets.QWidget): actions_mapping[action] = self._remove_site menu.addAction(action) + action = QtWidgets.QAction("Change priority") + action_kwarg_map[action] = self._get_action_kwargs(active_site) + actions_mapping[action] = self._change_priority + menu.addAction(action) + # # temp for testing only !!! # action = QtWidgets.QAction("Download") # action_kwarg_map[action] = self._get_action_kwargs(active_site) @@ -397,6 +409,15 @@ class _SyncRepresentationWidget(QtWidgets.QWidget): except OSError: raise OSError('unsupported xdg-open call??') + def _change_priority(self, **kwargs): + """Open editor to change priority on first selected row""" + if self._selected_ids: + index = self.model.get_index(self._selected_ids[0]) # column = 0 + column_no = self.model.get_header_index("priority") # real column + real_index = self.model.index(index.row(), column_no) + self.model.is_editing = True + self.table_view.openPersistentEditor(real_index) + def _get_progress(self, item, site_name, opposite=False): """Returns progress value according to site (side)""" progress = {'local': item.local_progress, @@ -470,6 +491,7 @@ class SyncRepresentationSummaryWidget(_SyncRepresentationWidget): -1, Qt.AscendingOrder) table_view.setAlternatingRowColors(True) table_view.verticalHeader().hide() + table_view.viewport().setAttribute(QtCore.Qt.WA_Hover, True) column = table_view.model().get_header_index("local_site") delegate = ImageDelegate(self) @@ -479,6 +501,10 @@ class SyncRepresentationSummaryWidget(_SyncRepresentationWidget): delegate = ImageDelegate(self) table_view.setItemDelegateForColumn(column, delegate) + column = table_view.model().get_header_index("priority") + priority_delegate = lib.PriorityDelegate(self) + table_view.setItemDelegateForColumn(column, priority_delegate) + layout = QtWidgets.QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) layout.addLayout(top_bar_layout) From 0c631e76f13f9cdf84253b8fcf20e1c329169b9a Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 3 May 2021 14:13:53 +0200 Subject: [PATCH 04/14] SyncServer - implemented priority in synchronization loop --- .../modules/sync_server/sync_server_module.py | 42 ++++++++++++++++--- 1 file changed, 37 insertions(+), 5 deletions(-) diff --git a/openpype/modules/sync_server/sync_server_module.py b/openpype/modules/sync_server/sync_server_module.py index e411366fd9..5ee9d87791 100644 --- a/openpype/modules/sync_server/sync_server_module.py +++ b/openpype/modules/sync_server/sync_server_module.py @@ -83,6 +83,7 @@ class SyncServerModule(PypeModule, ITrayModule): DEFAULT_SITE = 'studio' LOCAL_SITE = 'local' LOG_PROGRESS_SEC = 5 # how often log progress to DB + DEFAULT_PRIORITY = 50 # higher is better, allowed range 1 - 1000 name = "sync_server" label = "Sync Queue" @@ -662,7 +663,7 @@ class SyncServerModule(PypeModule, ITrayModule): self.connection.Session["AVALON_PROJECT"] = collection # retry_cnt - number of attempts to sync specific file before giving up retries_arr = self._get_retries_arr(collection) - query = { + match = { "type": "representation", "$or": [ {"$and": [ @@ -700,10 +701,41 @@ class SyncServerModule(PypeModule, ITrayModule): ]} ] } + + aggr = [ + {"$match": match}, + {'$unwind': '$files'}, + {'$addFields': { + 'order_remote': { + '$filter': {'input': '$files.sites', 'as': 'p', + 'cond': {'$eq': ['$$p.name', remote_site]} + }}, + 'order_local': { + '$filter': {'input': '$files.sites', 'as': 'p', + 'cond': {'$eq': ['$$p.name', active_site]} + }}, + }}, + {'$addFields': { + 'priority': { + '$cond': [{'$size': '$order_local.priority'}, + {'$first': '$order_local.priority'}, + self.DEFAULT_PRIORITY]} + }}, + {'$group': { + '_id': '$_id', + # pass through context - same for representation + 'context': {'$addToSet': '$context'}, + 'data': {'$addToSet': '$data'}, + # pass through files as a list + 'files': {'$addToSet': '$files'}, + 'priority': {'$max': "$priority"}, + }}, + {"$sort": {'priority': -1, '_id': 1}}, + ] log.debug("active_site:{} - remote_site:{}".format(active_site, remote_site)) - log.debug("query: {}".format(query)) - representations = self.connection.find(query) + log.debug("query: {}".format(aggr)) + representations = self.connection.aggregate(aggr) return representations @@ -796,7 +828,7 @@ class SyncServerModule(PypeModule, ITrayModule): {'s.name': site} ] if file_id: - arr_filter['f._id'] = ObjectId(file_id) + arr_filter.append({'f._id': ObjectId(file_id)}) self.connection.database[collection].update_one( query, @@ -1212,7 +1244,7 @@ class SyncServerModule(PypeModule, ITrayModule): str_key = "files.$[f].sites.$[s].priority" else: str_key = "files.$[].sites.$[s].priority" - return {str_key: priority} + return {str_key: int(priority)} def _get_retries_arr(self, project_name): """ From 5dd059d96ee43b83641d32106c4d1907a2c4150e Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 3 May 2021 14:15:44 +0200 Subject: [PATCH 05/14] SyncServer - implemented priority for SyncRepresentationDetailModel --- openpype/modules/sync_server/tray/models.py | 54 ++++++++++++++++++--- 1 file changed, 46 insertions(+), 8 deletions(-) diff --git a/openpype/modules/sync_server/tray/models.py b/openpype/modules/sync_server/tray/models.py index 9fcaefbe6e..ae6d6c569d 100644 --- a/openpype/modules/sync_server/tray/models.py +++ b/openpype/modules/sync_server/tray/models.py @@ -42,7 +42,8 @@ class _SyncRepresentationModel(QtCore.QAbstractTableModel): PAGE_SIZE = 20 # default page size to query for REFRESH_SEC = 5000 # in seconds, requery DB for new status - DEFAULT_PRIORITY = 50 + refresh_started = QtCore.Signal() + refresh_finished = QtCore.Signal() @property def dbcon(self): @@ -688,7 +689,7 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel): 'priority': { '$cond': [{'$size': '$order_local.priority'}, {'$first': '$order_local.priority'}, - self.DEFAULT_PRIORITY]}, + self.sync_server.DEFAULT_PRIORITY]}, }}, {'$group': { '_id': '$_id', @@ -860,7 +861,9 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel): representation.pop(), self.active_site, priority=value) self.is_editing = False - self.refresh(None, self._rec_loaded) + + # all other approaches messed up selection to 0th index + self.timer.setInterval(0) class SyncRepresentationDetailModel(_SyncRepresentationModel): @@ -896,7 +899,7 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel): "updated_dt_local", # local created_dt "updated_dt_remote", # remote created_dt "size", # remote progress - "size", # priority TODO + "priority", # priority "status" # status ] @@ -905,8 +908,7 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel): 'file': lib.RegexTextFilter('file'), } - refresh_started = QtCore.Signal() - refresh_finished = QtCore.Signal() + EDITABLE_COLUMNS = ["priority"] @attr.s class SyncRepresentationDetail: @@ -1072,7 +1074,7 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel): local_progress, remote_progress, lib.pretty_size(file.get('size', 0)), - 1, + repre.get("priority"), lib.STATUS[repre.get("status", -1)], repre.get("tries"), '\n'.join(errors), @@ -1190,7 +1192,11 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel): "$order_remote.tries", [] ]} - ]}} + ]}}, + 'priority': { + '$cond': [{'$size': '$order_local.priority'}, + {'$first': '$order_local.priority'}, + self.sync_server.DEFAULT_PRIORITY]} }}, {"$project": self.projection} ] @@ -1256,6 +1262,7 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel): 'failed_remote_error': 1, 'failed_local_error': 1, 'tries': 1, + 'priority': 1, 'status': { '$switch': { 'branches': [ @@ -1307,3 +1314,34 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel): }, 'data.path': 1 } + + def set_priority_data(self, index, value): + """ + Sets 'priority' flag and value on active site for selected reprs. + + Args: + index (QItemIndex): selected index from View + value (int): priority value + + Updates DB + """ + file_id = self.data(index, Qt.UserRole) + + updated_file = None + # conversion from cursor to list + representations = list(self.dbcon.find({"type": "representation", + "_id": self._id})) + + representation = representations.pop() + for repre_file in representation["files"]: + if repre_file["_id"] == file_id: + updated_file = repre_file + break + + if representation and updated_file: + self.sync_server.update_db(self.project, None, updated_file, + representation, self.active_site, + priority=value) + self.is_editing = False + # all other approaches messed up selection to 0th index + self.timer.setInterval(0) From 4b7cab42fbfb002ec51a57ace3567222179dc207 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 3 May 2021 14:17:50 +0200 Subject: [PATCH 06/14] SyncServer - refactored delegate location --- .../modules/sync_server/tray/delegates.py | 111 +++++++++++++++++ openpype/modules/sync_server/tray/lib.py | 54 --------- openpype/modules/sync_server/tray/widgets.py | 112 +++++------------- 3 files changed, 143 insertions(+), 134 deletions(-) create mode 100644 openpype/modules/sync_server/tray/delegates.py diff --git a/openpype/modules/sync_server/tray/delegates.py b/openpype/modules/sync_server/tray/delegates.py new file mode 100644 index 0000000000..0e694f1f9e --- /dev/null +++ b/openpype/modules/sync_server/tray/delegates.py @@ -0,0 +1,111 @@ +import os +from Qt import QtCore, QtWidgets, QtGui +from avalon.vendor import qtawesome + +from openpype.lib import PypeLogger +from openpype.modules.sync_server.tray import lib + +log = PypeLogger().get_logger("SyncServer") + + +class PriorityDelegate(QtWidgets.QStyledItemDelegate): + """Creates editable line edit to set priority on representation""" + def paint(self, painter, option, index): + super(PriorityDelegate, self).paint(painter, option, index) + + if option.widget.selectionModel().isSelected(index) or \ + option.state & QtWidgets.QStyle.State_MouseOver: + edit_icon = qtawesome.icon("fa.edit", color="white") + state = QtGui.QIcon.On + mode = QtGui.QIcon.Selected + + icon_side = 16 + icon_rect = QtCore.QRect( + option.rect.left() + option.rect.width() - icon_side - 4, + option.rect.top() + ((option.rect.height() - icon_side) / 2), + icon_side, + icon_side + ) + + edit_icon.paint( + painter, icon_rect, + QtCore.Qt.AlignRight, mode, state + ) + + def createEditor(self, parent, option, index): + editor = PriorityLineEdit( + parent, + option.widget.selectionModel().selectedRows()) + editor.setValidator(QtGui.QIntValidator(0, 1000, self)) + editor.setFocus(True) + return editor + + def setModelData(self, editor, model, index): + for index in editor.selected_idxs: + model.set_priority_data(index, editor.text()) + + +class PriorityLineEdit(QtWidgets.QLineEdit): + """Special LineEdit to consume Enter and store selected indexes""" + def __init__(self, parent=None, selected_idxs=None): + self.selected_idxs = selected_idxs + super(PriorityLineEdit, self).__init__(parent) + + def keyPressEvent(self, event): + result = super(PriorityLineEdit, self).keyPressEvent(event) + if ( + event.key() in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter) + ): + return event.accept() + + return result + + +class ImageDelegate(QtWidgets.QStyledItemDelegate): + """ + Prints icon of site and progress of synchronization + """ + + def __init__(self, parent=None): + super(ImageDelegate, self).__init__(parent) + self.icons = {} + + def paint(self, painter, option, index): + super(ImageDelegate, self).paint(painter, option, index) + option = QtWidgets.QStyleOptionViewItem(option) + option.showDecorationSelected = True + + provider = index.data(lib.ProviderRole) + value = index.data(lib.ProgressRole) + date_value = index.data(lib.DateRole) + is_failed = index.data(lib.FailedRole) + + if not self.icons.get(provider): + resource_path = os.path.dirname(__file__) + resource_path = os.path.join(resource_path, "..", + "providers", "resources") + pix_url = "{}/{}.png".format(resource_path, provider) + pixmap = QtGui.QPixmap(pix_url) + self.icons[provider] = pixmap + else: + pixmap = self.icons[provider] + + padding = 10 + point = QtCore.QPoint(option.rect.x() + padding, + option.rect.y() + + (option.rect.height() - pixmap.height()) / 2) + painter.drawPixmap(point, pixmap) + + overlay_rect = option.rect.translated(0, 0) + overlay_rect.setHeight(overlay_rect.height() * (1.0 - float(value))) + painter.fillRect(overlay_rect, + QtGui.QBrush(QtGui.QColor(0, 0, 0, 100))) + text_rect = option.rect.translated(10, 0) + painter.drawText(text_rect, + QtCore.Qt.AlignCenter, + date_value) + + if is_failed: + overlay_rect = option.rect.translated(0, 0) + painter.fillRect(overlay_rect, + QtGui.QBrush(QtGui.QColor(255, 0, 0, 35))) diff --git a/openpype/modules/sync_server/tray/lib.py b/openpype/modules/sync_server/tray/lib.py index 5e59d653ac..49d224d61f 100644 --- a/openpype/modules/sync_server/tray/lib.py +++ b/openpype/modules/sync_server/tray/lib.py @@ -1,11 +1,9 @@ from Qt import QtCore, QtWidgets, QtGui -from Qt.QtCore import Qt import attr import abc import six from openpype.lib import PypeLogger -from avalon.vendor import qtawesome log = PypeLogger().get_logger("SyncServer") @@ -167,55 +165,3 @@ def get_item_by_id(model, object_id): item = model.data(index, FullItemRole) return item - -class PriorityDelegate(QtWidgets.QStyledItemDelegate): - - def paint(self, painter, option, index): - super(PriorityDelegate, self).paint(painter, option, index) - - if option.widget.selectionModel().isSelected(index) or \ - option.state & QtWidgets.QStyle.State_MouseOver: - edit_icon = qtawesome.icon("fa.edit", color="white") - state = QtGui.QIcon.On - mode = QtGui.QIcon.Selected - - icon_side = 16 - icon_rect = QtCore.QRect( - option.rect.left() + option.rect.width() - icon_side - 4, - option.rect.top() + ((option.rect.height() - icon_side) / 2), - icon_side, - icon_side - ) - - edit_icon.paint( - painter, icon_rect, - QtCore.Qt.AlignRight, mode, state - ) - - def createEditor(self, parent, option, index): - editor = PriorityLineEdit( - parent, - option.widget.selectionModel().selectedRows()) - editor.setValidator(QtGui.QIntValidator(0, 1000, self)) - editor.setFocus(True) - return editor - - def setModelData(self, editor, model, index): - for index in editor.selected_idxs: - model.set_priority_data(index, editor.text()) - - -class PriorityLineEdit(QtWidgets.QLineEdit): - """Special LineEdit to consume Enter and store selected indexes""" - def __init__(self, parent=None, selected_idxs=None): - self.selected_idxs = selected_idxs - super(PriorityLineEdit, self).__init__(parent) - - def keyPressEvent(self, event): - result = super(PriorityLineEdit, self).keyPressEvent(event) - if ( - event.key() in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter) - ): - return event.accept() - - return result diff --git a/openpype/modules/sync_server/tray/widgets.py b/openpype/modules/sync_server/tray/widgets.py index 838b325c73..4ca06d3d07 100644 --- a/openpype/modules/sync_server/tray/widgets.py +++ b/openpype/modules/sync_server/tray/widgets.py @@ -23,6 +23,7 @@ from openpype.modules.sync_server.tray.models import ( ) from openpype.modules.sync_server.tray import lib +from openpype.modules.sync_server.tray import delegates log = PypeLogger().get_logger("SyncServer") @@ -145,10 +146,10 @@ class _SyncRepresentationWidget(QtWidgets.QWidget): def _selection_changed(self, _new_selected, _all_selected): idxs = self.selection_model.selectedRows() - self._selected_ids = [] + self._selected_ids = set() for index in idxs: - self._selected_ids.append(self.model.data(index, Qt.UserRole)) + self._selected_ids.add(self.model.data(index, Qt.UserRole)) def _set_selection(self): """ @@ -156,14 +157,14 @@ class _SyncRepresentationWidget(QtWidgets.QWidget): Keep selection during model refresh. """ - existing_ids = [] + existing_ids = set() for selected_id in self._selected_ids: index = self.model.get_index(selected_id) if index and index.isValid(): mode = QtCore.QItemSelectionModel.Select | \ QtCore.QItemSelectionModel.Rows self.selection_model.select(index, mode) - existing_ids.append(selected_id) + existing_ids.add(selected_id) self._selected_ids = existing_ids @@ -196,7 +197,7 @@ class _SyncRepresentationWidget(QtWidgets.QWidget): return if is_multi: - index = self.model.get_index(self._selected_ids[0]) + index = self.model.get_index(list(self._selected_ids)[0]) item = self.model.data(index, lib.FullItemRole) else: item = self.model.data(point_index, lib.FullItemRole) @@ -412,8 +413,9 @@ class _SyncRepresentationWidget(QtWidgets.QWidget): def _change_priority(self, **kwargs): """Open editor to change priority on first selected row""" if self._selected_ids: - index = self.model.get_index(self._selected_ids[0]) # column = 0 - column_no = self.model.get_header_index("priority") # real column + # get_index returns dummy index with column equals to 0 + index = self.model.get_index(list(self._selected_ids)[0]) + column_no = self.model.get_header_index("priority") # real column real_index = self.model.index(index.row(), column_no) self.model.is_editing = True self.table_view.openPersistentEditor(real_index) @@ -462,7 +464,7 @@ class SyncRepresentationSummaryWidget(_SyncRepresentationWidget): self.sync_server = sync_server - self._selected_ids = [] # keep last selected _id + self._selected_ids = set() # keep last selected _id txt_filter = QtWidgets.QLineEdit() txt_filter.setPlaceholderText("Quick filter representations..") @@ -494,15 +496,15 @@ class SyncRepresentationSummaryWidget(_SyncRepresentationWidget): table_view.viewport().setAttribute(QtCore.Qt.WA_Hover, True) column = table_view.model().get_header_index("local_site") - delegate = ImageDelegate(self) + delegate = delegates.ImageDelegate(self) table_view.setItemDelegateForColumn(column, delegate) column = table_view.model().get_header_index("remote_site") - delegate = ImageDelegate(self) + delegate = delegates.ImageDelegate(self) table_view.setItemDelegateForColumn(column, delegate) column = table_view.model().get_header_index("priority") - priority_delegate = lib.PriorityDelegate(self) + priority_delegate = delegates.PriorityDelegate(self) table_view.setItemDelegateForColumn(column, priority_delegate) layout = QtWidgets.QVBoxLayout(self) @@ -624,7 +626,7 @@ class SyncRepresentationDetailWidget(_SyncRepresentationWidget): self.sync_server = sync_server self.representation_id = _id - self._selected_ids = [] + self._selected_ids = set() self.txt_filter = QtWidgets.QLineEdit() self.txt_filter.setPlaceholderText("Quick filter representation..") @@ -654,13 +656,17 @@ class SyncRepresentationDetailWidget(_SyncRepresentationWidget): table_view.verticalHeader().hide() column = model.get_header_index("local_site") - delegate = ImageDelegate(self) + delegate = delegates.ImageDelegate(self) table_view.setItemDelegateForColumn(column, delegate) column = model.get_header_index("remote_site") - delegate = ImageDelegate(self) + delegate = delegates.ImageDelegate(self) table_view.setItemDelegateForColumn(column, delegate) + column = table_view.model().get_header_index("priority") + priority_delegate = delegates.PriorityDelegate(self) + table_view.setItemDelegateForColumn(column, priority_delegate) + layout = QtWidgets.QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) layout.addLayout(top_bar_layout) @@ -684,12 +690,24 @@ class SyncRepresentationDetailWidget(_SyncRepresentationWidget): self.txt_filter.textChanged.connect(lambda: model.set_word_filter( self.txt_filter.text())) + table_view.doubleClicked.connect(self._double_clicked) table_view.customContextMenuRequested.connect(self._on_context_menu) model.refresh_started.connect(self._save_scrollbar) model.refresh_finished.connect(self._set_scrollbar) model.modelReset.connect(self._set_selection) + def _double_clicked(self, index): + """ + Opens representation dialog with all files after doubleclick + """ + # priority editing + column_name = self.model.get_column(index.column()) + if column_name[0] in self.model.EDITABLE_COLUMNS: + self.model.is_editing = True + self.table_view.openPersistentEditor(index) + return + def _show_detail(self, selected_ids=None): """ Shows windows with error message for failed sync of a file. @@ -804,72 +822,6 @@ class SyncRepresentationErrorWidget(QtWidgets.QWidget): layout.addWidget(text_area) -class ImageDelegate(QtWidgets.QStyledItemDelegate): - """ - Prints icon of site and progress of synchronization - """ - - def __init__(self, parent=None): - super(ImageDelegate, self).__init__(parent) - self.icons = {} - - def paint(self, painter, option, index): - super(ImageDelegate, self).paint(painter, option, index) - option = QtWidgets.QStyleOptionViewItem(option) - option.showDecorationSelected = True - - provider = index.data(lib.ProviderRole) - value = index.data(lib.ProgressRole) - date_value = index.data(lib.DateRole) - is_failed = index.data(lib.FailedRole) - - if not self.icons.get(provider): - resource_path = os.path.dirname(__file__) - resource_path = os.path.join(resource_path, "..", - "providers", "resources") - pix_url = "{}/{}.png".format(resource_path, provider) - pixmap = QtGui.QPixmap(pix_url) - self.icons[provider] = pixmap - else: - pixmap = self.icons[provider] - - padding = 10 - point = QtCore.QPoint(option.rect.x() + padding, - option.rect.y() + - (option.rect.height() - pixmap.height()) / 2) - painter.drawPixmap(point, pixmap) - - overlay_rect = option.rect.translated(0, 0) - overlay_rect.setHeight(overlay_rect.height() * (1.0 - float(value))) - painter.fillRect(overlay_rect, - QtGui.QBrush(QtGui.QColor(0, 0, 0, 100))) - text_rect = option.rect.translated(10, 0) - painter.drawText(text_rect, - QtCore.Qt.AlignCenter, - date_value) - - if is_failed: - overlay_rect = option.rect.translated(0, 0) - painter.fillRect(overlay_rect, - QtGui.QBrush(QtGui.QColor(255, 0, 0, 35))) - - -class TransparentWidget(QtWidgets.QWidget): - """Used for header cell for resizing to work properly""" - clicked = QtCore.Signal(str) - - def __init__(self, column_name, *args, **kwargs): - super(TransparentWidget, self).__init__(*args, **kwargs) - self.column_name = column_name - # self.setStyleSheet("background: red;") - - def mouseReleaseEvent(self, event): - if event.button() == QtCore.Qt.LeftButton: - self.clicked.emit(self.column_name) - - super(TransparentWidget, self).mouseReleaseEvent(event) - - class HorizontalHeader(QtWidgets.QHeaderView): """Reiplemented QHeaderView to contain clickable changeable button""" def __init__(self, parent=None): From 1214455db0867d7781237089c0379c0d3349afa6 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 3 May 2021 14:21:00 +0200 Subject: [PATCH 07/14] SyncServer - propagate styles into context menu --- openpype/modules/sync_server/tray/widgets.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openpype/modules/sync_server/tray/widgets.py b/openpype/modules/sync_server/tray/widgets.py index 4ca06d3d07..c3b6684484 100644 --- a/openpype/modules/sync_server/tray/widgets.py +++ b/openpype/modules/sync_server/tray/widgets.py @@ -95,7 +95,7 @@ class SyncProjectListWidget(ProjectListWidget): self.project_name = point_index.data(QtCore.Qt.DisplayRole) - menu = QtWidgets.QMenu() + menu = QtWidgets.QMenu(self) actions_mapping = {} if self.sync_server.is_project_paused(self.project_name): @@ -215,7 +215,7 @@ class _SyncRepresentationWidget(QtWidgets.QWidget): self.model.refresh() def _prepare_menu(self, item, is_multi): - menu = QtWidgets.QMenu() + menu = QtWidgets.QMenu(self) actions_mapping = {} action_kwarg_map = {} From 7887c298cc6a10dac9af48b8f4350c3aab42dabc Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 3 May 2021 18:25:29 +0200 Subject: [PATCH 08/14] SyncServer - pausing only synchronization, not GUI --- openpype/modules/sync_server/tray/models.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/openpype/modules/sync_server/tray/models.py b/openpype/modules/sync_server/tray/models.py index ae6d6c569d..361baac6ee 100644 --- a/openpype/modules/sync_server/tray/models.py +++ b/openpype/modules/sync_server/tray/models.py @@ -120,9 +120,7 @@ class _SyncRepresentationModel(QtCore.QAbstractTableModel): actually queried (scrolled a couple of times to list more than single page of records) """ - if self.sync_server.is_paused() or \ - self.sync_server.is_project_paused(self.project) or \ - self.is_editing: + if self.is_editing: return self.refresh_started.emit() self.beginResetModel() From b559f29d946684e3c59ea5586e19fd4b94c7419f Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 3 May 2021 18:57:04 +0200 Subject: [PATCH 09/14] SyncServer - allow edit option only if site is local If no site is user local one, do not allow edit actions (Reset site, set priority) Potentially extensible by admin role --- openpype/modules/sync_server/tray/models.py | 6 ++ openpype/modules/sync_server/tray/widgets.py | 87 +++++++++++--------- 2 files changed, 55 insertions(+), 38 deletions(-) diff --git a/openpype/modules/sync_server/tray/models.py b/openpype/modules/sync_server/tray/models.py index 361baac6ee..353be92e70 100644 --- a/openpype/modules/sync_server/tray/models.py +++ b/openpype/modules/sync_server/tray/models.py @@ -9,6 +9,7 @@ from avalon.tools.delegates import pretty_timestamp from avalon.vendor import qtawesome from openpype.lib import PypeLogger +from openpype.api import get_local_site_id from openpype.modules.sync_server.tray import lib @@ -82,6 +83,11 @@ class _SyncRepresentationModel(QtCore.QAbstractTableModel): if orientation == Qt.Horizontal: return self.COLUMN_LABELS[section][0] # return name + @property + def can_edit(self): + """Returns true if some site is user local site, eg. could edit""" + return get_local_site_id() in (self.active_site, self.remote_site) + def get_column(self, index): """ Returns info about column diff --git a/openpype/modules/sync_server/tray/widgets.py b/openpype/modules/sync_server/tray/widgets.py index c3b6684484..24553f974b 100644 --- a/openpype/modules/sync_server/tray/widgets.py +++ b/openpype/modules/sync_server/tray/widgets.py @@ -98,13 +98,16 @@ class SyncProjectListWidget(ProjectListWidget): menu = QtWidgets.QMenu(self) actions_mapping = {} - if self.sync_server.is_project_paused(self.project_name): - action = QtWidgets.QAction("Unpause") - actions_mapping[action] = self._unpause - else: - action = QtWidgets.QAction("Pause") - actions_mapping[action] = self._pause - menu.addAction(action) + can_edit = self.model.can_edit + + if can_edit: + if self.sync_server.is_project_paused(self.project_name): + action = QtWidgets.QAction("Unpause") + actions_mapping[action] = self._unpause + else: + action = QtWidgets.QAction("Pause") + actions_mapping[action] = self._pause + menu.addAction(action) if self.local_site == get_local_site_id(): action = QtWidgets.QAction("Clear local project") @@ -173,11 +176,12 @@ class _SyncRepresentationWidget(QtWidgets.QWidget): Opens representation dialog with all files after doubleclick """ # priority editing - column_name = self.model.get_column(index.column()) - if column_name[0] in self.model.EDITABLE_COLUMNS: - self.model.is_editing = True - self.table_view.openPersistentEditor(index) - return + if self.model.can_edit: + column_name = self.model.get_column(index.column()) + if column_name[0] in self.model.EDITABLE_COLUMNS: + self.model.is_editing = True + self.table_view.openPersistentEditor(index) + return _id = self.model.data(index, Qt.UserRole) detail_window = SyncServerDetailWindow( @@ -202,8 +206,10 @@ class _SyncRepresentationWidget(QtWidgets.QWidget): else: item = self.model.data(point_index, lib.FullItemRole) + can_edit = self.model.can_edit action_kwarg_map, actions_mapping, menu = self._prepare_menu(item, - is_multi) + is_multi, + can_edit) result = menu.exec_(QtGui.QCursor.pos()) if result: @@ -214,7 +220,7 @@ class _SyncRepresentationWidget(QtWidgets.QWidget): self.model.refresh() - def _prepare_menu(self, item, is_multi): + def _prepare_menu(self, item, is_multi, can_edit): menu = QtWidgets.QMenu(self) actions_mapping = {} @@ -243,28 +249,29 @@ class _SyncRepresentationWidget(QtWidgets.QWidget): self._get_action_kwargs(site) menu.addAction(action) - if remote_progress == 1.0 or is_multi: + if can_edit and (remote_progress == 1.0 or is_multi): action = QtWidgets.QAction("Re-sync Active site") action_kwarg_map[action] = self._get_action_kwargs(active_site) actions_mapping[action] = self._reset_site menu.addAction(action) - if local_progress == 1.0 or is_multi: + if can_edit and (local_progress == 1.0 or is_multi): action = QtWidgets.QAction("Re-sync Remote site") action_kwarg_map[action] = self._get_action_kwargs(remote_site) actions_mapping[action] = self._reset_site menu.addAction(action) - if active_site == get_local_site_id(): + if can_edit and active_site == get_local_site_id(): action = QtWidgets.QAction("Completely remove from local") action_kwarg_map[action] = self._get_action_kwargs(active_site) actions_mapping[action] = self._remove_site menu.addAction(action) - action = QtWidgets.QAction("Change priority") - action_kwarg_map[action] = self._get_action_kwargs(active_site) - actions_mapping[action] = self._change_priority - menu.addAction(action) + if can_edit: + action = QtWidgets.QAction("Change priority") + action_kwarg_map[action] = self._get_action_kwargs(active_site) + actions_mapping[action] = self._change_priority + menu.addAction(action) # # temp for testing only !!! # action = QtWidgets.QAction("Download") @@ -503,9 +510,10 @@ class SyncRepresentationSummaryWidget(_SyncRepresentationWidget): delegate = delegates.ImageDelegate(self) table_view.setItemDelegateForColumn(column, delegate) - column = table_view.model().get_header_index("priority") - priority_delegate = delegates.PriorityDelegate(self) - table_view.setItemDelegateForColumn(column, priority_delegate) + if model.can_edit: + column = table_view.model().get_header_index("priority") + priority_delegate = delegates.PriorityDelegate(self) + table_view.setItemDelegateForColumn(column, priority_delegate) layout = QtWidgets.QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) @@ -536,18 +544,19 @@ class SyncRepresentationSummaryWidget(_SyncRepresentationWidget): self.selection_model = self.table_view.selectionModel() self.selection_model.selectionChanged.connect(self._selection_changed) - def _prepare_menu(self, item, is_multi): + def _prepare_menu(self, item, is_multi, can_edit): action_kwarg_map, actions_mapping, menu = \ - super()._prepare_menu(item, is_multi) + super()._prepare_menu(item, is_multi, can_edit) - if item.status in [lib.STATUS[0], lib.STATUS[1]] or is_multi: + if can_edit and ( + item.status in [lib.STATUS[0], lib.STATUS[1]] or is_multi): action = QtWidgets.QAction("Pause in queue") actions_mapping[action] = self._pause # pause handles which site_name it will pause itself action_kwarg_map[action] = {"selected_ids": self._selected_ids} menu.addAction(action) - if item.status == lib.STATUS[3] or is_multi: + if can_edit and (item.status == lib.STATUS[3] or is_multi): action = QtWidgets.QAction("Unpause in queue") actions_mapping[action] = self._unpause action_kwarg_map[action] = {"selected_ids": self._selected_ids} @@ -663,9 +672,10 @@ class SyncRepresentationDetailWidget(_SyncRepresentationWidget): delegate = delegates.ImageDelegate(self) table_view.setItemDelegateForColumn(column, delegate) - column = table_view.model().get_header_index("priority") - priority_delegate = delegates.PriorityDelegate(self) - table_view.setItemDelegateForColumn(column, priority_delegate) + if model.can_edit: + column = table_view.model().get_header_index("priority") + priority_delegate = delegates.PriorityDelegate(self) + table_view.setItemDelegateForColumn(column, priority_delegate) layout = QtWidgets.QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) @@ -702,11 +712,12 @@ class SyncRepresentationDetailWidget(_SyncRepresentationWidget): Opens representation dialog with all files after doubleclick """ # priority editing - column_name = self.model.get_column(index.column()) - if column_name[0] in self.model.EDITABLE_COLUMNS: - self.model.is_editing = True - self.table_view.openPersistentEditor(index) - return + if self.model.can_edit: + column_name = self.model.get_column(index.column()) + if column_name[0] in self.model.EDITABLE_COLUMNS: + self.model.is_editing = True + self.table_view.openPersistentEditor(index) + return def _show_detail(self, selected_ids=None): """ @@ -716,10 +727,10 @@ class SyncRepresentationDetailWidget(_SyncRepresentationWidget): detail_window.exec() - def _prepare_menu(self, item, is_multi): + def _prepare_menu(self, item, is_multi, can_edit): """Adds view (and model) dependent actions to default ones""" action_kwarg_map, actions_mapping, menu = \ - super()._prepare_menu(item, is_multi) + super()._prepare_menu(item, is_multi, can_edit) if item.status == lib.STATUS[2] or is_multi: action = QtWidgets.QAction("Open error detail") From ae27361657373f44121bcfac9e5a73a9642ddeb9 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 3 May 2021 19:08:10 +0200 Subject: [PATCH 10/14] SyncServer - allow modifying of priority only on local site --- openpype/modules/sync_server/tray/models.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/openpype/modules/sync_server/tray/models.py b/openpype/modules/sync_server/tray/models.py index 353be92e70..d05035616a 100644 --- a/openpype/modules/sync_server/tray/models.py +++ b/openpype/modules/sync_server/tray/models.py @@ -848,21 +848,27 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel): def set_priority_data(self, index, value): """ - Sets 'priority' flag and value on active site for selected reprs. + Sets 'priority' flag and value on local site for selected reprs. Args: index (QItemIndex): selected index from View value (int): priority value - Updates DB + Updates DB. + Potentially should allow set priority to any site when user + management is implemented. """ + if not self.can_edit: + return + repre_id = self.data(index, Qt.UserRole) representation = list(self.dbcon.find({"type": "representation", "_id": repre_id})) if representation: self.sync_server.update_db(self.project, None, None, - representation.pop(), self.active_site, + representation.pop(), + get_local_site_id(), priority=value) self.is_editing = False @@ -1321,7 +1327,7 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel): def set_priority_data(self, index, value): """ - Sets 'priority' flag and value on active site for selected reprs. + Sets 'priority' flag and value on local site for selected reprs. Args: index (QItemIndex): selected index from View @@ -1329,6 +1335,9 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel): Updates DB """ + if not self.can_edit: + return + file_id = self.data(index, Qt.UserRole) updated_file = None @@ -1344,7 +1353,7 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel): if representation and updated_file: self.sync_server.update_db(self.project, None, updated_file, - representation, self.active_site, + representation, get_local_site_id(), priority=value) self.is_editing = False # all other approaches messed up selection to 0th index From 5d40c9e31e812236d479073e6a0f8b9b2be21c20 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Mon, 3 May 2021 19:09:13 +0200 Subject: [PATCH 11/14] Hound --- openpype/modules/sync_server/tray/lib.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/openpype/modules/sync_server/tray/lib.py b/openpype/modules/sync_server/tray/lib.py index 49d224d61f..04bd1f568e 100644 --- a/openpype/modules/sync_server/tray/lib.py +++ b/openpype/modules/sync_server/tray/lib.py @@ -1,4 +1,4 @@ -from Qt import QtCore, QtWidgets, QtGui +from Qt import QtCore import attr import abc import six @@ -164,4 +164,3 @@ def get_item_by_id(model, object_id): index = model.get_index(object_id) item = model.data(index, FullItemRole) return item - From 999e1b78fda21119bdc7d30967d5fe61d2121f0a Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 4 May 2021 10:43:48 +0200 Subject: [PATCH 12/14] SyncServer - fix priority on remote sites --- .../modules/sync_server/sync_server_module.py | 12 +++++++--- .../modules/sync_server/tray/delegates.py | 7 ++++-- openpype/modules/sync_server/tray/models.py | 24 ++++++++++++++----- 3 files changed, 32 insertions(+), 11 deletions(-) diff --git a/openpype/modules/sync_server/sync_server_module.py b/openpype/modules/sync_server/sync_server_module.py index 5ee9d87791..a720d7d989 100644 --- a/openpype/modules/sync_server/sync_server_module.py +++ b/openpype/modules/sync_server/sync_server_module.py @@ -717,9 +717,15 @@ class SyncServerModule(PypeModule, ITrayModule): }}, {'$addFields': { 'priority': { - '$cond': [{'$size': '$order_local.priority'}, - {'$first': '$order_local.priority'}, - self.DEFAULT_PRIORITY]} + '$cond': [ + {'$size': '$order_local.priority'}, + {'$first': '$order_local.priority'}, + {'$cond': [ + {'$size': '$order_remote.priority'}, + {'$first': '$order_remote.priority'}, + self.DEFAULT_PRIORITY]} + ] + }, }}, {'$group': { '_id': '$_id', diff --git a/openpype/modules/sync_server/tray/delegates.py b/openpype/modules/sync_server/tray/delegates.py index 0e694f1f9e..72d7ec836e 100644 --- a/openpype/modules/sync_server/tray/delegates.py +++ b/openpype/modules/sync_server/tray/delegates.py @@ -36,13 +36,16 @@ class PriorityDelegate(QtWidgets.QStyledItemDelegate): editor = PriorityLineEdit( parent, option.widget.selectionModel().selectedRows()) - editor.setValidator(QtGui.QIntValidator(0, 1000, self)) editor.setFocus(True) return editor def setModelData(self, editor, model, index): for index in editor.selected_idxs: - model.set_priority_data(index, editor.text()) + try: + val = int(editor.text()) + except ValueError: + val = model.sync_server.DEFAULT_PRIORITY + model.set_priority_data(index, val) class PriorityLineEdit(QtWidgets.QLineEdit): diff --git a/openpype/modules/sync_server/tray/models.py b/openpype/modules/sync_server/tray/models.py index d05035616a..a5d2e5737d 100644 --- a/openpype/modules/sync_server/tray/models.py +++ b/openpype/modules/sync_server/tray/models.py @@ -691,9 +691,15 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel): 1, 0]}, 'priority': { - '$cond': [{'$size': '$order_local.priority'}, - {'$first': '$order_local.priority'}, - self.sync_server.DEFAULT_PRIORITY]}, + '$cond': [ + {'$size': '$order_local.priority'}, + {'$first': '$order_local.priority'}, + {'$cond': [ + {'$size': '$order_remote.priority'}, + {'$first': '$order_remote.priority'}, + self.sync_server.DEFAULT_PRIORITY]} + ] + }, }}, {'$group': { '_id': '$_id', @@ -1204,9 +1210,15 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel): ]} ]}}, 'priority': { - '$cond': [{'$size': '$order_local.priority'}, - {'$first': '$order_local.priority'}, - self.sync_server.DEFAULT_PRIORITY]} + '$cond': [ + {'$size': '$order_local.priority'}, + {'$first': '$order_local.priority'}, + {'$cond': [ + {'$size': '$order_remote.priority'}, + {'$first': '$order_remote.priority'}, + self.sync_server.DEFAULT_PRIORITY]} + ] + }, }}, {"$project": self.projection} ] From bf9151103ba416812c436f77294492b2e6de785a Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 4 May 2021 14:25:54 +0200 Subject: [PATCH 13/14] SyncServer - handle querying only when widget is shown --- .../modules/sync_server/sync_server_module.py | 1 + openpype/modules/sync_server/tray/app.py | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/openpype/modules/sync_server/sync_server_module.py b/openpype/modules/sync_server/sync_server_module.py index a720d7d989..5645cdfbec 100644 --- a/openpype/modules/sync_server/sync_server_module.py +++ b/openpype/modules/sync_server/sync_server_module.py @@ -473,6 +473,7 @@ class SyncServerModule(PypeModule, ITrayModule): try: self.sync_server_thread = SyncServerThread(self) + from .tray.app import SyncServerWindow self.widget = SyncServerWindow(self) except ValueError: diff --git a/openpype/modules/sync_server/tray/app.py b/openpype/modules/sync_server/tray/app.py index ef19d6dbb0..b3b6f0a6c3 100644 --- a/openpype/modules/sync_server/tray/app.py +++ b/openpype/modules/sync_server/tray/app.py @@ -89,6 +89,22 @@ class SyncServerWindow(QtWidgets.QDialog): self.pause_btn.setDefault(False) repres.message_generated.connect(self._update_message) + self.representationWidget = repres + + def showEvent(self, event): + self.representationWidget.model.set_project( + self.projects.current_project) + self._set_running(True) + super().showEvent(event) + + def closeEvent(self, event): + self._set_running(False) + super().closeEvent(event) + + def _set_running(self, running): + self.representationWidget.model.is_running = running + self.representationWidget.model.timer.setInterval(0) + def _pause(self): if self.sync_server.is_paused(): self.sync_server.unpause_server() From 8b1a32b2e2d3d7e80b7985d0a10668a032b387c1 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 4 May 2021 14:27:16 +0200 Subject: [PATCH 14/14] SyncServer - fix icon showing Fixed better reaction on changes in Local Settings --- .../modules/sync_server/tray/delegates.py | 6 ++-- openpype/modules/sync_server/tray/lib.py | 1 + openpype/modules/sync_server/tray/models.py | 32 +++++++++++++++---- openpype/modules/sync_server/tray/widgets.py | 14 ++++---- 4 files changed, 39 insertions(+), 14 deletions(-) diff --git a/openpype/modules/sync_server/tray/delegates.py b/openpype/modules/sync_server/tray/delegates.py index 72d7ec836e..9316ec2c3e 100644 --- a/openpype/modules/sync_server/tray/delegates.py +++ b/openpype/modules/sync_server/tray/delegates.py @@ -1,6 +1,5 @@ import os from Qt import QtCore, QtWidgets, QtGui -from avalon.vendor import qtawesome from openpype.lib import PypeLogger from openpype.modules.sync_server.tray import lib @@ -15,7 +14,10 @@ class PriorityDelegate(QtWidgets.QStyledItemDelegate): if option.widget.selectionModel().isSelected(index) or \ option.state & QtWidgets.QStyle.State_MouseOver: - edit_icon = qtawesome.icon("fa.edit", color="white") + edit_icon = index.data(lib.EditIconRole) + if not edit_icon: + return + state = QtGui.QIcon.On mode = QtGui.QIcon.Selected diff --git a/openpype/modules/sync_server/tray/lib.py b/openpype/modules/sync_server/tray/lib.py index 04bd1f568e..c1f8eaf629 100644 --- a/openpype/modules/sync_server/tray/lib.py +++ b/openpype/modules/sync_server/tray/lib.py @@ -25,6 +25,7 @@ DateRole = QtCore.Qt.UserRole + 6 FailedRole = QtCore.Qt.UserRole + 8 HeaderNameRole = QtCore.Qt.UserRole + 10 FullItemRole = QtCore.Qt.UserRole + 12 +EditIconRole = QtCore.Qt.UserRole + 14 @six.add_metaclass(abc.ABCMeta) diff --git a/openpype/modules/sync_server/tray/models.py b/openpype/modules/sync_server/tray/models.py index a5d2e5737d..efef039b8b 100644 --- a/openpype/modules/sync_server/tray/models.py +++ b/openpype/modules/sync_server/tray/models.py @@ -65,6 +65,14 @@ class _SyncRepresentationModel(QtCore.QAbstractTableModel): def column_filtering(self): return self._column_filtering + @property + def is_running(self): + return self._is_running + + @is_running.setter + def is_running(self, state): + self._is_running = state + def rowCount(self, _index): return len(self._data) @@ -126,7 +134,7 @@ class _SyncRepresentationModel(QtCore.QAbstractTableModel): actually queried (scrolled a couple of times to list more than single page of records) """ - if self.is_editing: + if self.is_editing or not self.is_running: return self.refresh_started.emit() self.beginResetModel() @@ -422,8 +430,8 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel): status = attr.ib(default=None) path = attr.ib(default=None) - def __init__(self, sync_server, header, project=None): - super(SyncRepresentationSummaryModel, self).__init__() + def __init__(self, sync_server, header, project=None, parent=None): + super(SyncRepresentationSummaryModel, self).__init__(parent=parent) self._header = header self._data = [] self._project = project @@ -431,13 +439,13 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel): self._total_records = 0 # how many documents query actually found self._word_filter = None self._column_filtering = {} + self._is_running = False self.edit_icon = qtawesome.icon("fa.edit", color="white") self.is_editing = False self._word_filter = None - self._initialized = False if not self._project or self._project == lib.DUMMY_PROJECT: return @@ -500,6 +508,11 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel): return "" return attr.asdict(item)[self._header[index.column()]] + + if role == lib.EditIconRole: + if self.can_edit and header_value in self.EDITABLE_COLUMNS: + return self.edit_icon + if role == Qt.UserRole: return item._id @@ -960,10 +973,11 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel): self._total_records = 0 # how many documents query actually found self._word_filter = None self._id = _id - self._initialized = False self._column_filtering = {} + self._is_running = False self.is_editing = False + self.edit_icon = qtawesome.icon("fa.edit", color="white") self.sync_server = sync_server # TODO think about admin mode @@ -1016,11 +1030,17 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel): return item.status == lib.STATUS[2] and \ item.remote_progress < 1 - if role == Qt.DisplayRole: + if role in (Qt.DisplayRole, Qt.EditRole): # because of ImageDelegate if header_value in ['remote_site', 'local_site']: return "" + return attr.asdict(item)[self._header[index.column()]] + + if role == lib.EditIconRole: + if self.can_edit and header_value in self.EDITABLE_COLUMNS: + return self.edit_icon + if role == Qt.UserRole: return item._id diff --git a/openpype/modules/sync_server/tray/widgets.py b/openpype/modules/sync_server/tray/widgets.py index 24553f974b..e80f91e09f 100644 --- a/openpype/modules/sync_server/tray/widgets.py +++ b/openpype/modules/sync_server/tray/widgets.py @@ -185,7 +185,7 @@ class _SyncRepresentationWidget(QtWidgets.QWidget): _id = self.model.data(index, Qt.UserRole) detail_window = SyncServerDetailWindow( - self.sync_server, _id, self.model.project) + self.sync_server, _id, self.model.project, parent=self) detail_window.exec() def _on_context_menu(self, point): @@ -489,7 +489,8 @@ class SyncRepresentationSummaryWidget(_SyncRepresentationWidget): table_view = QtWidgets.QTableView() headers = [item[0] for item in self.default_widths] - model = SyncRepresentationSummaryModel(sync_server, headers, project) + model = SyncRepresentationSummaryModel(sync_server, headers, project, + parent=self) table_view.setModel(model) table_view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) table_view.setSelectionMode( @@ -510,10 +511,9 @@ class SyncRepresentationSummaryWidget(_SyncRepresentationWidget): delegate = delegates.ImageDelegate(self) table_view.setItemDelegateForColumn(column, delegate) - if model.can_edit: - column = table_view.model().get_header_index("priority") - priority_delegate = delegates.PriorityDelegate(self) - table_view.setItemDelegateForColumn(column, priority_delegate) + column = table_view.model().get_header_index("priority") + priority_delegate = delegates.PriorityDelegate(self) + table_view.setItemDelegateForColumn(column, priority_delegate) layout = QtWidgets.QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) @@ -653,6 +653,8 @@ class SyncRepresentationDetailWidget(_SyncRepresentationWidget): model = SyncRepresentationDetailModel(sync_server, headers, _id, project) + model.is_running = True + table_view.setModel(model) table_view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) table_view.setSelectionMode(