diff --git a/openpype/tools/settings/settings/categories.py b/openpype/tools/settings/settings/categories.py index adbde00bf1..28e38ea51d 100644 --- a/openpype/tools/settings/settings/categories.py +++ b/openpype/tools/settings/settings/categories.py @@ -86,6 +86,8 @@ class SettingsCategoryWidget(QtWidgets.QWidget): state_changed = QtCore.Signal() saved = QtCore.Signal(QtWidgets.QWidget) restart_required_trigger = QtCore.Signal() + restart_started = QtCore.Signal() + restart_finished = QtCore.Signal() full_path_requested = QtCore.Signal(str, str) def __init__(self, user_role, parent=None): @@ -307,7 +309,12 @@ class SettingsCategoryWidget(QtWidgets.QWidget): """Change path of widget based on category full path.""" pass + def change_path(self, path): + """Change path and go to widget.""" + self.breadcrumbs_widget.change_path(path) + def set_path(self, path): + """Called from clicked widget.""" self.breadcrumbs_widget.set_path(path) def _add_developer_ui(self, footer_layout): @@ -429,6 +436,7 @@ class SettingsCategoryWidget(QtWidgets.QWidget): self.require_restart_label.setText(value) def reset(self): + self.restart_started.emit() self.set_state(CategoryState.Working) self._on_reset_start() @@ -509,6 +517,7 @@ class SettingsCategoryWidget(QtWidgets.QWidget): self._on_reset_crash() else: self._on_reset_success() + self.restart_finished.emit() def _on_reset_crash(self): self.save_btn.setEnabled(False) diff --git a/openpype/tools/settings/settings/search_dialog.py b/openpype/tools/settings/settings/search_dialog.py index 3d61dd33fe..3f987c0010 100644 --- a/openpype/tools/settings/settings/search_dialog.py +++ b/openpype/tools/settings/settings/search_dialog.py @@ -1,13 +1,14 @@ -from functools import partial import re +import collections -from Qt import QtCore, QtWidgets -from openpype.tools.utils.models import TreeModel, Item -from openpype.tools.utils.lib import schedule +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 [] @@ -23,115 +24,163 @@ class RecursiveSortFilterProxyModel(QtCore.QSortFilterProxyModel): 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 - for column in range(source_model.columnCount(parent)): - source_index = source_model.index(row, column, parent) - if not source_index.isValid(): - continue - - key = source_model.data(source_index, self.filterRole()) - if not key: - continue - - if re.search(pattern, key, re.IGNORECASE): - return True - + 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) + return super( + RecursiveSortFilterProxyModel, self + ).filterAcceptsRow(row, parent) class SearchEntitiesDialog(QtWidgets.QDialog): - path_clicked = QtCore.Signal(str) - def __init__(self, entity, parent=None): + def __init__(self, parent): super(SearchEntitiesDialog, self).__init__(parent=parent) - layout = QtWidgets.QVBoxLayout(self) + self.setWindowTitle("Search Settings") - filter_edit = QtWidgets.QLineEdit() - filter_edit.setPlaceholderText("Search..") + filter_edit = QtWidgets.QLineEdit(self) + filter_edit.setPlaceholderText("Search...") model = EntityTreeModel() proxy = RecursiveSortFilterProxyModel() proxy.setSourceModel(model) proxy.setDynamicSortFilter(True) - view = QtWidgets.QTreeView() - view.setModel(proxy) + 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) - view.selectionModel().selectionChanged.connect(self.on_select) - - view.setAllColumnsShowFocus(True) - view.setSortingEnabled(True) - view.sortByColumn(1, QtCore.Qt.AscendingOrder) + self._filter_edit = filter_edit self._model = model self._proxy = proxy self._view = view + self._filter_changed_timer = filter_changed_timer - # Refresh to the passed entity - model.set_root(entity) + self._first_show = True - view.resizeColumnToContents(0) + 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): - # Provide slight delay to filtering so user can type quickly - schedule(partial(self.on_filter_changed, txt), 250, channel="search") + self._filter_changed_timer.start() - def on_filter_changed(self, txt): - self._proxy.setFilterRegExp(txt) + 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_select(self): + def _on_selection_change(self): current = self._view.currentIndex() - item = current.data(EntityTreeModel.ItemRole) - self.path_clicked.emit(item["path"]) + path = current.data(ENTITY_PATH_ROLE) + self.path_clicked.emit(path) -class EntityTreeModel(TreeModel): +class EntityTreeModel(QtGui.QStandardItemModel): + def __init__(self, *args, **kwargs): + super(EntityTreeModel, self).__init__(*args, **kwargs) + self.setColumnCount(3) - Columns = ["trail", "label", "key", "path"] + def data(self, index, role=None): + if role is None: + role = QtCore.Qt.DisplayRole - def add_entity(self, entity, parent=None): + 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 - item = Item() + if col > 0: + index = self.index(index.row(), 0, index.parent()) + return super(EntityTreeModel, self).data(index, role) - # Label and key can sometimes be emtpy so we use the trail from path - # in the most left column since it's never empty - item["trail"] = entity.path.rsplit("/", 1)[-1] - item["label"] = entity.label - item["key"] = entity.key - item["path"] = entity.path + def flags(self, index): + if index.column() > 0: + index = self.index(index.row(), 0, index.parent()) + return super(EntityTreeModel, self).flags(index) - parent.add_child(item) + 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 + ) - for child in get_entity_children(entity): - self.add_entity(child, parent=item) - - def set_root(self, root_entity): - self.clear() - self.beginResetModel() + 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 - for child in get_entity_children(root_entity): - self.add_entity(child, parent=self._root_item) - self.endResetModel() + 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 55930b5d88..f6fa9a83a5 100644 --- a/openpype/tools/settings/settings/window.py +++ b/openpype/tools/settings/settings/window.py @@ -55,19 +55,27 @@ class MainWidget(QtWidgets.QWidget): 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.restart_started.connect(self._on_restart_started) + tab_widget.restart_finished.connect(self._on_restart_finished) tab_widget.full_path_requested.connect(self._on_full_path_request) 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: @@ -151,6 +159,21 @@ class MainWidget(QtWidgets.QWidget): 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. @@ -166,25 +189,24 @@ class MainWidget(QtWidgets.QWidget): if result == 1: self.trigger_restart.emit() + def _on_restart_started(self): + widget = self.sender() + current_widget = self._header_tab_widget.currentWidget() + if current_widget is widget: + self._update_search_dialog(True) + + def _on_restart_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): - - # todo: This might not be the cleanest place but works for prototype if event.matches(QtGui.QKeySequence.Find): - print("Search!") - # todo: search in all widgets (or in active)? widget = self._header_tab_widget.currentWidget() - root_entity = widget.entity - - search = SearchEntitiesDialog(root_entity, parent=self) - search.resize(700, 500) - search.setWindowTitle("Search") - search.show() - - def on_path(path): - widget.breadcrumbs_widget.change_path(path) - - search.path_clicked.connect(on_path) + self._search_dialog.show() + self._search_dialog.set_root_entity(widget.entity) event.accept() return