Merge pull request #2699 from BigRoy/settings_search

Settings: Implementation for search in settings
This commit is contained in:
Jakub Trllo 2022-02-22 13:50:15 +01:00 committed by GitHub
commit b43b304295
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 242 additions and 0 deletions

View file

@ -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 @@ 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_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 @@ class SettingsCategoryWidget(QtWidgets.QWidget):
self._update_labels_visibility()
def reset(self):
self.reset_started.emit()
self.set_state(CategoryState.Working)
self._on_reset_start()
@ -596,6 +604,7 @@ class SettingsCategoryWidget(QtWidgets.QWidget):
self._on_reset_crash()
else:
self._on_reset_success()
self.reset_finished.emit()
def _on_source_version_change(self, version):
if self._updating_root:

View file

@ -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)

View file

@ -9,6 +9,7 @@ from .widgets import (
RestartDialog,
SettingsTabWidget
)
from .search_dialog import SearchEntitiesDialog
from openpype import style
from openpype.lib import is_admin_password_required
@ -58,15 +59,22 @@ 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.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 @@ class MainWidget(QtWidgets.QWidget):
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 @@ 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.
@ -184,3 +208,26 @@ class MainWidget(QtWidgets.QWidget):
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)