ayon-core/openpype/modules/log_viewer/tray/widgets.py
2021-12-02 16:29:53 +01:00

323 lines
9.8 KiB
Python

from Qt import QtCore, QtWidgets
from avalon.vendor import qtawesome
from .models import LogModel, LogsFilterProxy
class SearchComboBox(QtWidgets.QComboBox):
"""Searchable ComboBox with empty placeholder value as first value"""
def __init__(self, parent=None, placeholder=""):
super(SearchComboBox, self).__init__(parent)
self.setEditable(True)
self.setInsertPolicy(self.NoInsert)
self.lineEdit().setPlaceholderText(placeholder)
# Apply completer settings
completer = self.completer()
completer.setCompletionMode(completer.PopupCompletion)
completer.setCaseSensitivity(QtCore.Qt.CaseInsensitive)
# Force style sheet on popup menu
# It won't take the parent stylesheet for some reason
# todo: better fix for completer popup stylesheet
if parent:
popup = completer.popup()
popup.setStyleSheet(parent.styleSheet())
self.currentIndexChanged.connect(self.onIndexChange)
def onIndexChange(self, index):
print(index)
def populate(self, items):
self.clear()
self.addItems([""]) # ensure first item is placeholder
self.addItems(items)
def get_valid_value(self):
"""Return the current text if it's a valid value else None
Note: The empty placeholder value is valid and returns as ""
"""
text = self.currentText()
lookup = set(self.itemText(i) for i in range(self.count()))
if text not in lookup:
return None
return text
class SelectableMenu(QtWidgets.QMenu):
selection_changed = QtCore.Signal()
def mouseReleaseEvent(self, event):
action = self.activeAction()
if action and action.isEnabled():
action.trigger()
self.selection_changed.emit()
else:
super(SelectableMenu, self).mouseReleaseEvent(event)
class CustomCombo(QtWidgets.QWidget):
selection_changed = QtCore.Signal()
def __init__(self, title, parent=None):
super(CustomCombo, self).__init__(parent)
toolbutton = QtWidgets.QToolButton(self)
toolbutton.setText(title)
toolmenu = SelectableMenu(self)
toolbutton.setMenu(toolmenu)
toolbutton.setPopupMode(QtWidgets.QToolButton.MenuButtonPopup)
layout = QtWidgets.QHBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.addWidget(toolbutton)
toolmenu.selection_changed.connect(self.selection_changed)
self.toolbutton = toolbutton
self.toolmenu = toolmenu
self.main_layout = layout
def populate(self, items):
self.toolmenu.clear()
self.addItems(items)
def addItems(self, items):
for item in items:
action = self.toolmenu.addAction(item)
action.setCheckable(True)
action.setChecked(True)
self.toolmenu.addAction(action)
def items(self):
for action in self.toolmenu.actions():
yield action
class LogsWidget(QtWidgets.QWidget):
"""A widget that lists the published subsets for an asset"""
def __init__(self, detail_widget, parent=None):
super(LogsWidget, self).__init__(parent=parent)
model = LogModel()
proxy_model = LogsFilterProxy()
proxy_model.setSourceModel(model)
proxy_model.col_usernames = model.COLUMNS.index("username")
filter_layout = QtWidgets.QHBoxLayout()
user_filter = CustomCombo("Users", self)
users = model.dbcon.distinct("username")
user_filter.populate(users)
user_filter.selection_changed.connect(self._user_changed)
proxy_model.update_users_filter(users)
level_filter = CustomCombo("Levels", self)
levels = model.dbcon.distinct("level")
level_filter.addItems(levels)
level_filter.selection_changed.connect(self._level_changed)
detail_widget.update_level_filter(levels)
icon = qtawesome.icon("fa.refresh", color="white")
refresh_btn = QtWidgets.QPushButton(icon, "")
filter_layout.addWidget(user_filter)
filter_layout.addWidget(level_filter)
filter_layout.addStretch(1)
filter_layout.addWidget(refresh_btn)
view = QtWidgets.QTreeView(self)
view.setEditTriggers(QtWidgets.QAbstractItemView.NoEditTriggers)
layout = QtWidgets.QVBoxLayout(self)
layout.setContentsMargins(0, 0, 0, 0)
layout.addLayout(filter_layout)
layout.addWidget(view)
view.setModel(proxy_model)
view.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
view.setSortingEnabled(True)
view.sortByColumn(
model.COLUMNS.index("started"),
QtCore.Qt.DescendingOrder
)
view.selectionModel().selectionChanged.connect(self._on_index_change)
refresh_btn.clicked.connect(self._on_refresh_clicked)
# Store to memory
self.model = model
self.proxy_model = proxy_model
self.view = view
self.user_filter = user_filter
self.level_filter = level_filter
self.detail_widget = detail_widget
self.refresh_btn = refresh_btn
# prepare
self.refresh()
def refresh(self):
self.model.refresh()
self.detail_widget.refresh()
def _on_refresh_clicked(self):
self.refresh()
def _on_index_change(self, to_index, from_index):
index = self._selected_log()
if index:
logs = index.data(self.model.ROLE_LOGS)
else:
logs = []
self.detail_widget.set_detail(logs)
def _user_changed(self):
checked_values = set()
for action in self.user_filter.items():
if action.isChecked():
checked_values.add(action.text())
self.proxy_model.update_users_filter(checked_values)
def _level_changed(self):
checked_values = set()
for action in self.level_filter.items():
if action.isChecked():
checked_values.add(action.text())
self.detail_widget.update_level_filter(checked_values)
def on_context_menu(self, point):
# TODO will be any actions? it's ready
return
point_index = self.view.indexAt(point)
if not point_index.isValid():
return
# Get selected subsets without groups
selection = self.view.selectionModel()
rows = selection.selectedRows(column=0)
def _selected_log(self):
selection = self.view.selectionModel()
rows = selection.selectedRows(column=0)
if len(rows) == 1:
return rows[0]
return None
class OutputWidget(QtWidgets.QWidget):
def __init__(self, parent=None):
super(OutputWidget, self).__init__(parent=parent)
layout = QtWidgets.QVBoxLayout(self)
show_timecode_checkbox = QtWidgets.QCheckBox("Show timestamp", self)
output_text = QtWidgets.QTextEdit(self)
output_text.setReadOnly(True)
# output_text.setLineWrapMode(QtWidgets.QTextEdit.FixedPixelWidth)
layout.addWidget(show_timecode_checkbox)
layout.addWidget(output_text)
show_timecode_checkbox.stateChanged.connect(
self.on_show_timecode_change
)
self.setLayout(layout)
self.output_text = output_text
self.show_timecode_checkbox = show_timecode_checkbox
self.refresh()
def refresh(self):
self.set_detail()
def show_timecode(self):
return self.show_timecode_checkbox.isChecked()
def on_show_timecode_change(self):
self.set_detail(self.las_logs)
def update_level_filter(self, levels):
self.filter_levels = set()
for level in levels or tuple():
self.filter_levels.add(level.lower())
self.set_detail(self.las_logs)
def add_line(self, line):
self.output_text.append(line)
def set_detail(self, logs=None):
self.las_logs = logs
self.output_text.clear()
if not logs:
return
show_timecode = self.show_timecode()
for log in logs:
level = log["level"].lower()
if level not in self.filter_levels:
continue
line_f = "<font color=\"White\">{message}"
if level == "debug":
line_f = (
"<font color=\"Yellow\"> -"
" <font color=\"Lime\">{{ {loggerName} }}: ["
" <font color=\"White\">{message}"
" <font color=\"Lime\">]"
)
elif level == "info":
line_f = (
"<font color=\"Lime\">>>> ["
" <font color=\"White\">{message}"
" <font color=\"Lime\">]"
)
elif level == "warning":
line_f = (
"<font color=\"Yellow\">*** WRN:"
" <font color=\"Lime\"> >>> {{ {loggerName} }}: ["
" <font color=\"White\">{message}"
" <font color=\"Lime\">]"
)
elif level == "error":
line_f = (
"<font color=\"Red\">!!! ERR:"
" <font color=\"White\">{timestamp}"
" <font color=\"Lime\">>>> {{ {loggerName} }}: ["
" <font color=\"White\">{message}"
" <font color=\"Lime\">]"
)
exc = log.get("exception")
if exc:
log["message"] = exc["message"]
line = line_f.format(**log)
if show_timecode:
timestamp = log["timestamp"]
line = timestamp.strftime("%Y-%d-%m %H:%M:%S") + " " + line
self.add_line(line)
if not exc:
continue
for _line in exc["stackTrace"].split("\n"):
self.add_line(_line)