artiq/artiq/gui/log.py

339 lines
12 KiB
Python
Raw Normal View History

2015-07-22 05:13:50 +08:00
import asyncio
import logging
import time
import re
2015-11-12 01:13:57 +08:00
from functools import partial
2015-07-22 05:13:50 +08:00
2016-02-15 07:23:47 +08:00
from PyQt5 import QtCore, QtGui, QtWidgets
2015-05-24 00:38:30 +08:00
2016-02-15 07:05:30 +08:00
from artiq.gui.tools import (LayoutWidget, log_level_to_name,
QDockWidgetCloseDetect)
2015-07-22 05:13:50 +08:00
def _make_wrappable(row, width=30):
level, source, time, msg = row
msg = re.sub("(\\S{{{}}})".format(width), "\\1\u200b", msg)
return [level, source, time, msg]
2015-11-11 12:13:19 +08:00
class Model(QtCore.QAbstractTableModel):
def __init__(self, init):
QtCore.QAbstractTableModel.__init__(self)
2015-10-31 00:34:14 +08:00
self.headers = ["Source", "Message"]
2015-10-31 00:34:14 +08:00
self.entries = list(map(_make_wrappable, init))
2015-10-31 00:34:14 +08:00
self.pending_entries = []
self.depth = 1000
timer = QtCore.QTimer(self)
timer.timeout.connect(self.timer_tick)
timer.start(100)
self.fixed_font = QtGui.QFontDatabase.systemFont(QtGui.QFontDatabase.FixedFont)
2015-07-25 11:38:26 +08:00
2015-10-14 16:30:16 +08:00
self.white = QtGui.QBrush(QtGui.QColor(255, 255, 255))
self.black = QtGui.QBrush(QtGui.QColor(0, 0, 0))
self.debug_fg = QtGui.QBrush(QtGui.QColor(55, 55, 55))
self.warning_bg = QtGui.QBrush(QtGui.QColor(255, 255, 180))
self.error_bg = QtGui.QBrush(QtGui.QColor(255, 150, 150))
2015-10-31 00:34:14 +08:00
def headerData(self, col, orientation, role):
if (orientation == QtCore.Qt.Horizontal
and role == QtCore.Qt.DisplayRole):
return self.headers[col]
return None
def rowCount(self, parent):
return len(self.entries)
def columnCount(self, parent):
return len(self.headers)
def __delitem__(self, k):
pass
def append(self, v):
self.pending_entries.append(_make_wrappable(v))
2015-10-31 00:34:14 +08:00
def insertRows(self, position, rows=1, index=QtCore.QModelIndex()):
self.beginInsertRows(QtCore.QModelIndex(), position, position+rows-1)
self.endInsertRows()
def removeRows(self, position, rows=1, index=QtCore.QModelIndex()):
self.beginRemoveRows(QtCore.QModelIndex(), position, position+rows-1)
self.endRemoveRows()
def timer_tick(self):
if not self.pending_entries:
return
nrows = len(self.entries)
records = self.pending_entries
self.pending_entries = []
self.entries.extend(records)
self.insertRows(nrows, len(records))
if len(self.entries) > self.depth:
start = len(self.entries) - self.depth
self.entries = self.entries[start:]
self.removeRows(0, start)
2015-07-25 11:38:26 +08:00
def data(self, index, role):
2015-10-31 00:34:14 +08:00
if index.isValid():
if (role == QtCore.Qt.FontRole
and index.column() == 1):
2015-10-31 00:34:14 +08:00
return self.fixed_font
2016-02-10 13:34:11 +08:00
elif role == QtCore.Qt.TextAlignmentRole:
return QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop
2015-10-31 00:34:14 +08:00
elif role == QtCore.Qt.BackgroundRole:
level = self.entries[index.row()][0]
if level >= logging.ERROR:
return self.error_bg
elif level >= logging.WARNING:
return self.warning_bg
else:
return self.white
elif role == QtCore.Qt.ForegroundRole:
level = self.entries[index.row()][0]
if level <= logging.DEBUG:
return self.debug_fg
else:
return self.black
elif role == QtCore.Qt.DisplayRole:
v = self.entries[index.row()]
column = index.column()
if column == 0:
return v[1]
else:
return v[3]
elif role == QtCore.Qt.ToolTipRole:
v = self.entries[index.row()]
return (log_level_to_name(v[0]) + ", " +
time.strftime("%m/%d %H:%M:%S", time.localtime(v[2])))
2015-07-22 05:13:50 +08:00
2015-05-24 00:38:30 +08:00
2016-01-10 18:38:24 +08:00
class _LogFilterProxyModel(QtCore.QSortFilterProxyModel):
2015-10-30 19:58:33 +08:00
def __init__(self, min_level, freetext):
2016-01-10 18:38:24 +08:00
QtCore.QSortFilterProxyModel.__init__(self)
2015-10-14 21:21:19 +08:00
self.min_level = min_level
2015-10-30 19:58:33 +08:00
self.freetext = freetext
2015-10-14 21:21:19 +08:00
def filterAcceptsRow(self, sourceRow, sourceParent):
model = self.sourceModel()
2015-10-30 19:58:33 +08:00
accepted_level = model.entries[sourceRow][0] >= self.min_level
2015-10-30 19:58:33 +08:00
if self.freetext:
data_source = model.entries[sourceRow][1]
data_message = model.entries[sourceRow][3]
2015-10-30 19:58:33 +08:00
accepted_freetext = (self.freetext in data_source
or self.freetext in data_message)
else:
accepted_freetext = True
return accepted_level and accepted_freetext
2015-10-14 21:21:19 +08:00
def set_min_level(self, min_level):
self.min_level = min_level
self.invalidateFilter()
2015-10-30 19:58:33 +08:00
def set_freetext(self, freetext):
self.freetext = freetext
self.invalidateFilter()
2015-10-14 21:21:19 +08:00
2016-02-15 05:22:07 +08:00
class _LogDock(QDockWidgetCloseDetect):
2015-11-12 01:13:57 +08:00
def __init__(self, manager, name, log_sub):
2016-02-15 05:22:07 +08:00
QDockWidgetCloseDetect.__init__(self, "Log")
self.setObjectName(name)
2015-07-22 05:13:50 +08:00
2015-10-14 21:21:19 +08:00
grid = LayoutWidget()
self.setWidget(grid)
2015-10-14 21:21:19 +08:00
2016-02-15 07:23:47 +08:00
grid.addWidget(QtWidgets.QLabel("Minimum level: "), 0, 0)
self.filter_level = QtWidgets.QComboBox()
2015-10-30 19:58:33 +08:00
self.filter_level.addItems(["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"])
self.filter_level.setToolTip("Display entries at or above this level")
grid.addWidget(self.filter_level, 0, 1)
self.filter_level.currentIndexChanged.connect(
self.filter_level_changed)
2016-02-15 07:23:47 +08:00
self.filter_freetext = QtWidgets.QLineEdit()
2015-10-30 19:58:33 +08:00
self.filter_freetext.setPlaceholderText("freetext filter...")
self.filter_freetext.editingFinished.connect(
self.filter_freetext_changed)
grid.addWidget(self.filter_freetext, 0, 2)
2015-10-14 21:21:19 +08:00
2016-02-15 07:23:47 +08:00
scrollbottom = QtWidgets.QToolButton()
2016-01-17 04:06:45 +08:00
scrollbottom.setToolTip("Scroll to bottom")
2016-02-15 07:23:47 +08:00
scrollbottom.setIcon(QtWidgets.QApplication.style().standardIcon(
QtWidgets.QStyle.SP_ArrowDown))
2016-01-17 04:06:45 +08:00
grid.addWidget(scrollbottom, 0, 3)
scrollbottom.clicked.connect(self.scroll_to_bottom)
2016-02-15 07:23:47 +08:00
newdock = QtWidgets.QToolButton()
newdock.setToolTip("Create new log dock")
2016-02-15 07:23:47 +08:00
newdock.setIcon(QtWidgets.QApplication.style().standardIcon(
QtWidgets.QStyle.SP_FileDialogNewFolder))
# note the lambda, the default parameter is overriden otherwise
newdock.clicked.connect(lambda: manager.create_new_dock())
2016-01-17 04:06:45 +08:00
grid.addWidget(newdock, 0, 4)
grid.layout.setColumnStretch(2, 1)
2016-02-15 07:23:47 +08:00
self.log = QtWidgets.QTableView()
self.log.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection)
self.log.horizontalHeader().setSectionResizeMode(
QtWidgets.QHeaderView.ResizeToContents)
2015-11-24 23:04:01 +08:00
self.log.horizontalHeader().setStretchLastSection(True)
2016-02-15 07:23:47 +08:00
self.log.verticalHeader().setSectionResizeMode(
QtWidgets.QHeaderView.ResizeToContents)
2015-11-24 23:04:01 +08:00
self.log.verticalHeader().hide()
2015-07-23 23:06:37 +08:00
self.log.setHorizontalScrollMode(
2016-02-15 07:23:47 +08:00
QtWidgets.QAbstractItemView.ScrollPerPixel)
2015-11-27 19:18:20 +08:00
self.log.setVerticalScrollMode(
2016-02-15 07:23:47 +08:00
QtWidgets.QAbstractItemView.ScrollPerPixel)
2015-07-23 23:06:37 +08:00
self.log.setShowGrid(False)
self.log.setTextElideMode(QtCore.Qt.ElideNone)
2016-01-17 04:06:45 +08:00
grid.addWidget(self.log, 1, 0, colspan=5)
2015-07-24 00:11:05 +08:00
self.scroll_at_bottom = False
2015-11-12 01:13:57 +08:00
self.scroll_value = 0
2015-11-11 12:13:19 +08:00
log_sub.add_setmodel_callback(self.set_model)
2015-07-22 05:13:50 +08:00
2015-10-30 19:58:33 +08:00
def filter_level_changed(self):
if not hasattr(self, "table_model_filter"):
return
2015-10-14 21:21:19 +08:00
self.table_model_filter.set_min_level(
2015-10-30 19:58:33 +08:00
getattr(logging, self.filter_level.currentText()))
def filter_freetext_changed(self):
if not hasattr(self, "table_model_filter"):
return
2015-10-30 19:58:33 +08:00
self.table_model_filter.set_freetext(self.filter_freetext.text())
2015-10-14 21:21:19 +08:00
2016-01-17 04:06:45 +08:00
def scroll_to_bottom(self):
self.log.scrollToBottom()
2015-07-24 00:11:05 +08:00
def rows_inserted_before(self):
scrollbar = self.log.verticalScrollBar()
self.scroll_value = scrollbar.value()
self.scroll_at_bottom = self.scroll_value == scrollbar.maximum()
2015-07-24 00:11:05 +08:00
def rows_inserted_after(self):
if self.scroll_at_bottom:
self.log.scrollToBottom()
2016-01-16 09:26:05 +08:00
# HACK:
# If we don't do this, after we first add some rows, the "Time"
# column gets undersized and the text in it gets wrapped.
# We can call self.log.resizeColumnsToContents(), which fixes
# that problem, but now the message column is too large and
# a horizontal scrollbar appears.
# This is almost certainly a Qt layout bug.
self.log.horizontalHeader().reset()
# HACK:
# Qt intermittently likes to scroll back to the top when rows are removed.
# Work around this by restoring the scrollbar to the previously memorized
# position, after the removal.
# Note that this works because _LogModel always does the insertion right
# before the removal.
def rows_removed(self):
if self.scroll_at_bottom:
self.log.scrollToBottom()
else:
scrollbar = self.log.verticalScrollBar()
scrollbar.setValue(self.scroll_value)
2015-11-11 12:13:19 +08:00
def set_model(self, model):
self.table_model = model
2015-10-30 19:58:33 +08:00
self.table_model_filter = _LogFilterProxyModel(
getattr(logging, self.filter_level.currentText()),
self.filter_freetext.text())
self.table_model_filter.setSourceModel(self.table_model)
2015-10-14 21:21:19 +08:00
self.log.setModel(self.table_model_filter)
self.table_model_filter.rowsAboutToBeInserted.connect(self.rows_inserted_before)
self.table_model_filter.rowsInserted.connect(self.rows_inserted_after)
self.table_model_filter.rowsRemoved.connect(self.rows_removed)
2015-10-14 21:21:19 +08:00
2016-02-18 08:09:51 +08:00
asyncio.get_event_loop().call_soon(self.log.scrollToBottom)
2015-10-14 21:21:19 +08:00
def save_state(self):
2015-11-12 01:13:57 +08:00
return {
"min_level_idx": self.filter_level.currentIndex(),
"freetext_filter": self.filter_freetext.text()
}
2015-10-14 21:21:19 +08:00
def restore_state(self, state):
try:
idx = state["min_level_idx"]
except KeyError:
pass
else:
2015-10-30 19:58:33 +08:00
self.filter_level.setCurrentIndex(idx)
2015-11-12 01:13:57 +08:00
try:
freetext = state["freetext_filter"]
except KeyError:
pass
else:
self.filter_freetext.setText(freetext)
# Note that editingFinished is not emitted when calling setText,
# (unlike currentIndexChanged) so we need to call the callback
# manually here, unlike for the combobox.
self.filter_freetext_changed()
class LogDockManager:
def __init__(self, main_window, log_sub):
self.main_window = main_window
2015-11-12 01:13:57 +08:00
self.log_sub = log_sub
self.docks = dict()
def create_new_dock(self, add_to_area=True):
n = 0
name = "log0"
while name in self.docks:
n += 1
name = "log" + str(n)
2015-11-27 19:18:20 +08:00
dock = _LogDock(self, name, self.log_sub)
2015-11-12 01:13:57 +08:00
self.docks[name] = dock
if add_to_area:
self.main_window.addDockWidget(QtCore.Qt.RightDockWidgetArea, dock)
dock.setFloating(True)
2015-11-12 01:13:57 +08:00
dock.sigClosed.connect(partial(self.on_dock_closed, name))
self.update_closable()
return dock
def on_dock_closed(self, name):
dock = self.docks[name]
dock.setParent(None)
dock.deleteLater()
2015-11-12 01:13:57 +08:00
del self.docks[name]
self.update_closable()
def update_closable(self):
flags = (QtWidgets.QDockWidget.DockWidgetMovable |
QtWidgets.QDockWidget.DockWidgetFloatable)
if len(self.docks) > 1:
flags |= QtWidgets.QDockWidget.DockWidgetClosable
2015-11-12 01:13:57 +08:00
for dock in self.docks.values():
dock.setFeatures(flags)
2015-11-12 01:13:57 +08:00
def save_state(self):
return {name: dock.save_state() for name, dock in self.docks.items()}
def restore_state(self, state):
if self.docks:
raise NotImplementedError
for name, dock_state in state.items():
2015-11-27 19:18:20 +08:00
dock = _LogDock(self, name, self.log_sub)
2016-02-21 05:33:24 +08:00
self.docks[name] = dock
2015-11-12 01:13:57 +08:00
dock.restore_state(dock_state)
self.main_window.addDockWidget(QtCore.Qt.RightDockWidgetArea, dock)
2016-02-21 05:33:24 +08:00
dock.sigClosed.connect(partial(self.on_dock_closed, name))
self.update_closable()
2015-11-12 01:13:57 +08:00
def first_log_dock(self):
if self.docks:
return None
dock = self.create_new_dock(False)
return dock