diff --git a/openpype/tools/settings/settings/categories.py b/openpype/tools/settings/settings/categories.py index 18a9764b34a..14e25a54d88 100644 --- a/openpype/tools/settings/settings/categories.py +++ b/openpype/tools/settings/settings/categories.py @@ -91,6 +91,8 @@ class SettingsCategoryWidget(QtWidgets.QWidget): state_changed = QtCore.Signal() saved = QtCore.Signal(QtWidgets.QWidget) restart_required_trigger = QtCore.Signal() + reset_started = QtCore.Signal() + reset_finished = QtCore.Signal() full_path_requested = QtCore.Signal(str, str) require_restart_label_text = ( @@ -379,7 +381,12 @@ def set_category_path(self, category, path): """Change path of widget based on category full path.""" pass + def change_path(self, path): + """Change path and go to widget.""" + self.breadcrumbs_bar.change_path(path) + def set_path(self, path): + """Called from clicked widget.""" self.breadcrumbs_bar.set_path(path) def _add_developer_ui(self, footer_layout, footer_widget): @@ -492,6 +499,7 @@ def _on_require_restart_change(self): self._update_labels_visibility() def reset(self): + self.reset_started.emit() self.set_state(CategoryState.Working) self._on_reset_start() @@ -596,6 +604,7 @@ def reset(self): self._on_reset_crash() else: self._on_reset_success() + self.reset_finished.emit() def _on_source_version_change(self, version): if self._updating_root: diff --git a/openpype/tools/settings/settings/search_dialog.py b/openpype/tools/settings/settings/search_dialog.py new file mode 100644 index 00000000000..3f987c0010f --- /dev/null +++ b/openpype/tools/settings/settings/search_dialog.py @@ -0,0 +1,186 @@ +import re +import collections + +from Qt import QtCore, QtWidgets, QtGui + +ENTITY_LABEL_ROLE = QtCore.Qt.UserRole + 1 +ENTITY_PATH_ROLE = QtCore.Qt.UserRole + 2 + + +def get_entity_children(entity): + # TODO find better way how to go through all children + if hasattr(entity, "values"): + return entity.values() + return [] + + +class RecursiveSortFilterProxyModel(QtCore.QSortFilterProxyModel): + """Filters recursively to regex in all columns""" + + def __init__(self): + super(RecursiveSortFilterProxyModel, self).__init__() + + # Note: Recursive filtering was introduced in Qt 5.10. + self.setRecursiveFilteringEnabled(True) + + def filterAcceptsRow(self, row, parent): + if not parent.isValid(): + return False + + regex = self.filterRegExp() + if not regex.isEmpty() and regex.isValid(): + pattern = regex.pattern() + compiled_regex = re.compile(pattern) + source_model = self.sourceModel() + + # Check current index itself in all columns + source_index = source_model.index(row, 0, parent) + if source_index.isValid(): + for role in (ENTITY_PATH_ROLE, ENTITY_LABEL_ROLE): + value = source_model.data(source_index, role) + if value and compiled_regex.search(value): + return True + return False + + return super( + RecursiveSortFilterProxyModel, self + ).filterAcceptsRow(row, parent) + + +class SearchEntitiesDialog(QtWidgets.QDialog): + path_clicked = QtCore.Signal(str) + + def __init__(self, parent): + super(SearchEntitiesDialog, self).__init__(parent=parent) + + self.setWindowTitle("Search Settings") + + filter_edit = QtWidgets.QLineEdit(self) + filter_edit.setPlaceholderText("Search...") + + model = EntityTreeModel() + proxy = RecursiveSortFilterProxyModel() + proxy.setSourceModel(model) + proxy.setDynamicSortFilter(True) + + view = QtWidgets.QTreeView(self) + view.setAllColumnsShowFocus(True) + view.setSortingEnabled(True) + view.setModel(proxy) + model.setColumnCount(3) + + layout = QtWidgets.QVBoxLayout(self) + layout.addWidget(filter_edit) + layout.addWidget(view) + + filter_changed_timer = QtCore.QTimer() + filter_changed_timer.setInterval(200) + + view.selectionModel().selectionChanged.connect( + self._on_selection_change + ) + filter_changed_timer.timeout.connect(self._on_filter_timer) + filter_edit.textChanged.connect(self._on_filter_changed) + + self._filter_edit = filter_edit + self._model = model + self._proxy = proxy + self._view = view + self._filter_changed_timer = filter_changed_timer + + self._first_show = True + + def set_root_entity(self, entity): + self._model.set_root_entity(entity) + self._view.resizeColumnToContents(0) + + def showEvent(self, event): + super(SearchEntitiesDialog, self).showEvent(event) + if self._first_show: + self._first_show = False + self.resize(700, 500) + + def _on_filter_changed(self, txt): + self._filter_changed_timer.start() + + def _on_filter_timer(self): + text = self._filter_edit.text() + self._proxy.setFilterRegExp(text) + + # WARNING This expanding and resizing is relatively slow. + self._view.expandAll() + self._view.resizeColumnToContents(0) + + def _on_selection_change(self): + current = self._view.currentIndex() + path = current.data(ENTITY_PATH_ROLE) + self.path_clicked.emit(path) + + +class EntityTreeModel(QtGui.QStandardItemModel): + def __init__(self, *args, **kwargs): + super(EntityTreeModel, self).__init__(*args, **kwargs) + self.setColumnCount(3) + + def data(self, index, role=None): + if role is None: + role = QtCore.Qt.DisplayRole + + col = index.column() + if role in (QtCore.Qt.DisplayRole, QtCore.Qt.EditRole): + if col == 0: + pass + elif col == 1: + role = ENTITY_LABEL_ROLE + elif col == 2: + role = ENTITY_PATH_ROLE + + if col > 0: + index = self.index(index.row(), 0, index.parent()) + return super(EntityTreeModel, self).data(index, role) + + def flags(self, index): + if index.column() > 0: + index = self.index(index.row(), 0, index.parent()) + return super(EntityTreeModel, self).flags(index) + + def headerData(self, section, orientation, role=QtCore.Qt.DisplayRole): + if role == QtCore.Qt.DisplayRole: + if section == 0: + return "Key" + elif section == 1: + return "Label" + elif section == 2: + return "Path" + return "" + return super(EntityTreeModel, self).headerData( + section, orientation, role + ) + + def set_root_entity(self, root_entity): + parent = self.invisibleRootItem() + parent.removeRows(0, parent.rowCount()) + if not root_entity: + return + + # We don't want to see the root entity so we directly add its children + fill_queue = collections.deque() + fill_queue.append((root_entity, parent)) + cols = self.columnCount() + while fill_queue: + parent_entity, parent_item = fill_queue.popleft() + child_items = [] + for child in get_entity_children(parent_entity): + label = child.label + path = child.path + key = path.split("/")[-1] + item = QtGui.QStandardItem(key) + item.setEditable(False) + item.setData(label, ENTITY_LABEL_ROLE) + item.setData(path, ENTITY_PATH_ROLE) + item.setColumnCount(cols) + child_items.append(item) + fill_queue.append((child, item)) + + if child_items: + parent_item.appendRows(child_items) diff --git a/openpype/tools/settings/settings/window.py b/openpype/tools/settings/settings/window.py index 8a01bf1bce4..22778e4a5b6 100644 --- a/openpype/tools/settings/settings/window.py +++ b/openpype/tools/settings/settings/window.py @@ -9,6 +9,7 @@ RestartDialog, SettingsTabWidget ) +from .search_dialog import SearchEntitiesDialog from openpype import style from openpype.lib import is_admin_password_required @@ -58,15 +59,22 @@ def __init__(self, user_role, parent=None, reset_on_show=True): self.setLayout(layout) + search_dialog = SearchEntitiesDialog(self) + self._shadow_widget = ShadowWidget("Working...", self) self._shadow_widget.setVisible(False) + header_tab_widget.currentChanged.connect(self._on_tab_changed) + search_dialog.path_clicked.connect(self._on_search_path_clicked) + for tab_widget in tab_widgets: tab_widget.saved.connect(self._on_tab_save) tab_widget.state_changed.connect(self._on_state_change) tab_widget.restart_required_trigger.connect( self._on_restart_required ) + tab_widget.reset_started.connect(self._on_reset_started) + tab_widget.reset_started.connect(self._on_reset_finished) tab_widget.full_path_requested.connect(self._on_full_path_request) header_tab_widget.context_menu_requested.connect( @@ -75,6 +83,7 @@ def __init__(self, user_role, parent=None, reset_on_show=True): self._header_tab_widget = header_tab_widget self.tab_widgets = tab_widgets + self._search_dialog = search_dialog def _on_tab_save(self, source_widget): for tab_widget in self.tab_widgets: @@ -170,6 +179,21 @@ def reset(self): for tab_widget in self.tab_widgets: tab_widget.reset() + def _update_search_dialog(self, clear=False): + if self._search_dialog.isVisible(): + entity = None + if not clear: + widget = self._header_tab_widget.currentWidget() + entity = widget.entity + self._search_dialog.set_root_entity(entity) + + def _on_tab_changed(self): + self._update_search_dialog() + + def _on_search_path_clicked(self, path): + widget = self._header_tab_widget.currentWidget() + widget.change_path(path) + def _on_restart_required(self): # Don't show dialog if there are not registered slots for # `trigger_restart` signal. @@ -184,3 +208,26 @@ def _on_restart_required(self): result = dialog.exec_() if result == 1: self.trigger_restart.emit() + + def _on_reset_started(self): + widget = self.sender() + current_widget = self._header_tab_widget.currentWidget() + if current_widget is widget: + self._update_search_dialog(True) + + def _on_reset_finished(self): + widget = self.sender() + current_widget = self._header_tab_widget.currentWidget() + if current_widget is widget: + self._update_search_dialog() + + def keyPressEvent(self, event): + if event.matches(QtGui.QKeySequence.Find): + # todo: search in all widgets (or in active)? + widget = self._header_tab_widget.currentWidget() + self._search_dialog.show() + self._search_dialog.set_root_entity(widget.entity) + event.accept() + return + + return super(MainWidget, self).keyPressEvent(event)