mirror of
https://github.com/ynput/ayon-core.git
synced 2025-12-26 13:52:15 +01:00
513 lines
15 KiB
Python
513 lines
15 KiB
Python
from qtpy import QtWidgets, QtGui, QtCore
|
|
|
|
PREFIX_ROLE = QtCore.Qt.UserRole + 1
|
|
LAST_SEGMENT_ROLE = QtCore.Qt.UserRole + 2
|
|
|
|
|
|
class BreadcrumbItem(QtGui.QStandardItem):
|
|
def __init__(self, *args, **kwargs):
|
|
self._display_value = None
|
|
self._edit_value = None
|
|
super(BreadcrumbItem, self).__init__(*args, **kwargs)
|
|
|
|
def data(self, role=None):
|
|
if role == QtCore.Qt.DisplayRole:
|
|
return self._display_value
|
|
|
|
if role == QtCore.Qt.EditRole:
|
|
return self._edit_value
|
|
|
|
if role is None:
|
|
args = tuple()
|
|
else:
|
|
args = (role, )
|
|
return super(BreadcrumbItem, self).data(*args)
|
|
|
|
def setData(self, value, role):
|
|
if role == QtCore.Qt.DisplayRole:
|
|
self._display_value = value
|
|
return True
|
|
|
|
if role == QtCore.Qt.EditRole:
|
|
self._edit_value = value
|
|
return True
|
|
|
|
if role is None:
|
|
args = (value, )
|
|
else:
|
|
args = (value, role)
|
|
return super(BreadcrumbItem, self).setData(*args)
|
|
|
|
|
|
class BreadcrumbsModel(QtGui.QStandardItemModel):
|
|
def __init__(self):
|
|
super(BreadcrumbsModel, self).__init__()
|
|
self.current_path = ""
|
|
|
|
self.reset()
|
|
|
|
def reset(self):
|
|
return
|
|
|
|
|
|
class SettingsBreadcrumbs(BreadcrumbsModel):
|
|
def __init__(self):
|
|
self.entity = None
|
|
|
|
self.entities_by_path = {}
|
|
self.dynamic_paths = set()
|
|
|
|
super(SettingsBreadcrumbs, self).__init__()
|
|
|
|
def set_entity(self, entity):
|
|
self.entities_by_path = {}
|
|
self.dynamic_paths = set()
|
|
self.entity = entity
|
|
self.reset()
|
|
|
|
def has_children(self, path):
|
|
for key in self.entities_by_path.keys():
|
|
if key.startswith(path):
|
|
return True
|
|
return False
|
|
|
|
def get_valid_path(self, path):
|
|
if not path:
|
|
return ""
|
|
|
|
path_items = path.split("/")
|
|
new_path_items = []
|
|
entity = self.entity
|
|
for item in path_items:
|
|
if not entity.has_child_with_key(item):
|
|
break
|
|
|
|
new_path_items.append(item)
|
|
entity = entity[item]
|
|
|
|
return "/".join(new_path_items)
|
|
|
|
def is_valid_path(self, path):
|
|
if not path:
|
|
return True
|
|
|
|
path_items = path.split("/")
|
|
|
|
entity = self.entity
|
|
for item in path_items:
|
|
if not entity.has_child_with_key(item):
|
|
return False
|
|
|
|
entity = entity[item]
|
|
|
|
return True
|
|
|
|
|
|
class SystemSettingsBreadcrumbs(SettingsBreadcrumbs):
|
|
def reset(self):
|
|
root_item = self.invisibleRootItem()
|
|
rows = root_item.rowCount()
|
|
if rows > 0:
|
|
root_item.removeRows(0, rows)
|
|
|
|
if self.entity is None:
|
|
return
|
|
|
|
entities_by_path = self.entity.collect_static_entities_by_path()
|
|
self.entities_by_path = entities_by_path
|
|
items = []
|
|
for path in entities_by_path.keys():
|
|
if not path:
|
|
continue
|
|
path_items = path.split("/")
|
|
value = path
|
|
label = path_items.pop(-1)
|
|
prefix = "/".join(path_items)
|
|
if prefix:
|
|
prefix += "/"
|
|
|
|
item = QtGui.QStandardItem(value)
|
|
item.setData(label, LAST_SEGMENT_ROLE)
|
|
item.setData(prefix, PREFIX_ROLE)
|
|
|
|
items.append(item)
|
|
|
|
root_item.appendRows(items)
|
|
|
|
|
|
class ProjectSettingsBreadcrumbs(SettingsBreadcrumbs):
|
|
def reset(self):
|
|
root_item = self.invisibleRootItem()
|
|
rows = root_item.rowCount()
|
|
if rows > 0:
|
|
root_item.removeRows(0, rows)
|
|
|
|
if self.entity is None:
|
|
return
|
|
|
|
entities_by_path = self.entity.collect_static_entities_by_path()
|
|
self.entities_by_path = entities_by_path
|
|
items = []
|
|
for path in entities_by_path.keys():
|
|
if not path:
|
|
continue
|
|
path_items = path.split("/")
|
|
value = path
|
|
label = path_items.pop(-1)
|
|
prefix = "/".join(path_items)
|
|
if prefix:
|
|
prefix += "/"
|
|
|
|
item = QtGui.QStandardItem(value)
|
|
item.setData(label, LAST_SEGMENT_ROLE)
|
|
item.setData(prefix, PREFIX_ROLE)
|
|
|
|
items.append(item)
|
|
|
|
root_item.appendRows(items)
|
|
|
|
|
|
class BreadcrumbsProxy(QtCore.QSortFilterProxyModel):
|
|
def __init__(self, *args, **kwargs):
|
|
super(BreadcrumbsProxy, self).__init__(*args, **kwargs)
|
|
|
|
self._current_path = ""
|
|
|
|
def set_path_prefix(self, prefix):
|
|
path = prefix
|
|
if not prefix.endswith("/"):
|
|
path_items = path.split("/")
|
|
if len(path_items) == 1:
|
|
path = ""
|
|
else:
|
|
path_items.pop(-1)
|
|
path = "/".join(path_items) + "/"
|
|
|
|
if path == self._current_path:
|
|
return
|
|
|
|
self._current_path = prefix
|
|
|
|
self.invalidateFilter()
|
|
|
|
def filterAcceptsRow(self, row, parent):
|
|
index = self.sourceModel().index(row, 0, parent)
|
|
prefix_path = index.data(PREFIX_ROLE)
|
|
return prefix_path == self._current_path
|
|
|
|
|
|
class BreadcrumbsHintMenu(QtWidgets.QMenu):
|
|
def __init__(self, model, path_prefix, parent):
|
|
super(BreadcrumbsHintMenu, self).__init__(parent)
|
|
|
|
self._path_prefix = path_prefix
|
|
self._model = model
|
|
|
|
def showEvent(self, event):
|
|
self.clear()
|
|
|
|
self._model.set_path_prefix(self._path_prefix)
|
|
|
|
row_count = self._model.rowCount()
|
|
if row_count == 0:
|
|
action = self.addAction("* Nothing")
|
|
action.setData(".")
|
|
else:
|
|
for row in range(self._model.rowCount()):
|
|
index = self._model.index(row, 0)
|
|
label = index.data(LAST_SEGMENT_ROLE)
|
|
value = index.data(QtCore.Qt.EditRole)
|
|
action = self.addAction(label)
|
|
action.setData(value)
|
|
|
|
super(BreadcrumbsHintMenu, self).showEvent(event)
|
|
|
|
|
|
class ClickableWidget(QtWidgets.QWidget):
|
|
clicked = QtCore.Signal()
|
|
|
|
def mouseReleaseEvent(self, event):
|
|
if event.button() == QtCore.Qt.LeftButton:
|
|
self.clicked.emit()
|
|
super(ClickableWidget, self).mouseReleaseEvent(event)
|
|
|
|
|
|
class BreadcrumbsPathInput(QtWidgets.QLineEdit):
|
|
cancelled = QtCore.Signal()
|
|
confirmed = QtCore.Signal()
|
|
|
|
def __init__(self, model, proxy_model, parent):
|
|
super(BreadcrumbsPathInput, self).__init__(parent)
|
|
|
|
self.setObjectName("BreadcrumbsPathInput")
|
|
|
|
self.setFrame(False)
|
|
|
|
completer = QtWidgets.QCompleter(self)
|
|
completer.setCaseSensitivity(QtCore.Qt.CaseInsensitive)
|
|
completer.setModel(proxy_model)
|
|
|
|
popup = completer.popup()
|
|
popup.setUniformItemSizes(True)
|
|
popup.setLayoutMode(QtWidgets.QListView.Batched)
|
|
|
|
self.setCompleter(completer)
|
|
|
|
completer.activated.connect(self._on_completer_activated)
|
|
self.textEdited.connect(self._on_text_change)
|
|
|
|
self._completer = completer
|
|
self._model = model
|
|
self._proxy_model = proxy_model
|
|
|
|
self._context_menu_visible = False
|
|
|
|
def set_model(self, model):
|
|
self._model = model
|
|
|
|
def event(self, event):
|
|
if (
|
|
event.type() == QtCore.QEvent.KeyPress
|
|
and event.key() == QtCore.Qt.Key_Tab
|
|
):
|
|
if self._model:
|
|
find_value = self.text() + "/"
|
|
if self._model.has_children(find_value):
|
|
self.insert("/")
|
|
else:
|
|
self._completer.popup().hide()
|
|
event.accept()
|
|
return True
|
|
|
|
return super(BreadcrumbsPathInput, self).event(event)
|
|
|
|
def keyPressEvent(self, event):
|
|
if event.key() == QtCore.Qt.Key_Escape:
|
|
self.cancelled.emit()
|
|
return
|
|
|
|
if event.key() in (QtCore.Qt.Key_Return, QtCore.Qt.Key_Enter):
|
|
self.confirmed.emit()
|
|
return
|
|
|
|
super(BreadcrumbsPathInput, self).keyPressEvent(event)
|
|
|
|
def focusOutEvent(self, event):
|
|
if not self._context_menu_visible:
|
|
self.cancelled.emit()
|
|
|
|
self._context_menu_visible = False
|
|
super(BreadcrumbsPathInput, self).focusOutEvent(event)
|
|
|
|
def contextMenuEvent(self, event):
|
|
self._context_menu_visible = True
|
|
super(BreadcrumbsPathInput, self).contextMenuEvent(event)
|
|
|
|
def _on_completer_activated(self, path):
|
|
self.confirmed.emit()
|
|
|
|
def _on_text_change(self, path):
|
|
self._proxy_model.set_path_prefix(path)
|
|
|
|
|
|
class BreadcrumbsButton(QtWidgets.QToolButton):
|
|
path_selected = QtCore.Signal(str)
|
|
|
|
def __init__(self, path, model, parent):
|
|
super(BreadcrumbsButton, self).__init__(parent)
|
|
|
|
self.setObjectName("BreadcrumbsButton")
|
|
|
|
path_prefix = path
|
|
if path:
|
|
path_prefix += "/"
|
|
|
|
self.setAutoRaise(True)
|
|
self.setPopupMode(QtWidgets.QToolButton.MenuButtonPopup)
|
|
|
|
self.setMouseTracking(True)
|
|
|
|
if path:
|
|
self.setText(path.split("/")[-1])
|
|
else:
|
|
self.setProperty("empty", "1")
|
|
|
|
menu = BreadcrumbsHintMenu(model, path_prefix, self)
|
|
|
|
self.setMenu(menu)
|
|
|
|
# fixed size breadcrumbs
|
|
self.setMinimumSize(self.minimumSizeHint())
|
|
size_policy = self.sizePolicy()
|
|
size_policy.setVerticalPolicy(QtWidgets.QSizePolicy.Minimum)
|
|
self.setSizePolicy(size_policy)
|
|
|
|
menu.triggered.connect(self._on_menu_click)
|
|
# Don't allow to go to root with mouse click
|
|
if path:
|
|
self.clicked.connect(self._on_click)
|
|
|
|
self._path = path
|
|
self._path_prefix = path_prefix
|
|
self._model = model
|
|
self._menu = menu
|
|
|
|
def _on_click(self):
|
|
self.path_selected.emit(self._path)
|
|
|
|
def _on_menu_click(self, action):
|
|
item = action.data()
|
|
self.path_selected.emit(item)
|
|
|
|
|
|
class BreadcrumbsAddressBar(QtWidgets.QFrame):
|
|
"Windows Explorer-like address bar"
|
|
path_changed = QtCore.Signal(str)
|
|
path_edited = QtCore.Signal(str)
|
|
|
|
def __init__(self, parent=None):
|
|
super(BreadcrumbsAddressBar, self).__init__(parent)
|
|
|
|
self.setAutoFillBackground(True)
|
|
self.setFrameShape(QtWidgets.QFrame.StyledPanel)
|
|
|
|
# Edit presented path textually
|
|
proxy_model = BreadcrumbsProxy()
|
|
path_input = BreadcrumbsPathInput(None, proxy_model, self)
|
|
path_input.setVisible(False)
|
|
|
|
path_input.cancelled.connect(self._on_input_cancel)
|
|
path_input.confirmed.connect(self._on_input_confirm)
|
|
|
|
# Container for `crumbs_panel`
|
|
crumbs_container = QtWidgets.QWidget(self)
|
|
|
|
# Container for breadcrumbs
|
|
crumbs_panel = QtWidgets.QWidget(crumbs_container)
|
|
crumbs_panel.setObjectName("BreadcrumbsPanel")
|
|
|
|
crumbs_layout = QtWidgets.QHBoxLayout()
|
|
crumbs_layout.setContentsMargins(0, 0, 0, 0)
|
|
crumbs_layout.setSpacing(0)
|
|
|
|
crumbs_cont_layout = QtWidgets.QHBoxLayout(crumbs_container)
|
|
crumbs_cont_layout.setContentsMargins(0, 0, 0, 0)
|
|
crumbs_cont_layout.setSpacing(0)
|
|
crumbs_cont_layout.addWidget(crumbs_panel)
|
|
|
|
# Clicking on empty space to the right puts the bar into edit mode
|
|
switch_space = ClickableWidget(self)
|
|
|
|
crumb_panel_layout = QtWidgets.QHBoxLayout(crumbs_panel)
|
|
crumb_panel_layout.setContentsMargins(0, 0, 0, 0)
|
|
crumb_panel_layout.setSpacing(0)
|
|
crumb_panel_layout.addLayout(crumbs_layout, 0)
|
|
crumb_panel_layout.addWidget(switch_space, 1)
|
|
|
|
switch_space.clicked.connect(self.switch_space_mouse_up)
|
|
|
|
layout = QtWidgets.QHBoxLayout(self)
|
|
layout.setContentsMargins(0, 0, 0, 0)
|
|
layout.setSpacing(0)
|
|
layout.addWidget(path_input)
|
|
layout.addWidget(crumbs_container)
|
|
|
|
self.setMaximumHeight(path_input.height())
|
|
|
|
self.crumbs_layout = crumbs_layout
|
|
self.crumbs_panel = crumbs_panel
|
|
self.switch_space = switch_space
|
|
self.path_input = path_input
|
|
self.crumbs_container = crumbs_container
|
|
|
|
self._model = None
|
|
self._proxy_model = proxy_model
|
|
|
|
self._current_path = None
|
|
|
|
def set_model(self, model):
|
|
self._model = model
|
|
self.path_input.set_model(model)
|
|
self._proxy_model.setSourceModel(model)
|
|
|
|
def _on_input_confirm(self):
|
|
self.change_path(self.path_input.text())
|
|
|
|
def _on_input_cancel(self):
|
|
self._cancel_edit()
|
|
|
|
def _clear_crumbs(self):
|
|
while self.crumbs_layout.count():
|
|
widget = self.crumbs_layout.takeAt(0).widget()
|
|
if widget:
|
|
widget.deleteLater()
|
|
|
|
def _insert_crumb(self, path):
|
|
btn = BreadcrumbsButton(path, self._proxy_model, self.crumbs_panel)
|
|
|
|
self.crumbs_layout.insertWidget(0, btn)
|
|
|
|
btn.path_selected.connect(self._on_crumb_clicked)
|
|
|
|
def _on_crumb_clicked(self, path):
|
|
"Breadcrumb was clicked"
|
|
self.change_path(path)
|
|
|
|
def change_path(self, path):
|
|
path = self._model.get_valid_path(path)
|
|
if self._model and not self._model.is_valid_path(path):
|
|
self._show_address_field()
|
|
else:
|
|
self.set_path(path)
|
|
self.path_edited.emit(path)
|
|
|
|
def set_path(self, path):
|
|
if path is None or path == ".":
|
|
path = self._current_path
|
|
|
|
# exit edit mode
|
|
self._cancel_edit()
|
|
|
|
self._clear_crumbs()
|
|
self._current_path = path
|
|
self.path_input.setText(path)
|
|
path_items = [
|
|
item
|
|
for item in path.split("/")
|
|
if item
|
|
]
|
|
while path_items:
|
|
item = "/".join(path_items)
|
|
self._insert_crumb(item)
|
|
path_items.pop(-1)
|
|
self._insert_crumb("")
|
|
|
|
self.path_changed.emit(self._current_path)
|
|
|
|
def _cancel_edit(self):
|
|
"Set edit line text back to current path and switch to view mode"
|
|
# revert path
|
|
self.path_input.setText(self.path())
|
|
# switch back to breadcrumbs view
|
|
self._show_address_field(False)
|
|
|
|
def path(self):
|
|
"Get path displayed in this BreadcrumbsAddressBar"
|
|
return self._current_path
|
|
|
|
def switch_space_mouse_up(self):
|
|
"EVENT: switch_space mouse clicked"
|
|
self._show_address_field(True)
|
|
|
|
def _show_address_field(self, show=True):
|
|
"Show text address field"
|
|
self.crumbs_container.setVisible(not show)
|
|
self.path_input.setVisible(show)
|
|
if show:
|
|
self.path_input.setFocus()
|
|
self.path_input.selectAll()
|
|
|
|
def minimumSizeHint(self):
|
|
result = super(BreadcrumbsAddressBar, self).minimumSizeHint()
|
|
result.setHeight(self.path_input.minimumSizeHint().height())
|
|
return result
|