From 5b907a9d661ea376fba05de86cd29b338aa21fc5 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 15 Apr 2021 15:39:55 +0200 Subject: [PATCH 1/9] Initial version of column filtering --- openpype/modules/sync_server/tray/lib.py | 7 + openpype/modules/sync_server/tray/models.py | 140 +++++++++++++---- openpype/modules/sync_server/tray/widgets.py | 155 ++++++++++++++++++- 3 files changed, 262 insertions(+), 40 deletions(-) diff --git a/openpype/modules/sync_server/tray/lib.py b/openpype/modules/sync_server/tray/lib.py index 0282d79ea1..051567ed6c 100644 --- a/openpype/modules/sync_server/tray/lib.py +++ b/openpype/modules/sync_server/tray/lib.py @@ -1,4 +1,5 @@ from Qt import QtCore +import attr from openpype.lib import PypeLogger @@ -20,8 +21,14 @@ ProviderRole = QtCore.Qt.UserRole + 2 ProgressRole = QtCore.Qt.UserRole + 4 DateRole = QtCore.Qt.UserRole + 6 FailedRole = QtCore.Qt.UserRole + 8 +HeaderNameRole = QtCore.Qt.UserRole + 10 +@attr.s +class FilterDefinition: + type = attr.ib() + values = attr.ib(factory=list) + def pretty_size(value, suffix='B'): for unit in ['', 'Ki', 'Mi', 'Gi', 'Ti', 'Pi', 'Ei', 'Zi']: if abs(value) < 1024.0: diff --git a/openpype/modules/sync_server/tray/models.py b/openpype/modules/sync_server/tray/models.py index 3cc53c6ec4..444422c56a 100644 --- a/openpype/modules/sync_server/tray/models.py +++ b/openpype/modules/sync_server/tray/models.py @@ -56,6 +56,10 @@ class _SyncRepresentationModel(QtCore.QAbstractTableModel): """Returns project""" return self._project + @property + def column_filtering(self): + return self._column_filtering + def rowCount(self, _index): return len(self._data) @@ -65,7 +69,15 @@ class _SyncRepresentationModel(QtCore.QAbstractTableModel): def headerData(self, section, orientation, role): if role == Qt.DisplayRole: if orientation == Qt.Horizontal: - return self.COLUMN_LABELS[section][1] + name = self.COLUMN_LABELS[section][0] + txt = "" + if name in self.column_filtering.keys(): + txt = "(F)" + return self.COLUMN_LABELS[section][1] + txt # return label + + if role == lib.HeaderNameRole: + if orientation == Qt.Horizontal: + return self.COLUMN_LABELS[section][0] # return name def get_header_index(self, value): """ @@ -190,6 +202,35 @@ class _SyncRepresentationModel(QtCore.QAbstractTableModel): self.word_filter = word_filter self.refresh() + def get_column_filter_values(self, index): + """ + Returns list of available values for filtering in the column + + Args: + index(int): index of column in header + + Returns: + (dict) of value: label shown in filtering menu + 'value' is used in MongoDB query, 'label' is human readable for + menu + for some columns ('subset') might be 'value' and 'label' same + """ + column_name = self._header[index] + + filter_def = self.COLUMN_FILTERS.get(column_name) + if not filter_def: + return {} + + if filter_def['type'] == 'predefined_set': + return dict(filter_def['values']) + elif filter_def['type'] == 'available_values': + recs = self.dbcon.find({'type': column_name}, {"name": 1, + "_id": -1}) + values = {} + for item in recs: + values[item["name"]] = item["name"] + return dict(sorted(values.items(), key=lambda item: item[1])) + def set_project(self, project): """ Changes project, called after project selection is changed @@ -251,7 +292,7 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel): ("files_count", "Files"), ("files_size", "Size"), ("priority", "Priority"), - ("state", "Status") + ("status", "Status") ] DEFAULT_SORT = { @@ -259,18 +300,26 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel): "_id": 1 } SORT_BY_COLUMN = [ - "context.asset", # asset - "context.subset", # subset - "context.version", # version - "context.representation", # representation + "asset", # asset + "subset", # subset + "version", # version + "representation", # representation "updated_dt_local", # local created_dt "updated_dt_remote", # remote created_dt "files_count", # count of files "files_size", # file size of all files "context.asset", # priority TODO - "status" # state + "status" # status ] + COLUMN_FILTERS = { + 'status': {'type': 'predefined_set', + 'values': {k: v for k, v in lib.STATUS.items()}}, + 'subset': {'type': 'available_values'}, + 'asset': {'type': 'available_values'}, + 'representation': {'type': 'available_values'} + } + refresh_started = QtCore.Signal() refresh_finished = QtCore.Signal() @@ -297,7 +346,7 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel): files_count = attr.ib(default=None) files_size = attr.ib(default=None) priority = attr.ib(default=None) - state = attr.ib(default=None) + status = attr.ib(default=None) path = attr.ib(default=None) def __init__(self, sync_server, header, project=None): @@ -308,6 +357,7 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel): self._rec_loaded = 0 self._total_records = 0 # how many documents query actually found self.word_filter = None + self._column_filtering = {} self._initialized = False if not self._project or self._project == lib.DUMMY_PROJECT: @@ -359,9 +409,9 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel): if role == lib.FailedRole: if header_value == 'local_site': - return item.state == lib.STATUS[2] and item.local_progress < 1 + return item.status == lib.STATUS[2] and item.local_progress < 1 if header_value == 'remote_site': - return item.state == lib.STATUS[2] and item.remote_progress < 1 + return item.status == lib.STATUS[2] and item.remote_progress < 1 if role == Qt.DisplayRole: # because of ImageDelegate @@ -397,7 +447,6 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel): remote_site) for repre in result.get("paginatedResults"): - context = repre.get("context").pop() files = repre.get("files", []) if isinstance(files, dict): # aggregate returns dictionary files = [files] @@ -420,17 +469,17 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel): avg_progress_local = lib.convert_progress( repre.get('avg_progress_local', '0')) - if context.get("version"): - version = "v{:0>3d}".format(context.get("version")) + if repre.get("version"): + version = "v{:0>3d}".format(repre.get("version")) else: version = "master" item = self.SyncRepresentation( repre.get("_id"), - context.get("asset"), - context.get("subset"), + repre.get("asset"), + repre.get("subset"), version, - context.get("representation"), + repre.get("representation"), local_updated, remote_updated, local_site, @@ -461,7 +510,7 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel): 'sync_dt' - same for remote side 'local_site' - progress of repr on local side, 1 = finished 'remote_site' - progress on remote side, calculates from files - 'state' - + 'status' - 0 - in progress 1 - failed 2 - queued @@ -481,7 +530,7 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel): if limit == 0: limit = SyncRepresentationSummaryModel.PAGE_SIZE - return [ + aggr = [ {"$match": self.get_match_part()}, {'$unwind': '$files'}, # merge potentially unwinded records back to single per repre @@ -584,16 +633,43 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel): 'paused_local': {'$sum': '$paused_local'}, 'updated_dt_local': {'$max': "$updated_dt_local"} }}, - {"$project": self.projection}, - {"$sort": self.sort}, - { + {"$project": self.projection} + ] + + if self.column_filtering: + aggr.append( + {"$match": self.column_filtering} + ) + print(self.column_filtering) + + aggr.extend( + [{"$sort": self.sort}, + { '$facet': { 'paginatedResults': [{'$skip': self._rec_loaded}, {'$limit': limit}], 'totalCount': [{'$count': 'count'}] } - } - ] + }] + ) + + return aggr + + def set_column_filtering(self, checked_values): + """ + Sets dictionary used in '$match' part of MongoDB aggregate + + Args: + checked_values(dict): key:values ({'status':{1:"Foo",3:"Bar"}} + + Modifies: + self._column_filtering : {'status': {'$in': [1, 2, 3]}} + """ + filtering = {} + for key, dict_value in checked_values.items(): + filtering[key] = {'$in': list(dict_value.keys())} + + self._column_filtering = filtering def get_match_part(self): """ @@ -639,10 +715,10 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel): (dict) """ return { - "context.subset": 1, - "context.asset": 1, - "context.version": 1, - "context.representation": 1, + "subset": {"$first": "$context.subset"}, + "asset": {"$first": "$context.asset"}, + "version": {"$first": "$context.version"}, + "representation": {"$first": "$context.representation"}, "data.path": 1, "files": 1, 'files_count': 1, @@ -721,7 +797,7 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel): ("remote_site", "Remote site"), ("files_size", "Size"), ("priority", "Priority"), - ("state", "Status") + ("status", "Status") ] PAGE_SIZE = 30 @@ -734,7 +810,7 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel): "updated_dt_remote", # remote created_dt "size", # remote progress "context.asset", # priority TODO - "status" # state + "status" # status ] refresh_started = QtCore.Signal() @@ -759,7 +835,7 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel): remote_progress = attr.ib(default=None) size = attr.ib(default=None) priority = attr.ib(default=None) - state = attr.ib(default=None) + status = attr.ib(default=None) tries = attr.ib(default=None) error = attr.ib(default=None) path = attr.ib(default=None) @@ -821,9 +897,9 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel): if role == lib.FailedRole: if header_value == 'local_site': - return item.state == lib.STATUS[2] and item.local_progress < 1 + return item.status == lib.STATUS[2] and item.local_progress < 1 if header_value == 'remote_site': - return item.state == lib.STATUS[2] and item.remote_progress < 1 + return item.status == lib.STATUS[2] and item.remote_progress < 1 if role == Qt.DisplayRole: # because of ImageDelegate diff --git a/openpype/modules/sync_server/tray/widgets.py b/openpype/modules/sync_server/tray/widgets.py index 5071ffa2b0..5719d13716 100644 --- a/openpype/modules/sync_server/tray/widgets.py +++ b/openpype/modules/sync_server/tray/widgets.py @@ -1,6 +1,7 @@ import os import subprocess import sys +from functools import partial from Qt import QtWidgets, QtCore, QtGui from Qt.QtCore import Qt @@ -91,7 +92,7 @@ class SyncProjectListWidget(ProjectListWidget): self.project_name = point_index.data(QtCore.Qt.DisplayRole) menu = QtWidgets.QMenu() - menu.setStyleSheet(style.load_stylesheet()) + #menu.setStyleSheet(style.load_stylesheet()) actions_mapping = {} if self.sync_server.is_project_paused(self.project_name): @@ -150,7 +151,7 @@ class SyncRepresentationWidget(QtWidgets.QWidget): ("files_count", 50), ("files_size", 60), ("priority", 50), - ("state", 110) + ("status", 110) ) def __init__(self, sync_server, project=None, parent=None): @@ -217,6 +218,14 @@ class SyncRepresentationWidget(QtWidgets.QWidget): self.selection_model = self.table_view.selectionModel() self.selection_model.selectionChanged.connect(self._selection_changed) + self.checked_values = {} + + self.horizontal_header = self.table_view.horizontalHeader() + self.horizontal_header.setContextMenuPolicy( + QtCore.Qt.CustomContextMenu) + self.horizontal_header.customContextMenuRequested.connect( + self._on_section_clicked) + def _selection_changed(self, _new_selection): index = self.selection_model.currentIndex() self._selected_id = \ @@ -246,6 +255,136 @@ class SyncRepresentationWidget(QtWidgets.QWidget): self.sync_server, _id, self.table_view.model().project) detail_window.exec() + def _on_section_clicked(self, point): + + logical_index = self.horizontal_header.logicalIndexAt(point) + + model = self.table_view.model() + column_name = model.headerData(logical_index, + Qt.Horizontal, lib.HeaderNameRole) + items_dict = model.get_column_filter_values(logical_index) + + if not items_dict: + return + + menu = QtWidgets.QMenu(self) + + # text filtering only if labels same as values, not if codes are used + if list(items_dict.keys())[0] == list(items_dict.values())[0]: + self.line_edit = QtWidgets.QLineEdit(self) + self.line_edit.setPlaceholderText("Type and enter...") + action_le = QtWidgets.QWidgetAction(menu) + action_le.setDefaultWidget(self.line_edit) + self.line_edit.returnPressed.connect( + partial(self._apply_text_filter, column_name, items_dict)) + menu.addAction(action_le) + menu.addSeparator() + + action_all = QtWidgets.QAction("All", self) + state_checked = 2 + # action_all.triggered.connect(partial(self._apply_filter, column_name, + # items_dict, state_checked)) + action_all.triggered.connect(partial(self._reset_filter, column_name)) + menu.addAction(action_all) + + action_none = QtWidgets.QAction("Unselect all", self) + state_unchecked = 0 + action_none.triggered.connect(partial(self._apply_filter, column_name, + items_dict, state_unchecked)) + menu.addAction(action_none) + menu.addSeparator() + + # nothing explicitly >> ALL implicitly >> first time + if self.checked_values.get(column_name) is None: + checked_keys = items_dict.keys() + else: + checked_keys = self.checked_values[column_name] + + for value, label in items_dict.items(): + checkbox = QtWidgets.QCheckBox(str(label), menu) + if value in checked_keys: + checkbox.setChecked(True) + + action = QtWidgets.QWidgetAction(menu) + action.setDefaultWidget(checkbox) + + checkbox.stateChanged.connect(partial(self._apply_filter, + column_name, {value: label})) + menu.addAction(action) + + self.menu = menu + self.menu_items_dict = items_dict # all available items + self.menu.exec_(QtGui.QCursor.pos()) + + def _reset_filter(self, column_name): + """ + Remove whole column from filter >> not in $match at all (faster) + """ + if self.checked_values.get(column_name) is not None: + self.checked_values.pop(column_name) + self._refresh_model_and_menu(column_name, True, True) + + def _apply_filter(self, column_name, values, state): + """ + Sets 'values' to specific 'state' (checked/unchecked), + sends to model. + """ + self._update_checked_values(column_name, values, state) + self._refresh_model_and_menu(column_name, True, False) + + def _apply_text_filter(self, column_name, items): + """ + Resets all checkboxes, prefers inserted text. + """ + self._update_checked_values(column_name, items, 0) # reset other + text_item = {self.line_edit.text(): self.line_edit.text()} + self._update_checked_values(column_name, text_item, 2) + self._refresh_model_and_menu(column_name, True, True) + + def _refresh_model_and_menu(self, column_name, model=True, menu=True): + """ + Refresh model and its content and possibly menu for big changes. + """ + if model: + self.table_view.model().set_column_filtering(self.checked_values) + self.table_view.model().refresh() + if menu: + self._menu_refresh(column_name) + + def _menu_refresh(self, column_name): + """ + Reset boxes after big change - word filtering or reset + """ + for action in self.menu.actions(): + if not isinstance(action, QtWidgets.QWidgetAction): + continue + + widget = action.defaultWidget() + if not isinstance(widget, QtWidgets.QCheckBox): + continue + + if not self.checked_values.get(column_name) or \ + widget.text() in self.checked_values[column_name].values(): + widget.setChecked(True) + else: + widget.setChecked(False) + + def _update_checked_values(self, column_name, values, state): + """ + Modify dictionary of set values in columns for filtering. + + Modifies 'self.checked_values' + """ + checked = self.checked_values.get(column_name, self.menu_items_dict) + set_items = dict(values.items()) # prevent dictionary change during iter + for value, label in set_items.items(): + if state == 2: # checked + checked[value] = label + elif state == 0 and checked.get(value): + checked.pop(value) + + self.checked_values[column_name] = checked + def _on_context_menu(self, point): """ Shows menu with loader actions on Right-click. @@ -291,17 +430,17 @@ class SyncRepresentationWidget(QtWidgets.QWidget): else: self.site_name = remote_site - if self.item.state in [lib.STATUS[0], lib.STATUS[1]]: + if self.item.status in [lib.STATUS[0], lib.STATUS[1]]: action = QtWidgets.QAction("Pause") actions_mapping[action] = self._pause menu.addAction(action) - if self.item.state == lib.STATUS[3]: + if self.item.status == lib.STATUS[3]: action = QtWidgets.QAction("Unpause") actions_mapping[action] = self._unpause menu.addAction(action) - # if self.item.state == lib.STATUS[1]: + # if self.item.status == lib.STATUS[1]: # action = QtWidgets.QAction("Open error detail") # actions_mapping[action] = self._show_detail # menu.addAction(action) @@ -467,7 +606,7 @@ class SyncRepresentationDetailWidget(QtWidgets.QWidget): ("remote_site", 185), ("size", 60), ("priority", 25), - ("state", 110) + ("status", 110) ) def __init__(self, sync_server, _id=None, project=None, parent=None): @@ -579,7 +718,7 @@ class SyncRepresentationDetailWidget(QtWidgets.QWidget): self.item = self.table_view.model()._data[point_index.row()] menu = QtWidgets.QMenu() - menu.setStyleSheet(style.load_stylesheet()) + #menu.setStyleSheet(style.load_stylesheet()) actions_mapping = {} actions_kwargs_mapping = {} @@ -604,7 +743,7 @@ class SyncRepresentationDetailWidget(QtWidgets.QWidget): actions_kwargs_mapping[action] = {'site': site} menu.addAction(action) - if self.item.state == lib.STATUS[2]: + if self.item.status == lib.STATUS[2]: action = QtWidgets.QAction("Open error detail") actions_mapping[action] = self._show_detail menu.addAction(action) From 72ac25e60e949ad3ab8185983f2d011dc9703c5d Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Tue, 20 Apr 2021 20:18:31 +0200 Subject: [PATCH 2/9] SyncServer GUI - rework of header and menus Reiplemented QHeaderView Refactored accessing models Still wip --- openpype/modules/sync_server/tray/lib.py | 96 ++++ openpype/modules/sync_server/tray/models.py | 102 ++-- openpype/modules/sync_server/tray/widgets.py | 505 ++++++++++++------- 3 files changed, 488 insertions(+), 215 deletions(-) diff --git a/openpype/modules/sync_server/tray/lib.py b/openpype/modules/sync_server/tray/lib.py index 051567ed6c..41b0eb43f9 100644 --- a/openpype/modules/sync_server/tray/lib.py +++ b/openpype/modules/sync_server/tray/lib.py @@ -1,5 +1,7 @@ from Qt import QtCore import attr +import abc +import six from openpype.lib import PypeLogger @@ -24,6 +26,100 @@ FailedRole = QtCore.Qt.UserRole + 8 HeaderNameRole = QtCore.Qt.UserRole + 10 +@six.add_metaclass(abc.ABCMeta) +class AbstractColumnFilter: + + def __init__(self, column_name, dbcon=None): + self.column_name = column_name + self.dbcon = dbcon + self._search_variants = [] + + def search_variants(self): + """ + Returns all flavors of search available for this column, + """ + return self._search_variants + + @abc.abstractmethod + def values(self): + """ + Returns dict of available values for filter {'label':'value'} + """ + pass + + @abc.abstractmethod + def prepare_match_part(self, values): + """ + Prepares format valid for $match part from 'values + + Args: + values (dict): {'label': 'value'} + Returns: + (dict): {'COLUMN_NAME': {'$in': ['val1', 'val2']}} + """ + pass + + +class PredefinedSetFilter(AbstractColumnFilter): + + def __init__(self, column_name, values): + super().__init__(column_name) + self._search_variants = ['text', 'checkbox'] + self._values = values + + def values(self): + return {k: v for k, v in self._values.items()} + + def prepare_match_part(self, values): + return {'$in': list(values.keys())} + + +class RegexTextFilter(AbstractColumnFilter): + + def __init__(self, column_name): + super().__init__(column_name) + self._search_variants = ['text'] + + def values(self): + return {} + + def prepare_match_part(self, values): + """ values = {'text1 text2': 'text1 text2'} """ + if not values: + return {} + + regex_strs = set() + text = list(values.keys())[0] # only single key always expected + for word in text.split(): + regex_strs.add('.*{}.*'.format(word)) + + return {"$regex": "|".join(regex_strs), + "$options": 'i'} + + +class MultiSelectFilter(AbstractColumnFilter): + + def __init__(self, column_name, values=None, dbcon=None): + super().__init__(column_name) + self._values = values + self.dbcon = dbcon + self._search_variants = ['checkbox'] + + def values(self): + if self._values: + return {k: v for k, v in self._values.items()} + + recs = self.dbcon.find({'type': self.column_name}, {"name": 1, + "_id": -1}) + values = {} + for item in recs: + values[item["name"]] = item["name"] + return dict(sorted(values.items(), key=lambda it: it[1])) + + def prepare_match_part(self, values): + return {'$in': list(values.keys())} + + @attr.s class FilterDefinition: type = attr.ib() diff --git a/openpype/modules/sync_server/tray/models.py b/openpype/modules/sync_server/tray/models.py index 444422c56a..4b70fbae15 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 @@ -67,18 +68,24 @@ class _SyncRepresentationModel(QtCore.QAbstractTableModel): return len(self._header) def headerData(self, section, orientation, role): + name = self.COLUMN_LABELS[section][0] if role == Qt.DisplayRole: if orientation == Qt.Horizontal: - name = self.COLUMN_LABELS[section][0] - txt = "" - if name in self.column_filtering.keys(): - txt = "(F)" - return self.COLUMN_LABELS[section][1] + txt # return label + return self.COLUMN_LABELS[section][1] + + if role == Qt.DecorationRole: + if name in self.column_filtering.keys(): + return qtawesome.icon("fa.filter", color="white") + if self.COLUMN_FILTERS.get(name): + return qtawesome.icon("fa.filter", color="gray") if role == lib.HeaderNameRole: if orientation == Qt.Horizontal: return self.COLUMN_LABELS[section][0] # return name + def get_column(self, index): + return self.COLUMN_LABELS[index] + def get_header_index(self, value): """ Returns index of 'value' in headers @@ -199,9 +206,28 @@ class _SyncRepresentationModel(QtCore.QAbstractTableModel): Args: word_filter (str): string inputted by user """ - self.word_filter = word_filter + self._word_filter = word_filter self.refresh() + def get_column_filter(self, index): + """ + Returns filter object for column 'index + + Args: + index(int): index of column in header + + Returns: + (AbstractColumnFilter) + """ + column_name = self._header[index] + + filter_rec = self.COLUMN_FILTERS.get(column_name) + if filter_rec: + filter_rec.dbcon = self.dbcon # up-to-date db connection + + return filter_rec + + def get_column_filter_values(self, index): """ Returns list of available values for filtering in the column @@ -215,21 +241,11 @@ class _SyncRepresentationModel(QtCore.QAbstractTableModel): menu for some columns ('subset') might be 'value' and 'label' same """ - column_name = self._header[index] - - filter_def = self.COLUMN_FILTERS.get(column_name) - if not filter_def: + filter_rec = self.get_column_filter(index) + if not filter_rec: return {} - if filter_def['type'] == 'predefined_set': - return dict(filter_def['values']) - elif filter_def['type'] == 'available_values': - recs = self.dbcon.find({'type': column_name}, {"name": 1, - "_id": -1}) - values = {} - for item in recs: - values[item["name"]] = item["name"] - return dict(sorted(values.items(), key=lambda item: item[1])) + return filter_rec.values() def set_project(self, project): """ @@ -313,11 +329,10 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel): ] COLUMN_FILTERS = { - 'status': {'type': 'predefined_set', - 'values': {k: v for k, v in lib.STATUS.items()}}, - 'subset': {'type': 'available_values'}, - 'asset': {'type': 'available_values'}, - 'representation': {'type': 'available_values'} + 'status': lib.PredefinedSetFilter('status', lib.STATUS), + 'subset': lib.RegexTextFilter('subset'), + 'asset': lib.RegexTextFilter('asset'), + 'representation': lib.MultiSelectFilter('representation') } refresh_started = QtCore.Signal() @@ -356,9 +371,11 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel): self._project = project self._rec_loaded = 0 self._total_records = 0 # how many documents query actually found - self.word_filter = None + self._word_filter = None self._column_filtering = {} + self._word_filter = None + self._initialized = False if not self._project or self._project == lib.DUMMY_PROJECT: return @@ -383,6 +400,19 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel): self.timer.timeout.connect(self.tick) self.timer.start(self.REFRESH_SEC) + def get_filters(self): + """ + Returns all available filter editors per column_name keys. + """ + filters = {} + for column_name, _ in self.COLUMN_LABELS: + filter_rec = self.COLUMN_FILTERS.get(column_name) + if filter_rec: + filter_rec.dbcon = self.dbcon + filters[column_name] = filter_rec + + return filters + def data(self, index, role): item = self._data[index.row()] @@ -666,8 +696,12 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel): self._column_filtering : {'status': {'$in': [1, 2, 3]}} """ filtering = {} - for key, dict_value in checked_values.items(): - filtering[key] = {'$in': list(dict_value.keys())} + for column_name, dict_value in checked_values.items(): + column_f = self.COLUMN_FILTERS.get(column_name) + if not column_f: + continue + column_f.dbcon = self.dbcon + filtering[column_name] = column_f.prepare_match_part(dict_value) self._column_filtering = filtering @@ -690,18 +724,18 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel): 'files.sites.name': {'$all': [self.local_site, self.remote_site]} } - if not self.word_filter: + if not self._word_filter: return base_match else: - regex_str = '.*{}.*'.format(self.word_filter) + regex_str = '.*{}.*'.format(self._word_filter) base_match['$or'] = [ {'context.subset': {'$regex': regex_str, '$options': 'i'}}, {'context.asset': {'$regex': regex_str, '$options': 'i'}}, {'context.representation': {'$regex': regex_str, '$options': 'i'}}] - if ObjectId.is_valid(self.word_filter): - base_match['$or'] = [{'_id': ObjectId(self.word_filter)}] + if ObjectId.is_valid(self._word_filter): + base_match['$or'] = [{'_id': ObjectId(self._word_filter)}] return base_match @@ -848,7 +882,7 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel): self._project = project self._rec_loaded = 0 self._total_records = 0 # how many documents query actually found - self.word_filter = None + self._word_filter = None self._id = _id self._initialized = False @@ -1114,13 +1148,13 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel): Returns: (dict) """ - if not self.word_filter: + if not self._word_filter: return { "type": "representation", "_id": self._id } else: - regex_str = '.*{}.*'.format(self.word_filter) + regex_str = '.*{}.*'.format(self._word_filter) return { "type": "representation", "_id": self._id, diff --git a/openpype/modules/sync_server/tray/widgets.py b/openpype/modules/sync_server/tray/widgets.py index 5719d13716..f9f904cd03 100644 --- a/openpype/modules/sync_server/tray/widgets.py +++ b/openpype/modules/sync_server/tray/widgets.py @@ -15,6 +15,7 @@ from openpype.api import get_local_site_id from openpype.lib import PypeLogger from avalon.tools.delegates import pretty_timestamp +from avalon.vendor import qtawesome from openpype.modules.sync_server.tray.models import ( SyncRepresentationSummaryModel, @@ -142,15 +143,15 @@ class SyncRepresentationWidget(QtWidgets.QWidget): message_generated = QtCore.Signal(str) default_widths = ( - ("asset", 220), - ("subset", 190), - ("version", 55), - ("representation", 95), + ("asset", 200), + ("subset", 170), + ("version", 60), + ("representation", 135), ("local_site", 170), ("remote_site", 170), ("files_count", 50), ("files_size", 60), - ("priority", 50), + ("priority", 70), ("status", 110) ) @@ -183,8 +184,6 @@ class SyncRepresentationWidget(QtWidgets.QWidget): QtWidgets.QAbstractItemView.SelectRows) self.table_view.horizontalHeader().setSortIndicator( -1, Qt.AscendingOrder) - self.table_view.setSortingEnabled(True) - self.table_view.horizontalHeader().setSortIndicatorShown(True) self.table_view.setAlternatingRowColors(True) self.table_view.verticalHeader().hide() @@ -196,10 +195,6 @@ class SyncRepresentationWidget(QtWidgets.QWidget): delegate = ImageDelegate(self) self.table_view.setItemDelegateForColumn(column, delegate) - for column_name, width in self.default_widths: - idx = model.get_header_index(column_name) - self.table_view.setColumnWidth(idx, width) - layout = QtWidgets.QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) layout.addLayout(top_bar_layout) @@ -213,23 +208,26 @@ class SyncRepresentationWidget(QtWidgets.QWidget): model.refresh_started.connect(self._save_scrollbar) model.refresh_finished.connect(self._set_scrollbar) - self.table_view.model().modelReset.connect(self._set_selection) + model.modelReset.connect(self._set_selection) + + self.model = model self.selection_model = self.table_view.selectionModel() self.selection_model.selectionChanged.connect(self._selection_changed) - self.checked_values = {} + self.horizontal_header = HorizontalHeader(self) + self.table_view.setHorizontalHeader(self.horizontal_header) + # self.table_view.setSortingEnabled(True) + # self.table_view.horizontalHeader().setSortIndicatorShown(True) - self.horizontal_header = self.table_view.horizontalHeader() - self.horizontal_header.setContextMenuPolicy( - QtCore.Qt.CustomContextMenu) - self.horizontal_header.customContextMenuRequested.connect( - self._on_section_clicked) + for column_name, width in self.default_widths: + idx = model.get_header_index(column_name) + self.table_view.setColumnWidth(idx, width) def _selection_changed(self, _new_selection): index = self.selection_model.currentIndex() self._selected_id = \ - self.table_view.model().data(index, Qt.UserRole) + self.model.data(index, Qt.UserRole) def _set_selection(self): """ @@ -238,7 +236,7 @@ class SyncRepresentationWidget(QtWidgets.QWidget): Keep selection during model refresh. """ if self._selected_id: - index = self.table_view.model().get_index(self._selected_id) + index = self.model.get_index(self._selected_id) if index and index.isValid(): mode = QtCore.QItemSelectionModel.Select | \ QtCore.QItemSelectionModel.Rows @@ -250,141 +248,11 @@ class SyncRepresentationWidget(QtWidgets.QWidget): """ Opens representation dialog with all files after doubleclick """ - _id = self.table_view.model().data(index, Qt.UserRole) + _id = self.model.data(index, Qt.UserRole) detail_window = SyncServerDetailWindow( - self.sync_server, _id, self.table_view.model().project) + self.sync_server, _id, self.model.project) detail_window.exec() - def _on_section_clicked(self, point): - - logical_index = self.horizontal_header.logicalIndexAt(point) - - model = self.table_view.model() - column_name = model.headerData(logical_index, - Qt.Horizontal, lib.HeaderNameRole) - items_dict = model.get_column_filter_values(logical_index) - - if not items_dict: - return - - menu = QtWidgets.QMenu(self) - - # text filtering only if labels same as values, not if codes are used - if list(items_dict.keys())[0] == list(items_dict.values())[0]: - self.line_edit = QtWidgets.QLineEdit(self) - self.line_edit.setPlaceholderText("Type and enter...") - action_le = QtWidgets.QWidgetAction(menu) - action_le.setDefaultWidget(self.line_edit) - self.line_edit.returnPressed.connect( - partial(self._apply_text_filter, column_name, items_dict)) - menu.addAction(action_le) - menu.addSeparator() - - action_all = QtWidgets.QAction("All", self) - state_checked = 2 - # action_all.triggered.connect(partial(self._apply_filter, column_name, - # items_dict, state_checked)) - action_all.triggered.connect(partial(self._reset_filter, column_name)) - menu.addAction(action_all) - - action_none = QtWidgets.QAction("Unselect all", self) - state_unchecked = 0 - action_none.triggered.connect(partial(self._apply_filter, column_name, - items_dict, state_unchecked)) - menu.addAction(action_none) - menu.addSeparator() - - # nothing explicitly >> ALL implicitly >> first time - if self.checked_values.get(column_name) is None: - checked_keys = items_dict.keys() - else: - checked_keys = self.checked_values[column_name] - - for value, label in items_dict.items(): - checkbox = QtWidgets.QCheckBox(str(label), menu) - if value in checked_keys: - checkbox.setChecked(True) - - action = QtWidgets.QWidgetAction(menu) - action.setDefaultWidget(checkbox) - - checkbox.stateChanged.connect(partial(self._apply_filter, - column_name, {value: label})) - menu.addAction(action) - - self.menu = menu - self.menu_items_dict = items_dict # all available items - self.menu.exec_(QtGui.QCursor.pos()) - - def _reset_filter(self, column_name): - """ - Remove whole column from filter >> not in $match at all (faster) - """ - if self.checked_values.get(column_name) is not None: - self.checked_values.pop(column_name) - self._refresh_model_and_menu(column_name, True, True) - - def _apply_filter(self, column_name, values, state): - """ - Sets 'values' to specific 'state' (checked/unchecked), - sends to model. - """ - self._update_checked_values(column_name, values, state) - self._refresh_model_and_menu(column_name, True, False) - - def _apply_text_filter(self, column_name, items): - """ - Resets all checkboxes, prefers inserted text. - """ - self._update_checked_values(column_name, items, 0) # reset other - text_item = {self.line_edit.text(): self.line_edit.text()} - self._update_checked_values(column_name, text_item, 2) - self._refresh_model_and_menu(column_name, True, True) - - def _refresh_model_and_menu(self, column_name, model=True, menu=True): - """ - Refresh model and its content and possibly menu for big changes. - """ - if model: - self.table_view.model().set_column_filtering(self.checked_values) - self.table_view.model().refresh() - if menu: - self._menu_refresh(column_name) - - def _menu_refresh(self, column_name): - """ - Reset boxes after big change - word filtering or reset - """ - for action in self.menu.actions(): - if not isinstance(action, QtWidgets.QWidgetAction): - continue - - widget = action.defaultWidget() - if not isinstance(widget, QtWidgets.QCheckBox): - continue - - if not self.checked_values.get(column_name) or \ - widget.text() in self.checked_values[column_name].values(): - widget.setChecked(True) - else: - widget.setChecked(False) - - def _update_checked_values(self, column_name, values, state): - """ - Modify dictionary of set values in columns for filtering. - - Modifies 'self.checked_values' - """ - checked = self.checked_values.get(column_name, self.menu_items_dict) - set_items = dict(values.items()) # prevent dictionary change during iter - for value, label in set_items.items(): - if state == 2: # checked - checked[value] = label - elif state == 0 and checked.get(value): - checked.pop(value) - - self.checked_values[column_name] = checked - def _on_context_menu(self, point): """ Shows menu with loader actions on Right-click. @@ -393,7 +261,7 @@ class SyncRepresentationWidget(QtWidgets.QWidget): if not point_index.isValid(): return - self.item = self.table_view.model()._data[point_index.row()] + self.item = self.model._data[point_index.row()] self.representation_id = self.item._id log.debug("menu representation _id:: {}". format(self.representation_id)) @@ -410,7 +278,7 @@ class SyncRepresentationWidget(QtWidgets.QWidget): for site, progress in {local_site: local_progress, remote_site: remote_progress}.items(): - project = self.table_view.model().project + project = self.model.project provider = self.sync_server.get_provider_for_site(project, site) if provider == 'local_drive': @@ -476,10 +344,10 @@ class SyncRepresentationWidget(QtWidgets.QWidget): if to_run: to_run(**to_run_kwargs) - self.table_view.model().refresh() + self.model.refresh() def _pause(self): - self.sync_server.pause_representation(self.table_view.model().project, + self.sync_server.pause_representation(self.model.project, self.representation_id, self.site_name) self.site_name = None @@ -487,7 +355,7 @@ class SyncRepresentationWidget(QtWidgets.QWidget): def _unpause(self): self.sync_server.unpause_representation( - self.table_view.model().project, + self.model.project, self.representation_id, self.site_name) self.site_name = None @@ -497,7 +365,7 @@ class SyncRepresentationWidget(QtWidgets.QWidget): # temporary here for testing, will be removed TODO def _add_site(self): log.info(self.representation_id) - project_name = self.table_view.model().project + project_name = self.model.project local_site_name = get_local_site_id() try: self.sync_server.add_site( @@ -525,15 +393,15 @@ class SyncRepresentationWidget(QtWidgets.QWidget): try: local_site = get_local_site_id() self.sync_server.remove_site( - self.table_view.model().project, + self.model.project, self.representation_id, local_site, True) self.message_generated.emit("Site {} removed".format(local_site)) except ValueError as exp: self.message_generated.emit("Error {}".format(str(exp))) - self.table_view.model().refresh( - load_records=self.table_view.model()._rec_loaded) + self.model.refresh( + load_records=self.model._rec_loaded) def _reset_local_site(self): """ @@ -541,11 +409,11 @@ class SyncRepresentationWidget(QtWidgets.QWidget): redo of upload/download """ self.sync_server.reset_provider_for_file( - self.table_view.model().project, + self.model.project, self.representation_id, 'local') - self.table_view.model().refresh( - load_records=self.table_view.model()._rec_loaded) + self.model.refresh( + load_records=self.model._rec_loaded) def _reset_remote_site(self): """ @@ -553,18 +421,18 @@ class SyncRepresentationWidget(QtWidgets.QWidget): redo of upload/download """ self.sync_server.reset_provider_for_file( - self.table_view.model().project, + self.model.project, self.representation_id, 'remote') - self.table_view.model().refresh( - load_records=self.table_view.model()._rec_loaded) + self.model.refresh( + load_records=self.model._rec_loaded) def _open_in_explorer(self, site): if not self.item: return fpath = self.item.path - project = self.table_view.model().project + project = self.model.project fpath = self.sync_server.get_local_file_path(project, site, fpath) @@ -647,11 +515,11 @@ class SyncRepresentationDetailWidget(QtWidgets.QWidget): self.table_view.setAlternatingRowColors(True) self.table_view.verticalHeader().hide() - column = self.table_view.model().get_header_index("local_site") + column = model.get_header_index("local_site") delegate = ImageDelegate(self) self.table_view.setItemDelegateForColumn(column, delegate) - column = self.table_view.model().get_header_index("remote_site") + column = model.get_header_index("remote_site") delegate = ImageDelegate(self) self.table_view.setItemDelegateForColumn(column, delegate) @@ -671,14 +539,15 @@ class SyncRepresentationDetailWidget(QtWidgets.QWidget): model.refresh_started.connect(self._save_scrollbar) model.refresh_finished.connect(self._set_scrollbar) - self.table_view.model().modelReset.connect(self._set_selection) + model.modelReset.connect(self._set_selection) + self.model = model self.selection_model = self.table_view.selectionModel() self.selection_model.selectionChanged.connect(self._selection_changed) def _selection_changed(self): index = self.selection_model.currentIndex() - self._selected_id = self.table_view.model().data(index, Qt.UserRole) + self._selected_id = self.model.data(index, Qt.UserRole) def _set_selection(self): """ @@ -687,7 +556,7 @@ class SyncRepresentationDetailWidget(QtWidgets.QWidget): Keep selection during model refresh. """ if self._selected_id: - index = self.table_view.model().get_index(self._selected_id) + index = self.model.get_index(self._selected_id) if index and index.isValid(): mode = QtCore.QItemSelectionModel.Select | \ QtCore.QItemSelectionModel.Rows @@ -715,7 +584,7 @@ class SyncRepresentationDetailWidget(QtWidgets.QWidget): if not point_index.isValid(): return - self.item = self.table_view.model()._data[point_index.row()] + self.item = self.model._data[point_index.row()] menu = QtWidgets.QMenu() #menu.setStyleSheet(style.load_stylesheet()) @@ -729,7 +598,7 @@ class SyncRepresentationDetailWidget(QtWidgets.QWidget): for site, progress in {local_site: local_progress, remote_site: remote_progress}.items(): - project = self.table_view.model().project + project = self.model.project provider = self.sync_server.get_provider_for_site(project, site) if provider == 'local_drive': @@ -776,12 +645,12 @@ class SyncRepresentationDetailWidget(QtWidgets.QWidget): redo of upload/download """ self.sync_server.reset_provider_for_file( - self.table_view.model().project, + self.model.project, self.representation_id, 'local', self.item._id) - self.table_view.model().refresh( - load_records=self.table_view.model()._rec_loaded) + self.model.refresh( + load_records=self.model._rec_loaded) def _reset_remote_site(self): """ @@ -789,12 +658,12 @@ class SyncRepresentationDetailWidget(QtWidgets.QWidget): redo of upload/download """ self.sync_server.reset_provider_for_file( - self.table_view.model().project, + self.model.project, self.representation_id, 'remote', self.item._id) - self.table_view.model().refresh( - load_records=self.table_view.model()._rec_loaded) + self.model.refresh( + load_records=self.model._rec_loaded) def _open_in_explorer(self, site): if not self.item: @@ -957,3 +826,277 @@ class SyncRepresentationErrorWindow(QtWidgets.QDialog): self.setLayout(body_layout) self.setWindowTitle("Sync Representation Error Detail") + + +class HorizontalHeader(QtWidgets.QHeaderView): + + def __init__(self, parent=None): + super(HorizontalHeader, self).__init__(QtCore.Qt.Horizontal, parent) + self._parent = parent + self.checked_values = {} + + self.setSectionsMovable(True) + self.setSectionsClickable(True) + self.setHighlightSections(True) + + self.menu_items_dict = {} + self.menu = None + self.header_cells = [] + self.filter_buttons = {} + + self.init_layout() + + self.filter_icon = qtawesome.icon("fa.filter", color="gray") + self.filter_set_icon = qtawesome.icon("fa.filter", color="white") + + self._resetting = False + + self.sectionResized.connect(self.handleSectionResized) + self.sectionMoved.connect(self.handleSectionMoved) + #self.sectionPressed.connect(self.model.sort) + + + @property + def model(self): + """Keep model synchronized with parent widget""" + return self._parent.model + + def init_layout(self): + for i in range(self.count()): + cell_content = QtWidgets.QWidget(self) + column_name, column_label = self.model.get_column(i) + + layout = QtWidgets.QHBoxLayout() + layout.setContentsMargins(5, 5, 5, 0) + layout.setAlignment(Qt.AlignVCenter) + layout.addWidget(QtWidgets.QLabel(column_label)) + + filter_rec = self.model.get_filters().get(column_name) + if filter_rec: + icon = self.filter_icon + button = QtWidgets.QPushButton(icon, "") + layout.addWidget(button) + + # button.setMenu(menu) + button.setFixedSize(24, 24) + # button.setAlignment(Qt.AlignRight) + button.setStyleSheet("QPushButton::menu-indicator{width:0px;}" + "QPushButton{border: none}") + button.clicked.connect(partial(self._get_menu, + column_name, i)) + button.setFlat(True) + self.filter_buttons[column_name] = button + + cell_content.setLayout(layout) + + self.header_cells.append(cell_content) + + def showEvent(self, event): + if not self.header_cells: + self.init_layout() + + for i in range(len(self.header_cells)): + cell_content = self.header_cells[i] + cell_content.setGeometry(self.sectionViewportPosition(i), 0, + self.sectionSize(i)-1, self.height()) + + cell_content.show() + + if len(self.model.get_filters()) > self.count(): + for i in range(self.count(), len(self.header_cells)): + self.header_cells[i].deleteLater() + + super(HorizontalHeader, self).showEvent(event) + + def _set_filter_icon(self, column_name): + button = self.filter_buttons.get(column_name) + if button: + if self.checked_values.get(column_name): + button.setIcon(self.filter_set_icon) + else: + button.setIcon(self.filter_icon) + + def _reset_filter(self, column_name): + """ + Remove whole column from filter >> not in $match at all (faster) + """ + self._resetting = True # mark changes to consume them + if self.checked_values.get(column_name) is not None: + self.checked_values.pop(column_name) + self._set_filter_icon(column_name) + self._filter_and_refresh_model_and_menu(column_name, True, True) + self._resetting = False + + def _apply_filter(self, column_name, values, state): + """ + Sets 'values' to specific 'state' (checked/unchecked), + sends to model. + """ + if self._resetting: # event triggered by _resetting, skip it + return + + self._update_checked_values(column_name, values, state) + self._set_filter_icon(column_name) + self._filter_and_refresh_model_and_menu(column_name, True, False) + + def _apply_text_filter(self, column_name, items): + """ + Resets all checkboxes, prefers inserted text. + """ + self._update_checked_values(column_name, items, 0) # reset other + if self.checked_values.get(column_name) is not None or \ + self.line_edit.text() == '': + self.checked_values.pop(column_name) # reset during typing + + text_item = {self.line_edit.text(): self.line_edit.text()} + if self.line_edit.text(): + self._update_checked_values(column_name, text_item, 2) + self._set_filter_icon(column_name) + self._filter_and_refresh_model_and_menu(column_name, True, True) + + def _filter_and_refresh_model_and_menu(self, column_name, + model=True, menu=True): + """ + Refresh model and its content and possibly menu for big changes. + """ + if model: + self.model.set_column_filtering(self.checked_values) + self.model.refresh() + if menu: + self._menu_refresh(column_name) + + def _get_menu(self, column_name, index): + """Prepares content of menu for 'column_name'""" + menu = QtWidgets.QMenu(self) + filter_rec = self.model.get_filters()[column_name] + self.menu_items_dict[column_name] = filter_rec.values() + self.line_edit = None + + # text filtering only if labels same as values, not if codes are used + if 'text' in filter_rec.search_variants(): + self.line_edit = QtWidgets.QLineEdit(self) + self.line_edit.setSizePolicy( + QtWidgets.QSizePolicy.Maximum, + QtWidgets.QSizePolicy.Maximum) + txt = "Type..." + if self.checked_values.get(column_name): + txt = list(self.checked_values.get(column_name).keys())[0] + self.line_edit.setPlaceholderText(txt) + + action_le = QtWidgets.QWidgetAction(menu) + action_le.setDefaultWidget(self.line_edit) + self.line_edit.textChanged.connect( + partial(self._apply_text_filter, column_name, + filter_rec.values())) + menu.addAction(action_le) + menu.addSeparator() + + if 'checkbox' in filter_rec.search_variants(): + action_all = QtWidgets.QAction("All", self) + action_all.triggered.connect(partial(self._reset_filter, + column_name)) + menu.addAction(action_all) + + action_none = QtWidgets.QAction("Unselect all", self) + state_unchecked = 0 + action_none.triggered.connect(partial(self._apply_filter, + column_name, + filter_rec.values(), + state_unchecked)) + menu.addAction(action_none) + menu.addSeparator() + + # nothing explicitly >> ALL implicitly >> first time + if self.checked_values.get(column_name) is None: + checked_keys = self.menu_items_dict[column_name].keys() + else: + checked_keys = self.checked_values[column_name] + + for value, label in self.menu_items_dict[column_name].items(): + checkbox = QtWidgets.QCheckBox(str(label), menu) + + # temp + checkbox.setStyleSheet("QCheckBox{spacing: 5px;" + "padding:5px 5px 5px 5px;}") + if value in checked_keys: + checkbox.setChecked(True) + + action = QtWidgets.QWidgetAction(menu) + action.setDefaultWidget(checkbox) + + checkbox.stateChanged.connect(partial(self._apply_filter, + column_name, {value: label})) + menu.addAction(action) + + self.menu = menu + + self._show_menu(index, menu) + + def _show_menu(self, index, menu): + """Shows 'menu' under header column of 'index'""" + global_pos_point = self.mapToGlobal( + QtCore.QPoint(self.sectionViewportPosition(index), 0)) + menu.setMinimumWidth(self.sectionSize(index)) + menu.setMinimumHeight(self.height()) + menu.exec_(QtCore.QPoint(global_pos_point.x(), + global_pos_point.y() + self.height())) + + def _menu_refresh(self, column_name): + """ + Reset boxes after big change - word filtering or reset + """ + for action in self.menu.actions(): + if not isinstance(action, QtWidgets.QWidgetAction): + continue + + widget = action.defaultWidget() + if not isinstance(widget, QtWidgets.QCheckBox): + continue + + if not self.checked_values.get(column_name) or \ + widget.text() in self.checked_values[column_name].values(): + widget.setChecked(True) + else: + widget.setChecked(False) + + def _update_checked_values(self, column_name, values, state): + """ + Modify dictionary of set values in columns for filtering. + + Modifies 'self.checked_values' + """ + checked = self.checked_values.get(column_name, + dict(self.menu_items_dict[column_name])) + set_items = dict(values.items()) # prevent dict change during loop + for value, label in set_items.items(): + if state == 2 and label: # checked + checked[value] = label + elif state == 0 and checked.get(value): + checked.pop(value) + + self.checked_values[column_name] = checked + + def handleSectionResized(self, i): + if not self.header_cells: + self.init_layout() + for i in range(self.count()): + j = self.visualIndex(i) + logical = self.logicalIndex(j) + self.header_cells[i].setGeometry( + self.sectionViewportPosition(logical), 0, + self.sectionSize(logical) - 1, self.height()) + + def handleSectionMoved(self, i, oldVisualIndex, newVisualIndex): + if not self.header_cells: + self.init_layout() + for i in range(min(oldVisualIndex, newVisualIndex), self.count()): + logical = self.logicalIndex(i) + self.header_cells[i].setGeometry( + self.ectionViewportPosition(logical), 0, + self.sectionSize(logical) - 2, self.height()) + + def fixComboPositions(self): + for i in range(self.count()): + self.header_cells[i].setGeometry( + self.sectionViewportPosition(i), 0, + self.sectionSize(i) - 2, self.height()) From e918acea48755bab9b4744cfe6ecb8dd8e67f08c Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 21 Apr 2021 15:13:33 +0200 Subject: [PATCH 3/9] SyncServer GUI - working version for Summary list --- openpype/modules/sync_server/tray/models.py | 14 +- openpype/modules/sync_server/tray/widgets.py | 172 ++++++++++--------- 2 files changed, 95 insertions(+), 91 deletions(-) diff --git a/openpype/modules/sync_server/tray/models.py b/openpype/modules/sync_server/tray/models.py index 4b70fbae15..266a9289ca 100644 --- a/openpype/modules/sync_server/tray/models.py +++ b/openpype/modules/sync_server/tray/models.py @@ -64,20 +64,20 @@ class _SyncRepresentationModel(QtCore.QAbstractTableModel): def rowCount(self, _index): return len(self._data) - def columnCount(self, _index): + def columnCount(self, _index=None): return len(self._header) - def headerData(self, section, orientation, role): + def headerData(self, section, orientation, role=Qt.DisplayRole): name = self.COLUMN_LABELS[section][0] if role == Qt.DisplayRole: if orientation == Qt.Horizontal: return self.COLUMN_LABELS[section][1] - if role == Qt.DecorationRole: - if name in self.column_filtering.keys(): - return qtawesome.icon("fa.filter", color="white") - if self.COLUMN_FILTERS.get(name): - return qtawesome.icon("fa.filter", color="gray") + # if role == Qt.DecorationRole: + # if name in self.column_filtering.keys(): + # return qtawesome.icon("fa.filter", color="white") + # if self.COLUMN_FILTERS.get(name): + # return qtawesome.icon("fa.filter", color="gray") if role == lib.HeaderNameRole: if orientation == Qt.Horizontal: diff --git a/openpype/modules/sync_server/tray/widgets.py b/openpype/modules/sync_server/tray/widgets.py index f9f904cd03..25abc73a70 100644 --- a/openpype/modules/sync_server/tray/widgets.py +++ b/openpype/modules/sync_server/tray/widgets.py @@ -42,6 +42,8 @@ class SyncProjectListWidget(ProjectListWidget): self.local_site = None self.icons = {} + self.layout().setContentsMargins(0, 0, 0, 0) + def validate_context_change(self): return True @@ -143,12 +145,12 @@ class SyncRepresentationWidget(QtWidgets.QWidget): message_generated = QtCore.Signal(str) default_widths = ( - ("asset", 200), + ("asset", 190), ("subset", 170), ("version", 60), - ("representation", 135), - ("local_site", 170), - ("remote_site", 170), + ("representation", 145), + ("local_site", 160), + ("remote_site", 160), ("files_count", 50), ("files_size", 60), ("priority", 70), @@ -215,10 +217,10 @@ class SyncRepresentationWidget(QtWidgets.QWidget): self.selection_model = self.table_view.selectionModel() self.selection_model.selectionChanged.connect(self._selection_changed) - self.horizontal_header = HorizontalHeader(self) - self.table_view.setHorizontalHeader(self.horizontal_header) - # self.table_view.setSortingEnabled(True) - # self.table_view.horizontalHeader().setSortIndicatorShown(True) + horizontal_header = HorizontalHeader(self) + + self.table_view.setHorizontalHeader(horizontal_header) + self.table_view.setSortingEnabled(True) for column_name, width in self.default_widths: idx = model.get_header_index(column_name) @@ -828,6 +830,21 @@ class SyncRepresentationErrorWindow(QtWidgets.QDialog): self.setWindowTitle("Sync Representation Error Detail") +class TransparentWidget(QtWidgets.QWidget): + 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): def __init__(self, parent=None): @@ -835,26 +852,23 @@ class HorizontalHeader(QtWidgets.QHeaderView): self._parent = parent self.checked_values = {} - self.setSectionsMovable(True) + self.setModel(self._parent.model) + self.setSectionsClickable(True) - self.setHighlightSections(True) self.menu_items_dict = {} self.menu = None self.header_cells = [] self.filter_buttons = {} - self.init_layout() - self.filter_icon = qtawesome.icon("fa.filter", color="gray") self.filter_set_icon = qtawesome.icon("fa.filter", color="white") + self.init_layout() + self._resetting = False - self.sectionResized.connect(self.handleSectionResized) - self.sectionMoved.connect(self.handleSectionMoved) - #self.sectionPressed.connect(self.model.sort) - + self.sectionClicked.connect(self.on_section_clicked) @property def model(self): @@ -862,38 +876,30 @@ class HorizontalHeader(QtWidgets.QHeaderView): return self._parent.model def init_layout(self): - for i in range(self.count()): - cell_content = QtWidgets.QWidget(self) - column_name, column_label = self.model.get_column(i) - - layout = QtWidgets.QHBoxLayout() - layout.setContentsMargins(5, 5, 5, 0) - layout.setAlignment(Qt.AlignVCenter) - layout.addWidget(QtWidgets.QLabel(column_label)) - + for column_idx in range(self.model.columnCount()): + column_name, column_label = self.model.get_column(column_idx) filter_rec = self.model.get_filters().get(column_name) - if filter_rec: - icon = self.filter_icon - button = QtWidgets.QPushButton(icon, "") - layout.addWidget(button) + if not filter_rec: + continue - # button.setMenu(menu) - button.setFixedSize(24, 24) - # button.setAlignment(Qt.AlignRight) - button.setStyleSheet("QPushButton::menu-indicator{width:0px;}" - "QPushButton{border: none}") - button.clicked.connect(partial(self._get_menu, - column_name, i)) - button.setFlat(True) - self.filter_buttons[column_name] = button + icon = self.filter_icon + button = QtWidgets.QPushButton(icon, "", self) - cell_content.setLayout(layout) + # button.setMenu(menu) + button.setFixedSize(24, 24) + # button.setAlignment(Qt.AlignRight) + button.setStyleSheet("QPushButton::menu-indicator{width:0px;}" + "QPushButton{border: none;background: transparent;}") + button.clicked.connect(partial(self._get_menu, + column_name, column_idx)) + button.setFlat(True) + self.filter_buttons[column_name] = button - self.header_cells.append(cell_content) + def on_section_clicked(self, column_name): + print("on_section_clicked {}".format(column_name)) def showEvent(self, event): - if not self.header_cells: - self.init_layout() + super(HorizontalHeader, self).showEvent(event) for i in range(len(self.header_cells)): cell_content = self.header_cells[i] @@ -902,12 +908,6 @@ class HorizontalHeader(QtWidgets.QHeaderView): cell_content.show() - if len(self.model.get_filters()) > self.count(): - for i in range(self.count(), len(self.header_cells)): - self.header_cells[i].deleteLater() - - super(HorizontalHeader, self).showEvent(event) - def _set_filter_icon(self, column_name): button = self.filter_buttons.get(column_name) if button: @@ -939,18 +939,18 @@ class HorizontalHeader(QtWidgets.QHeaderView): self._set_filter_icon(column_name) self._filter_and_refresh_model_and_menu(column_name, True, False) - def _apply_text_filter(self, column_name, items): + def _apply_text_filter(self, column_name, items, line_edit): """ Resets all checkboxes, prefers inserted text. """ + le_text = line_edit.text() self._update_checked_values(column_name, items, 0) # reset other if self.checked_values.get(column_name) is not None or \ - self.line_edit.text() == '': + le_text == '': self.checked_values.pop(column_name) # reset during typing - text_item = {self.line_edit.text(): self.line_edit.text()} - if self.line_edit.text(): - self._update_checked_values(column_name, text_item, 2) + if le_text: + self._update_checked_values(column_name, {le_text: le_text}, 2) self._set_filter_icon(column_name) self._filter_and_refresh_model_and_menu(column_name, True, True) @@ -970,24 +970,23 @@ class HorizontalHeader(QtWidgets.QHeaderView): menu = QtWidgets.QMenu(self) filter_rec = self.model.get_filters()[column_name] self.menu_items_dict[column_name] = filter_rec.values() - self.line_edit = None # text filtering only if labels same as values, not if codes are used if 'text' in filter_rec.search_variants(): - self.line_edit = QtWidgets.QLineEdit(self) - self.line_edit.setSizePolicy( - QtWidgets.QSizePolicy.Maximum, - QtWidgets.QSizePolicy.Maximum) + line_edit = QtWidgets.QLineEdit(menu) + line_edit.setClearButtonEnabled(True) + + line_edit.setFixedHeight(line_edit.height()) txt = "Type..." if self.checked_values.get(column_name): txt = list(self.checked_values.get(column_name).keys())[0] - self.line_edit.setPlaceholderText(txt) + line_edit.setPlaceholderText(txt) action_le = QtWidgets.QWidgetAction(menu) - action_le.setDefaultWidget(self.line_edit) - self.line_edit.textChanged.connect( + action_le.setDefaultWidget(line_edit) + line_edit.textChanged.connect( partial(self._apply_text_filter, column_name, - filter_rec.values())) + filter_rec.values(), line_edit)) menu.addAction(action_le) menu.addSeparator() @@ -1076,27 +1075,32 @@ class HorizontalHeader(QtWidgets.QHeaderView): self.checked_values[column_name] = checked - def handleSectionResized(self, i): - if not self.header_cells: - self.init_layout() - for i in range(self.count()): - j = self.visualIndex(i) - logical = self.logicalIndex(j) - self.header_cells[i].setGeometry( - self.sectionViewportPosition(logical), 0, - self.sectionSize(logical) - 1, self.height()) + def paintEvent(self, event): + self._fix_size() + super(HorizontalHeader, self).paintEvent(event) - def handleSectionMoved(self, i, oldVisualIndex, newVisualIndex): - if not self.header_cells: - self.init_layout() - for i in range(min(oldVisualIndex, newVisualIndex), self.count()): - logical = self.logicalIndex(i) - self.header_cells[i].setGeometry( - self.ectionViewportPosition(logical), 0, - self.sectionSize(logical) - 2, self.height()) + def _fix_size(self): + for column_idx in range(self.count()): + vis_index = self.visualIndex(column_idx) + index = self.logicalIndex(vis_index) + section_width = self.sectionSize(index) + + column_name = self.model.headerData(column_idx, + QtCore.Qt.Horizontal, + lib.HeaderNameRole) + button = self.filter_buttons.get(column_name) + if not button: + continue + + pos_x = self.sectionViewportPosition( + index) + section_width - self.height() + + pos_y = 0 + if button.height() < self.height(): + pos_y = int((self.height() - button.height()) / 2) + button.setGeometry( + pos_x, + pos_y, + self.height(), + self.height()) - def fixComboPositions(self): - for i in range(self.count()): - self.header_cells[i].setGeometry( - self.sectionViewportPosition(i), 0, - self.sectionSize(i) - 2, self.height()) From d9cbb6eea6cfc18e8ad7b4a2f2c7357f60a9e94b Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 21 Apr 2021 16:04:58 +0200 Subject: [PATCH 4/9] SyncServer GUI - working version for Detail list --- openpype/modules/sync_server/tray/models.py | 101 +++++++++++-------- openpype/modules/sync_server/tray/widgets.py | 63 ++++++------ 2 files changed, 90 insertions(+), 74 deletions(-) diff --git a/openpype/modules/sync_server/tray/models.py b/openpype/modules/sync_server/tray/models.py index 266a9289ca..d2dc3594c6 100644 --- a/openpype/modules/sync_server/tray/models.py +++ b/openpype/modules/sync_server/tray/models.py @@ -68,17 +68,14 @@ class _SyncRepresentationModel(QtCore.QAbstractTableModel): return len(self._header) def headerData(self, section, orientation, role=Qt.DisplayRole): + if section >= len(self.COLUMN_LABELS): + return + name = self.COLUMN_LABELS[section][0] if role == Qt.DisplayRole: if orientation == Qt.Horizontal: return self.COLUMN_LABELS[section][1] - # if role == Qt.DecorationRole: - # if name in self.column_filtering.keys(): - # return qtawesome.icon("fa.filter", color="white") - # if self.COLUMN_FILTERS.get(name): - # return qtawesome.icon("fa.filter", color="gray") - if role == lib.HeaderNameRole: if orientation == Qt.Horizontal: return self.COLUMN_LABELS[section][0] # return name @@ -199,7 +196,7 @@ class _SyncRepresentationModel(QtCore.QAbstractTableModel): representations = self.dbcon.aggregate(self.query) self.refresh(representations) - def set_filter(self, word_filter): + def set_word_filter(self, word_filter): """ Adds text value filtering @@ -209,6 +206,19 @@ class _SyncRepresentationModel(QtCore.QAbstractTableModel): self._word_filter = word_filter self.refresh() + def get_filters(self): + """ + Returns all available filter editors per column_name keys. + """ + filters = {} + for column_name, _ in self.COLUMN_LABELS: + filter_rec = self.COLUMN_FILTERS.get(column_name) + if filter_rec: + filter_rec.dbcon = self.dbcon + filters[column_name] = filter_rec + + return filters + def get_column_filter(self, index): """ Returns filter object for column 'index @@ -227,6 +237,25 @@ class _SyncRepresentationModel(QtCore.QAbstractTableModel): return filter_rec + def set_column_filtering(self, checked_values): + """ + Sets dictionary used in '$match' part of MongoDB aggregate + + Args: + checked_values(dict): key:values ({'status':{1:"Foo",3:"Bar"}} + + Modifies: + self._column_filtering : {'status': {'$in': [1, 2, 3]}} + """ + filtering = {} + for column_name, dict_value in checked_values.items(): + column_f = self.COLUMN_FILTERS.get(column_name) + if not column_f: + continue + column_f.dbcon = self.dbcon + filtering[column_name] = column_f.prepare_match_part(dict_value) + + self._column_filtering = filtering def get_column_filter_values(self, index): """ @@ -400,19 +429,6 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel): self.timer.timeout.connect(self.tick) self.timer.start(self.REFRESH_SEC) - def get_filters(self): - """ - Returns all available filter editors per column_name keys. - """ - filters = {} - for column_name, _ in self.COLUMN_LABELS: - filter_rec = self.COLUMN_FILTERS.get(column_name) - if filter_rec: - filter_rec.dbcon = self.dbcon - filters[column_name] = filter_rec - - return filters - def data(self, index, role): item = self._data[index.row()] @@ -685,26 +701,6 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel): return aggr - def set_column_filtering(self, checked_values): - """ - Sets dictionary used in '$match' part of MongoDB aggregate - - Args: - checked_values(dict): key:values ({'status':{1:"Foo",3:"Bar"}} - - Modifies: - self._column_filtering : {'status': {'$in': [1, 2, 3]}} - """ - filtering = {} - for column_name, dict_value in checked_values.items(): - column_f = self.COLUMN_FILTERS.get(column_name) - if not column_f: - continue - column_f.dbcon = self.dbcon - filtering[column_name] = column_f.prepare_match_part(dict_value) - - self._column_filtering = filtering - def get_match_part(self): """ Extend match part with word_filter if present. @@ -843,10 +839,15 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel): "updated_dt_local", # local created_dt "updated_dt_remote", # remote created_dt "size", # remote progress - "context.asset", # priority TODO + "size", # priority TODO "status" # status ] + COLUMN_FILTERS = { + 'status': lib.PredefinedSetFilter('status', lib.STATUS), + 'file': lib.RegexTextFilter('file'), + } + refresh_started = QtCore.Signal() refresh_finished = QtCore.Signal() @@ -885,6 +886,7 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel): self._word_filter = None self._id = _id self._initialized = False + self._column_filtering = {} self.sync_server = sync_server # TODO think about admin mode @@ -1033,7 +1035,7 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel): if limit == 0: limit = SyncRepresentationSummaryModel.PAGE_SIZE - return [ + aggr = [ {"$match": self.get_match_part()}, {"$unwind": "$files"}, {'$addFields': { @@ -1129,7 +1131,16 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel): ]} ]}} }}, - {"$project": self.projection}, + {"$project": self.projection} + ] + + if self.column_filtering: + aggr.append( + {"$match": self.column_filtering} + ) + print(self.column_filtering) + + aggr.extend([ {"$sort": self.sort}, { '$facet': { @@ -1138,7 +1149,9 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel): 'totalCount': [{'$count': 'count'}] } } - ] + ]) + + return aggr def get_match_part(self): """ diff --git a/openpype/modules/sync_server/tray/widgets.py b/openpype/modules/sync_server/tray/widgets.py index 25abc73a70..e3d5d0fd12 100644 --- a/openpype/modules/sync_server/tray/widgets.py +++ b/openpype/modules/sync_server/tray/widgets.py @@ -203,7 +203,7 @@ class SyncRepresentationWidget(QtWidgets.QWidget): layout.addWidget(self.table_view) self.table_view.doubleClicked.connect(self._double_clicked) - self.filter.textChanged.connect(lambda: model.set_filter( + self.filter.textChanged.connect(lambda: model.set_word_filter( self.filter.text())) self.table_view.customContextMenuRequested.connect( self._on_context_menu) @@ -475,7 +475,7 @@ class SyncRepresentationDetailWidget(QtWidgets.QWidget): ("local_site", 185), ("remote_site", 185), ("size", 60), - ("priority", 25), + ("priority", 60), ("status", 110) ) @@ -499,53 +499,58 @@ class SyncRepresentationDetailWidget(QtWidgets.QWidget): top_bar_layout = QtWidgets.QHBoxLayout() top_bar_layout.addWidget(self.filter) - self.table_view = QtWidgets.QTableView() + table_view = QtWidgets.QTableView() headers = [item[0] for item in self.default_widths] model = SyncRepresentationDetailModel(sync_server, headers, _id, project) - self.table_view.setModel(model) - self.table_view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) - self.table_view.setSelectionMode( + table_view.setModel(model) + table_view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + table_view.setSelectionMode( QtWidgets.QAbstractItemView.SingleSelection) - self.table_view.setSelectionBehavior( + table_view.setSelectionBehavior( QtWidgets.QTableView.SelectRows) - self.table_view.horizontalHeader().setSortIndicator(-1, - Qt.AscendingOrder) - self.table_view.setSortingEnabled(True) - self.table_view.horizontalHeader().setSortIndicatorShown(True) - self.table_view.setAlternatingRowColors(True) - self.table_view.verticalHeader().hide() + table_view.horizontalHeader().setSortIndicator(-1, Qt.AscendingOrder) + table_view.horizontalHeader().setSortIndicatorShown(True) + table_view.setAlternatingRowColors(True) + table_view.verticalHeader().hide() column = model.get_header_index("local_site") delegate = ImageDelegate(self) - self.table_view.setItemDelegateForColumn(column, delegate) + table_view.setItemDelegateForColumn(column, delegate) column = model.get_header_index("remote_site") delegate = ImageDelegate(self) - self.table_view.setItemDelegateForColumn(column, delegate) - - for column_name, width in self.default_widths: - idx = model.get_header_index(column_name) - self.table_view.setColumnWidth(idx, width) + table_view.setItemDelegateForColumn(column, delegate) layout = QtWidgets.QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) layout.addLayout(top_bar_layout) - layout.addWidget(self.table_view) + layout.addWidget(table_view) - self.filter.textChanged.connect(lambda: model.set_filter( + self.model = model + + self.selection_model = table_view.selectionModel() + self.selection_model.selectionChanged.connect(self._selection_changed) + + horizontal_header = HorizontalHeader(self) + + table_view.setHorizontalHeader(horizontal_header) + table_view.setSortingEnabled(True) + + for column_name, width in self.default_widths: + idx = model.get_header_index(column_name) + table_view.setColumnWidth(idx, width) + + self.table_view = table_view + + self.filter.textChanged.connect(lambda: model.set_word_filter( self.filter.text())) - self.table_view.customContextMenuRequested.connect( - self._on_context_menu) + 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) - self.model = model - - self.selection_model = self.table_view.selectionModel() - self.selection_model.selectionChanged.connect(self._selection_changed) def _selection_changed(self): index = self.selection_model.currentIndex() @@ -885,9 +890,7 @@ class HorizontalHeader(QtWidgets.QHeaderView): icon = self.filter_icon button = QtWidgets.QPushButton(icon, "", self) - # button.setMenu(menu) button.setFixedSize(24, 24) - # button.setAlignment(Qt.AlignRight) button.setStyleSheet("QPushButton::menu-indicator{width:0px;}" "QPushButton{border: none;background: transparent;}") button.clicked.connect(partial(self._get_menu, @@ -1080,7 +1083,7 @@ class HorizontalHeader(QtWidgets.QHeaderView): super(HorizontalHeader, self).paintEvent(event) def _fix_size(self): - for column_idx in range(self.count()): + for column_idx in range(self.model.columnCount()): vis_index = self.visualIndex(column_idx) index = self.logicalIndex(vis_index) section_width = self.sectionSize(index) From 7c53a6587d27f2849c7d40d9bd86c4f80d4c8a47 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 21 Apr 2021 16:11:28 +0200 Subject: [PATCH 5/9] SyncServer GUI - renamed methods --- openpype/modules/sync_server/tray/models.py | 27 +++++++++------------ 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/openpype/modules/sync_server/tray/models.py b/openpype/modules/sync_server/tray/models.py index d2dc3594c6..8a4bc53b25 100644 --- a/openpype/modules/sync_server/tray/models.py +++ b/openpype/modules/sync_server/tray/models.py @@ -119,7 +119,7 @@ class _SyncRepresentationModel(QtCore.QAbstractTableModel): self._rec_loaded = 0 if not representations: - self.query = self.get_default_query(load_records) + self.query = self.get_query(load_records) representations = self.dbcon.aggregate(self.query) self.add_page_records(self.local_site, self.remote_site, @@ -154,7 +154,7 @@ class _SyncRepresentationModel(QtCore.QAbstractTableModel): log.debug("fetchMore") items_to_fetch = min(self._total_records - self._rec_loaded, self.PAGE_SIZE) - self.query = self.get_default_query(self._rec_loaded) + self.query = self.get_query(self._rec_loaded) representations = self.dbcon.aggregate(self.query) self.beginInsertRows(index, self._rec_loaded, @@ -187,7 +187,7 @@ class _SyncRepresentationModel(QtCore.QAbstractTableModel): order = -1 self.sort = {self.SORT_BY_COLUMN[index]: order, '_id': 1} - self.query = self.get_default_query() + self.query = self.get_query() # import json # log.debug(json.dumps(self.query, indent=4).\ # replace('False', 'false').\ @@ -415,12 +415,10 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel): self.local_site = self.sync_server.get_active_site(self.project) self.remote_site = self.sync_server.get_remote_site(self.project) - self.projection = self.get_default_projection() - self.sort = self.DEFAULT_SORT - self.query = self.get_default_query() - self.default_query = list(self.get_default_query()) + self.query = self.get_query() + self.default_query = list(self.get_query()) representations = self.dbcon.aggregate(self.query) self.refresh(representations) @@ -544,7 +542,7 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel): self._data.append(item) self._rec_loaded += 1 - def get_default_query(self, limit=0): + def get_query(self, limit=0): """ Returns basic aggregate query for main table. @@ -735,7 +733,8 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel): return base_match - def get_default_projection(self): + @property + def projection(self): """ Projection part for aggregate query. @@ -896,10 +895,7 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel): self.sort = self.DEFAULT_SORT - # in case we would like to hide/show some columns - self.projection = self.get_default_projection() - - self.query = self.get_default_query() + self.query = self.get_query() representations = self.dbcon.aggregate(self.query) self.refresh(representations) @@ -1021,7 +1017,7 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel): self._data.append(item) self._rec_loaded += 1 - def get_default_query(self, limit=0): + def get_query(self, limit=0): """ Gets query that gets used when no extra sorting, filtering or projecting is needed. @@ -1174,7 +1170,8 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel): '$or': [{'files.path': {'$regex': regex_str, '$options': 'i'}}] } - def get_default_projection(self): + @property + def projection(self): """ Projection part for aggregate query. From 9efe5d4f6e675de00b5688c2858e6f7c704efb8c Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 21 Apr 2021 16:16:30 +0200 Subject: [PATCH 6/9] SyncServer GUI - added icon, clean up --- openpype/modules/sync_server/tray/models.py | 1 - openpype/modules/sync_server/tray/widgets.py | 7 ++----- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/openpype/modules/sync_server/tray/models.py b/openpype/modules/sync_server/tray/models.py index 8a4bc53b25..3ee372d27d 100644 --- a/openpype/modules/sync_server/tray/models.py +++ b/openpype/modules/sync_server/tray/models.py @@ -684,7 +684,6 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel): aggr.append( {"$match": self.column_filtering} ) - print(self.column_filtering) aggr.extend( [{"$sort": self.sort}, diff --git a/openpype/modules/sync_server/tray/widgets.py b/openpype/modules/sync_server/tray/widgets.py index e3d5d0fd12..9771d656ff 100644 --- a/openpype/modules/sync_server/tray/widgets.py +++ b/openpype/modules/sync_server/tray/widgets.py @@ -873,8 +873,6 @@ class HorizontalHeader(QtWidgets.QHeaderView): self._resetting = False - self.sectionClicked.connect(self.on_section_clicked) - @property def model(self): """Keep model synchronized with parent widget""" @@ -898,9 +896,6 @@ class HorizontalHeader(QtWidgets.QHeaderView): button.setFlat(True) self.filter_buttons[column_name] = button - def on_section_clicked(self, column_name): - print("on_section_clicked {}".format(column_name)) - def showEvent(self, event): super(HorizontalHeader, self).showEvent(event) @@ -978,6 +973,8 @@ class HorizontalHeader(QtWidgets.QHeaderView): if 'text' in filter_rec.search_variants(): line_edit = QtWidgets.QLineEdit(menu) line_edit.setClearButtonEnabled(True) + line_edit.addAction(self.filter_icon, + QtWidgets.QLineEdit.LeadingPosition) line_edit.setFixedHeight(line_edit.height()) txt = "Type..." From 6742fc89009642df555e20aefdaad736a55da637 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 21 Apr 2021 21:42:10 +0200 Subject: [PATCH 7/9] Hound --- openpype/modules/sync_server/tray/models.py | 14 ++++++++------ openpype/modules/sync_server/tray/widgets.py | 12 +++++------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/openpype/modules/sync_server/tray/models.py b/openpype/modules/sync_server/tray/models.py index 3ee372d27d..8e97f5207d 100644 --- a/openpype/modules/sync_server/tray/models.py +++ b/openpype/modules/sync_server/tray/models.py @@ -6,7 +6,6 @@ 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 @@ -71,7 +70,6 @@ class _SyncRepresentationModel(QtCore.QAbstractTableModel): if section >= len(self.COLUMN_LABELS): return - name = self.COLUMN_LABELS[section][0] if role == Qt.DisplayRole: if orientation == Qt.Horizontal: return self.COLUMN_LABELS[section][1] @@ -453,9 +451,11 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel): if role == lib.FailedRole: if header_value == 'local_site': - return item.status == lib.STATUS[2] and item.local_progress < 1 + return item.status == lib.STATUS[2] and \ + item.local_progress < 1 if header_value == 'remote_site': - return item.status == lib.STATUS[2] and item.remote_progress < 1 + return item.status == lib.STATUS[2] and \ + item.remote_progress < 1 if role == Qt.DisplayRole: # because of ImageDelegate @@ -928,9 +928,11 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel): if role == lib.FailedRole: if header_value == 'local_site': - return item.status == lib.STATUS[2] and item.local_progress < 1 + return item.status == lib.STATUS[2] and \ + item.local_progress < 1 if header_value == 'remote_site': - return item.status == lib.STATUS[2] and item.remote_progress < 1 + return item.status == lib.STATUS[2] and \ + item.remote_progress <1 if role == Qt.DisplayRole: # because of ImageDelegate diff --git a/openpype/modules/sync_server/tray/widgets.py b/openpype/modules/sync_server/tray/widgets.py index 9771d656ff..e8b276580a 100644 --- a/openpype/modules/sync_server/tray/widgets.py +++ b/openpype/modules/sync_server/tray/widgets.py @@ -95,7 +95,6 @@ class SyncProjectListWidget(ProjectListWidget): self.project_name = point_index.data(QtCore.Qt.DisplayRole) menu = QtWidgets.QMenu() - #menu.setStyleSheet(style.load_stylesheet()) actions_mapping = {} if self.sync_server.is_project_paused(self.project_name): @@ -269,7 +268,6 @@ class SyncRepresentationWidget(QtWidgets.QWidget): format(self.representation_id)) menu = QtWidgets.QMenu() - menu.setStyleSheet(style.load_stylesheet()) actions_mapping = {} actions_kwargs_mapping = {} @@ -594,7 +592,6 @@ class SyncRepresentationDetailWidget(QtWidgets.QWidget): self.item = self.model._data[point_index.row()] menu = QtWidgets.QMenu() - #menu.setStyleSheet(style.load_stylesheet()) actions_mapping = {} actions_kwargs_mapping = {} @@ -889,7 +886,8 @@ class HorizontalHeader(QtWidgets.QHeaderView): button = QtWidgets.QPushButton(icon, "", self) button.setFixedSize(24, 24) - button.setStyleSheet("QPushButton::menu-indicator{width:0px;}" + button.setStyleSheet( + "QPushButton::menu-indicator{width:0px;}" "QPushButton{border: none;background: transparent;}") button.clicked.connect(partial(self._get_menu, column_name, column_idx)) @@ -902,7 +900,7 @@ class HorizontalHeader(QtWidgets.QHeaderView): for i in range(len(self.header_cells)): cell_content = self.header_cells[i] cell_content.setGeometry(self.sectionViewportPosition(i), 0, - self.sectionSize(i)-1, self.height()) + self.sectionSize(i) - 1, self.height()) cell_content.show() @@ -1064,8 +1062,8 @@ class HorizontalHeader(QtWidgets.QHeaderView): Modifies 'self.checked_values' """ - checked = self.checked_values.get(column_name, - dict(self.menu_items_dict[column_name])) + copy_menu_items = dict(self.menu_items_dict[column_name]) + checked = self.checked_values.get(column_name, copy_menu_items) set_items = dict(values.items()) # prevent dict change during loop for value, label in set_items.items(): if state == 2 and label: # checked From 498151db82eba4656f669c3c9dc562c5e1483765 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Wed, 21 Apr 2021 22:15:50 +0200 Subject: [PATCH 8/9] Hound --- openpype/modules/sync_server/tray/models.py | 8 ++++---- openpype/modules/sync_server/tray/widgets.py | 1 - 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/openpype/modules/sync_server/tray/models.py b/openpype/modules/sync_server/tray/models.py index 8e97f5207d..dc2094825e 100644 --- a/openpype/modules/sync_server/tray/models.py +++ b/openpype/modules/sync_server/tray/models.py @@ -452,10 +452,10 @@ class SyncRepresentationSummaryModel(_SyncRepresentationModel): if role == lib.FailedRole: if header_value == 'local_site': return item.status == lib.STATUS[2] and \ - item.local_progress < 1 + item.local_progress < 1 if header_value == 'remote_site': return item.status == lib.STATUS[2] and \ - item.remote_progress < 1 + item.remote_progress < 1 if role == Qt.DisplayRole: # because of ImageDelegate @@ -929,10 +929,10 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel): if role == lib.FailedRole: if header_value == 'local_site': return item.status == lib.STATUS[2] and \ - item.local_progress < 1 + item.local_progress < 1 if header_value == 'remote_site': return item.status == lib.STATUS[2] and \ - item.remote_progress <1 + item.remote_progress <1 if role == Qt.DisplayRole: # because of ImageDelegate diff --git a/openpype/modules/sync_server/tray/widgets.py b/openpype/modules/sync_server/tray/widgets.py index e8b276580a..2cdc99c671 100644 --- a/openpype/modules/sync_server/tray/widgets.py +++ b/openpype/modules/sync_server/tray/widgets.py @@ -1101,4 +1101,3 @@ class HorizontalHeader(QtWidgets.QHeaderView): pos_y, self.height(), self.height()) - From 02447101bf4e83b2229279ebb602fdfe1a4a7474 Mon Sep 17 00:00:00 2001 From: Petr Kalis Date: Thu, 22 Apr 2021 10:43:19 +0200 Subject: [PATCH 9/9] SyncServer GUI - small fixes in Status filtering Clear text in column filtering --- openpype/modules/sync_server/tray/lib.py | 5 +++- openpype/modules/sync_server/tray/models.py | 2 +- openpype/modules/sync_server/tray/widgets.py | 30 ++++++++++++-------- 3 files changed, 23 insertions(+), 14 deletions(-) diff --git a/openpype/modules/sync_server/tray/lib.py b/openpype/modules/sync_server/tray/lib.py index 41b0eb43f9..3597213b31 100644 --- a/openpype/modules/sync_server/tray/lib.py +++ b/openpype/modules/sync_server/tray/lib.py @@ -64,8 +64,11 @@ class PredefinedSetFilter(AbstractColumnFilter): def __init__(self, column_name, values): super().__init__(column_name) - self._search_variants = ['text', 'checkbox'] + self._search_variants = ['checkbox'] self._values = values + if self._values and \ + list(self._values.keys())[0] == list(self._values.values())[0]: + self._search_variants.append('text') def values(self): return {k: v for k, v in self._values.items()} diff --git a/openpype/modules/sync_server/tray/models.py b/openpype/modules/sync_server/tray/models.py index dc2094825e..981299c6cf 100644 --- a/openpype/modules/sync_server/tray/models.py +++ b/openpype/modules/sync_server/tray/models.py @@ -932,7 +932,7 @@ class SyncRepresentationDetailModel(_SyncRepresentationModel): item.local_progress < 1 if header_value == 'remote_site': return item.status == lib.STATUS[2] and \ - item.remote_progress <1 + item.remote_progress < 1 if role == Qt.DisplayRole: # because of ImageDelegate diff --git a/openpype/modules/sync_server/tray/widgets.py b/openpype/modules/sync_server/tray/widgets.py index 2cdc99c671..6d8348becb 100644 --- a/openpype/modules/sync_server/tray/widgets.py +++ b/openpype/modules/sync_server/tray/widgets.py @@ -165,13 +165,16 @@ class SyncRepresentationWidget(QtWidgets.QWidget): self.representation_id = None self.site_name = None # to pause/unpause representation - self.filter = QtWidgets.QLineEdit() - self.filter.setPlaceholderText("Filter representations..") + self.txt_filter = QtWidgets.QLineEdit() + self.txt_filter.setPlaceholderText("Quick filter representations..") + self.txt_filter.setClearButtonEnabled(True) + self.txt_filter.addAction(qtawesome.icon("fa.filter", color="gray"), + QtWidgets.QLineEdit.LeadingPosition) self._scrollbar_pos = None top_bar_layout = QtWidgets.QHBoxLayout() - top_bar_layout.addWidget(self.filter) + top_bar_layout.addWidget(self.txt_filter) self.table_view = QtWidgets.QTableView() headers = [item[0] for item in self.default_widths] @@ -202,8 +205,8 @@ class SyncRepresentationWidget(QtWidgets.QWidget): layout.addWidget(self.table_view) self.table_view.doubleClicked.connect(self._double_clicked) - self.filter.textChanged.connect(lambda: model.set_word_filter( - self.filter.text())) + self.txt_filter.textChanged.connect(lambda: model.set_word_filter( + self.txt_filter.text())) self.table_view.customContextMenuRequested.connect( self._on_context_menu) @@ -489,13 +492,16 @@ class SyncRepresentationDetailWidget(QtWidgets.QWidget): self._selected_id = None - self.filter = QtWidgets.QLineEdit() - self.filter.setPlaceholderText("Filter representation..") + self.txt_filter = QtWidgets.QLineEdit() + self.txt_filter.setPlaceholderText("Quick filter representation..") + self.txt_filter.setClearButtonEnabled(True) + self.txt_filter.addAction(qtawesome.icon("fa.filter", color="gray"), + QtWidgets.QLineEdit.LeadingPosition) self._scrollbar_pos = None top_bar_layout = QtWidgets.QHBoxLayout() - top_bar_layout.addWidget(self.filter) + top_bar_layout.addWidget(self.txt_filter) table_view = QtWidgets.QTableView() headers = [item[0] for item in self.default_widths] @@ -542,8 +548,8 @@ class SyncRepresentationDetailWidget(QtWidgets.QWidget): self.table_view = table_view - self.filter.textChanged.connect(lambda: model.set_word_filter( - self.filter.text())) + self.txt_filter.textChanged.connect(lambda: model.set_word_filter( + self.txt_filter.text())) table_view.customContextMenuRequested.connect(self._on_context_menu) model.refresh_started.connect(self._save_scrollbar) @@ -975,10 +981,10 @@ class HorizontalHeader(QtWidgets.QHeaderView): QtWidgets.QLineEdit.LeadingPosition) line_edit.setFixedHeight(line_edit.height()) - txt = "Type..." + txt = "" if self.checked_values.get(column_name): txt = list(self.checked_values.get(column_name).keys())[0] - line_edit.setPlaceholderText(txt) + line_edit.setText(txt) action_le = QtWidgets.QWidgetAction(menu) action_le.setDefaultWidget(line_edit)