forked from M-Labs/artiq
This commit is contained in:
parent
b190581102
commit
bebd89c959
|
@ -130,6 +130,7 @@ def main():
|
||||||
|
|
||||||
d_datasets = datasets.DatasetsDock(sub_clients["datasets"],
|
d_datasets = datasets.DatasetsDock(sub_clients["datasets"],
|
||||||
rpc_clients["dataset_db"])
|
rpc_clients["dataset_db"])
|
||||||
|
smgr.register(d_datasets)
|
||||||
|
|
||||||
d_applets = applets.AppletsDock(main_window, sub_clients["datasets"])
|
d_applets = applets.AppletsDock(main_window, sub_clients["datasets"])
|
||||||
atexit_register_coroutine(d_applets.stop)
|
atexit_register_coroutine(d_applets.stop)
|
||||||
|
@ -142,6 +143,7 @@ def main():
|
||||||
|
|
||||||
d_schedule = schedule.ScheduleDock(
|
d_schedule = schedule.ScheduleDock(
|
||||||
status_bar, rpc_clients["schedule"], sub_clients["schedule"])
|
status_bar, rpc_clients["schedule"], sub_clients["schedule"])
|
||||||
|
smgr.register(d_schedule)
|
||||||
|
|
||||||
logmgr = log.LogDockManager(main_window, sub_clients["log"])
|
logmgr = log.LogDockManager(main_window, sub_clients["log"])
|
||||||
smgr.register(logmgr)
|
smgr.register(logmgr)
|
||||||
|
@ -172,7 +174,7 @@ def main():
|
||||||
# create first log dock if not already in state
|
# create first log dock if not already in state
|
||||||
d_log0 = logmgr.first_log_dock()
|
d_log0 = logmgr.first_log_dock()
|
||||||
if d_log0 is not None:
|
if d_log0 is not None:
|
||||||
main_window.tabifyDockWidget(d_shortcuts, d_log0)
|
main_window.tabifyDockWidget(d_schedule, d_log0)
|
||||||
|
|
||||||
# run
|
# run
|
||||||
main_window.show()
|
main_window.show()
|
||||||
|
|
|
@ -47,8 +47,6 @@ class DatasetsDock(QtWidgets.QDockWidget):
|
||||||
self.table = QtWidgets.QTreeView()
|
self.table = QtWidgets.QTreeView()
|
||||||
self.table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
|
self.table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
|
||||||
self.table.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection)
|
self.table.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection)
|
||||||
self.table.header().setSectionResizeMode(
|
|
||||||
QtWidgets.QHeaderView.ResizeToContents)
|
|
||||||
grid.addWidget(self.table, 1, 0)
|
grid.addWidget(self.table, 1, 0)
|
||||||
|
|
||||||
self.table.setContextMenuPolicy(QtCore.Qt.ActionsContextMenu)
|
self.table.setContextMenuPolicy(QtCore.Qt.ActionsContextMenu)
|
||||||
|
@ -79,3 +77,9 @@ class DatasetsDock(QtWidgets.QDockWidget):
|
||||||
key = self.table_model.index_to_key(idx)
|
key = self.table_model.index_to_key(idx)
|
||||||
if key is not None:
|
if key is not None:
|
||||||
asyncio.ensure_future(self.dataset_ctl.delete(key))
|
asyncio.ensure_future(self.dataset_ctl.delete(key))
|
||||||
|
|
||||||
|
def save_state(self):
|
||||||
|
return bytes(self.table.header().saveState())
|
||||||
|
|
||||||
|
def restore_state(self, state):
|
||||||
|
self.table.header().restoreState(QtCore.QByteArray(state))
|
||||||
|
|
175
artiq/gui/log.py
175
artiq/gui/log.py
|
@ -10,20 +10,24 @@ from artiq.gui.tools import (LayoutWidget, log_level_to_name,
|
||||||
QDockWidgetCloseDetect)
|
QDockWidgetCloseDetect)
|
||||||
|
|
||||||
|
|
||||||
def _make_wrappable(row, width=30):
|
class ModelItem:
|
||||||
level, source, time, msg = row
|
def __init__(self, parent, row):
|
||||||
msg = re.sub("(\\S{{{}}})".format(width), "\\1\u200b", msg)
|
self.parent = parent
|
||||||
return [level, source, time, msg]
|
self.row = row
|
||||||
|
self.children_by_row = []
|
||||||
|
|
||||||
|
|
||||||
class Model(QtCore.QAbstractTableModel):
|
class Model(QtCore.QAbstractItemModel):
|
||||||
def __init__(self, init):
|
def __init__(self, init):
|
||||||
QtCore.QAbstractTableModel.__init__(self)
|
QtCore.QAbstractTableModel.__init__(self)
|
||||||
|
|
||||||
self.headers = ["Source", "Message"]
|
self.headers = ["Source", "Message"]
|
||||||
|
self.children_by_row = []
|
||||||
|
|
||||||
self.entries = list(map(_make_wrappable, init))
|
self.entries = []
|
||||||
self.pending_entries = []
|
self.pending_entries = []
|
||||||
|
for entry in init:
|
||||||
|
self.append(entry)
|
||||||
self.depth = 1000
|
self.depth = 1000
|
||||||
timer = QtCore.QTimer(self)
|
timer = QtCore.QTimer(self)
|
||||||
timer.timeout.connect(self.timer_tick)
|
timer.timeout.connect(self.timer_tick)
|
||||||
|
@ -44,7 +48,11 @@ class Model(QtCore.QAbstractTableModel):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def rowCount(self, parent):
|
def rowCount(self, parent):
|
||||||
return len(self.entries)
|
if parent.isValid():
|
||||||
|
item = parent.internalPointer()
|
||||||
|
return len(item.children_by_row)
|
||||||
|
else:
|
||||||
|
return len(self.entries)
|
||||||
|
|
||||||
def columnCount(self, parent):
|
def columnCount(self, parent):
|
||||||
return len(self.headers)
|
return len(self.headers)
|
||||||
|
@ -53,7 +61,9 @@ class Model(QtCore.QAbstractTableModel):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def append(self, v):
|
def append(self, v):
|
||||||
self.pending_entries.append(_make_wrappable(v))
|
severity, source, timestamp, message = v
|
||||||
|
self.pending_entries.append((severity, source, timestamp,
|
||||||
|
message.split("\n")))
|
||||||
|
|
||||||
def insertRows(self, position, rows=1, index=QtCore.QModelIndex()):
|
def insertRows(self, position, rows=1, index=QtCore.QModelIndex()):
|
||||||
self.beginInsertRows(QtCore.QModelIndex(), position, position+rows-1)
|
self.beginInsertRows(QtCore.QModelIndex(), position, position+rows-1)
|
||||||
|
@ -70,44 +80,82 @@ class Model(QtCore.QAbstractTableModel):
|
||||||
records = self.pending_entries
|
records = self.pending_entries
|
||||||
self.pending_entries = []
|
self.pending_entries = []
|
||||||
self.entries.extend(records)
|
self.entries.extend(records)
|
||||||
|
for rec in records:
|
||||||
|
item = ModelItem(self, len(self.children_by_row))
|
||||||
|
self.children_by_row.append(item)
|
||||||
|
for i in range(len(rec[3])-1):
|
||||||
|
item.children_by_row.append(ModelItem(item, i))
|
||||||
self.insertRows(nrows, len(records))
|
self.insertRows(nrows, len(records))
|
||||||
|
|
||||||
if len(self.entries) > self.depth:
|
if len(self.entries) > self.depth:
|
||||||
start = len(self.entries) - self.depth
|
start = len(self.entries) - self.depth
|
||||||
self.entries = self.entries[start:]
|
self.entries = self.entries[start:]
|
||||||
|
self.children_by_row = self.children_by_row[start:]
|
||||||
|
for child in self.children_by_row:
|
||||||
|
child.row -= start
|
||||||
self.removeRows(0, start)
|
self.removeRows(0, start)
|
||||||
|
|
||||||
def data(self, index, role):
|
def index(self, row, column, parent):
|
||||||
|
if parent.isValid():
|
||||||
|
parent_item = parent.internalPointer()
|
||||||
|
return self.createIndex(row, column,
|
||||||
|
parent_item.children_by_row[row])
|
||||||
|
else:
|
||||||
|
return self.createIndex(row, column, self.children_by_row[row])
|
||||||
|
|
||||||
|
def parent(self, index):
|
||||||
if index.isValid():
|
if index.isValid():
|
||||||
if (role == QtCore.Qt.FontRole
|
parent = index.internalPointer().parent
|
||||||
and index.column() == 1):
|
if parent is self:
|
||||||
return self.fixed_font
|
return QtCore.QModelIndex()
|
||||||
elif role == QtCore.Qt.TextAlignmentRole:
|
else:
|
||||||
return QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop
|
return self.createIndex(parent.row, 0, parent)
|
||||||
elif role == QtCore.Qt.BackgroundRole:
|
else:
|
||||||
level = self.entries[index.row()][0]
|
return QtCore.QModelIndex()
|
||||||
if level >= logging.ERROR:
|
|
||||||
return self.error_bg
|
def data(self, index, role):
|
||||||
elif level >= logging.WARNING:
|
if not index.isValid():
|
||||||
return self.warning_bg
|
return
|
||||||
else:
|
|
||||||
return self.white
|
item = index.internalPointer()
|
||||||
elif role == QtCore.Qt.ForegroundRole:
|
if item.parent is self:
|
||||||
level = self.entries[index.row()][0]
|
msgnum = item.row
|
||||||
if level <= logging.DEBUG:
|
else:
|
||||||
return self.debug_fg
|
msgnum = item.parent.row
|
||||||
else:
|
|
||||||
return self.black
|
if role == QtCore.Qt.FontRole and index.column() == 1:
|
||||||
elif role == QtCore.Qt.DisplayRole:
|
return self.fixed_font
|
||||||
v = self.entries[index.row()]
|
elif role == QtCore.Qt.BackgroundRole:
|
||||||
column = index.column()
|
level = self.entries[msgnum][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[msgnum][0]
|
||||||
|
if level <= logging.DEBUG:
|
||||||
|
return self.debug_fg
|
||||||
|
else:
|
||||||
|
return self.black
|
||||||
|
elif role == QtCore.Qt.DisplayRole:
|
||||||
|
v = self.entries[msgnum]
|
||||||
|
column = index.column()
|
||||||
|
if item.parent is self:
|
||||||
if column == 0:
|
if column == 0:
|
||||||
return v[1]
|
return v[1]
|
||||||
else:
|
else:
|
||||||
return v[3]
|
return v[3][0]
|
||||||
elif role == QtCore.Qt.ToolTipRole:
|
else:
|
||||||
v = self.entries[index.row()]
|
if column == 0:
|
||||||
return (log_level_to_name(v[0]) + ", " +
|
return ""
|
||||||
time.strftime("%m/%d %H:%M:%S", time.localtime(v[2])))
|
else:
|
||||||
|
return v[3][item.row+1]
|
||||||
|
elif role == QtCore.Qt.ToolTipRole:
|
||||||
|
v = self.entries[msgnum]
|
||||||
|
return (log_level_to_name(v[0]) + ", " +
|
||||||
|
time.strftime("%m/%d %H:%M:%S", time.localtime(v[2])))
|
||||||
|
|
||||||
|
|
||||||
class _LogFilterProxyModel(QtCore.QSortFilterProxyModel):
|
class _LogFilterProxyModel(QtCore.QSortFilterProxyModel):
|
||||||
|
@ -118,14 +166,19 @@ class _LogFilterProxyModel(QtCore.QSortFilterProxyModel):
|
||||||
|
|
||||||
def filterAcceptsRow(self, sourceRow, sourceParent):
|
def filterAcceptsRow(self, sourceRow, sourceParent):
|
||||||
model = self.sourceModel()
|
model = self.sourceModel()
|
||||||
|
if sourceParent.isValid():
|
||||||
|
parent_item = sourceParent.internalPointer()
|
||||||
|
msgnum = parent_item.row
|
||||||
|
else:
|
||||||
|
msgnum = sourceRow
|
||||||
|
|
||||||
accepted_level = model.entries[sourceRow][0] >= self.min_level
|
accepted_level = model.entries[msgnum][0] >= self.min_level
|
||||||
|
|
||||||
if self.freetext:
|
if self.freetext:
|
||||||
data_source = model.entries[sourceRow][1]
|
data_source = model.entries[msgnum][1]
|
||||||
data_message = model.entries[sourceRow][3]
|
data_message = model.entries[msgnum][3]
|
||||||
accepted_freetext = (self.freetext in data_source
|
accepted_freetext = (self.freetext in data_source
|
||||||
or self.freetext in data_message)
|
or any(self.freetext in m for m in data_message))
|
||||||
else:
|
else:
|
||||||
accepted_freetext = True
|
accepted_freetext = True
|
||||||
|
|
||||||
|
@ -176,26 +229,30 @@ class _LogDock(QDockWidgetCloseDetect):
|
||||||
grid.addWidget(newdock, 0, 4)
|
grid.addWidget(newdock, 0, 4)
|
||||||
grid.layout.setColumnStretch(2, 1)
|
grid.layout.setColumnStretch(2, 1)
|
||||||
|
|
||||||
self.log = QtWidgets.QTableView()
|
self.log = QtWidgets.QTreeView()
|
||||||
self.log.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection)
|
self.log.setSelectionMode(QtWidgets.QAbstractItemView.NoSelection)
|
||||||
self.log.horizontalHeader().setSectionResizeMode(
|
|
||||||
QtWidgets.QHeaderView.ResizeToContents)
|
|
||||||
self.log.horizontalHeader().setStretchLastSection(True)
|
|
||||||
self.log.verticalHeader().setSectionResizeMode(
|
|
||||||
QtWidgets.QHeaderView.ResizeToContents)
|
|
||||||
self.log.verticalHeader().hide()
|
|
||||||
self.log.setHorizontalScrollMode(
|
self.log.setHorizontalScrollMode(
|
||||||
QtWidgets.QAbstractItemView.ScrollPerPixel)
|
QtWidgets.QAbstractItemView.ScrollPerPixel)
|
||||||
self.log.setVerticalScrollMode(
|
self.log.setVerticalScrollMode(
|
||||||
QtWidgets.QAbstractItemView.ScrollPerPixel)
|
QtWidgets.QAbstractItemView.ScrollPerPixel)
|
||||||
self.log.setShowGrid(False)
|
self.log.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
|
||||||
self.log.setTextElideMode(QtCore.Qt.ElideNone)
|
|
||||||
grid.addWidget(self.log, 1, 0, colspan=5)
|
grid.addWidget(self.log, 1, 0, colspan=5)
|
||||||
self.scroll_at_bottom = False
|
self.scroll_at_bottom = False
|
||||||
self.scroll_value = 0
|
self.scroll_value = 0
|
||||||
|
|
||||||
|
# If Qt worked correctly, this would be nice to have. Alas, resizeSections
|
||||||
|
# is broken when the horizontal scrollbar is enabled.
|
||||||
|
# self.log.setContextMenuPolicy(QtCore.Qt.ActionsContextMenu)
|
||||||
|
# sizeheader_action = QtWidgets.QAction("Resize header", self.log)
|
||||||
|
# sizeheader_action.triggered.connect(
|
||||||
|
# lambda: self.log.header().resizeSections(QtWidgets.QHeaderView.ResizeToContents))
|
||||||
|
# self.log.addAction(sizeheader_action)
|
||||||
|
|
||||||
log_sub.add_setmodel_callback(self.set_model)
|
log_sub.add_setmodel_callback(self.set_model)
|
||||||
|
|
||||||
|
cw = QtGui.QFontMetrics(self.font()).averageCharWidth()
|
||||||
|
self.log.header().resizeSection(0, 26*cw)
|
||||||
|
|
||||||
def filter_level_changed(self):
|
def filter_level_changed(self):
|
||||||
if not hasattr(self, "table_model_filter"):
|
if not hasattr(self, "table_model_filter"):
|
||||||
return
|
return
|
||||||
|
@ -219,21 +276,13 @@ class _LogDock(QDockWidgetCloseDetect):
|
||||||
if self.scroll_at_bottom:
|
if self.scroll_at_bottom:
|
||||||
self.log.scrollToBottom()
|
self.log.scrollToBottom()
|
||||||
|
|
||||||
# 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:
|
# HACK:
|
||||||
# Qt intermittently likes to scroll back to the top when rows are removed.
|
# Qt intermittently likes to scroll back to the top when rows are removed.
|
||||||
# Work around this by restoring the scrollbar to the previously memorized
|
# Work around this by restoring the scrollbar to the previously memorized
|
||||||
# position, after the removal.
|
# position, after the removal.
|
||||||
# Note that this works because _LogModel always does the insertion right
|
# Note that this works because _LogModel always does the insertion right
|
||||||
# before the removal.
|
# before the removal.
|
||||||
|
# TODO: check if this is still required after moving to QTreeView
|
||||||
def rows_removed(self):
|
def rows_removed(self):
|
||||||
if self.scroll_at_bottom:
|
if self.scroll_at_bottom:
|
||||||
self.log.scrollToBottom()
|
self.log.scrollToBottom()
|
||||||
|
@ -257,7 +306,8 @@ class _LogDock(QDockWidgetCloseDetect):
|
||||||
def save_state(self):
|
def save_state(self):
|
||||||
return {
|
return {
|
||||||
"min_level_idx": self.filter_level.currentIndex(),
|
"min_level_idx": self.filter_level.currentIndex(),
|
||||||
"freetext_filter": self.filter_freetext.text()
|
"freetext_filter": self.filter_freetext.text(),
|
||||||
|
"header": bytes(self.log.header().saveState())
|
||||||
}
|
}
|
||||||
|
|
||||||
def restore_state(self, state):
|
def restore_state(self, state):
|
||||||
|
@ -279,6 +329,13 @@ class _LogDock(QDockWidgetCloseDetect):
|
||||||
# manually here, unlike for the combobox.
|
# manually here, unlike for the combobox.
|
||||||
self.filter_freetext_changed()
|
self.filter_freetext_changed()
|
||||||
|
|
||||||
|
try:
|
||||||
|
header = state["header"]
|
||||||
|
except KeyError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
self.log.header().restoreState(QtCore.QByteArray(header))
|
||||||
|
|
||||||
|
|
||||||
class LogDockManager:
|
class LogDockManager:
|
||||||
def __init__(self, main_window, log_sub):
|
def __init__(self, main_window, log_sub):
|
||||||
|
|
|
@ -2,7 +2,7 @@ import asyncio
|
||||||
import time
|
import time
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
from PyQt5 import QtCore, QtWidgets
|
from PyQt5 import QtCore, QtWidgets, QtGui
|
||||||
|
|
||||||
from artiq.gui.models import DictSyncModel
|
from artiq.gui.models import DictSyncModel
|
||||||
from artiq.tools import elide
|
from artiq.tools import elide
|
||||||
|
@ -67,8 +67,6 @@ class ScheduleDock(QtWidgets.QDockWidget):
|
||||||
self.table = QtWidgets.QTableView()
|
self.table = QtWidgets.QTableView()
|
||||||
self.table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
|
self.table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
|
||||||
self.table.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection)
|
self.table.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection)
|
||||||
self.table.horizontalHeader().setSectionResizeMode(
|
|
||||||
QtWidgets.QHeaderView.ResizeToContents)
|
|
||||||
self.table.verticalHeader().setSectionResizeMode(
|
self.table.verticalHeader().setSectionResizeMode(
|
||||||
QtWidgets.QHeaderView.ResizeToContents)
|
QtWidgets.QHeaderView.ResizeToContents)
|
||||||
self.table.verticalHeader().hide()
|
self.table.verticalHeader().hide()
|
||||||
|
@ -89,17 +87,20 @@ class ScheduleDock(QtWidgets.QDockWidget):
|
||||||
self.table_model = Model(dict())
|
self.table_model = Model(dict())
|
||||||
schedule_sub.add_setmodel_callback(self.set_model)
|
schedule_sub.add_setmodel_callback(self.set_model)
|
||||||
|
|
||||||
def rows_inserted_after(self):
|
cw = QtGui.QFontMetrics(self.font()).averageCharWidth()
|
||||||
# HACK:
|
h = self.table.horizontalHeader()
|
||||||
# workaround the usual Qt layout bug when the first row is inserted
|
h.resizeSection(0, 7*cw)
|
||||||
# (columns are undersized if an experiment with a due date is scheduled
|
h.resizeSection(1, 12*cw)
|
||||||
# and the schedule was empty)
|
h.resizeSection(2, 16*cw)
|
||||||
self.table.horizontalHeader().reset()
|
h.resizeSection(3, 6*cw)
|
||||||
|
h.resizeSection(4, 16*cw)
|
||||||
|
h.resizeSection(5, 30*cw)
|
||||||
|
h.resizeSection(6, 20*cw)
|
||||||
|
h.resizeSection(7, 20*cw)
|
||||||
|
|
||||||
def set_model(self, model):
|
def set_model(self, model):
|
||||||
self.table_model = model
|
self.table_model = model
|
||||||
self.table.setModel(self.table_model)
|
self.table.setModel(self.table_model)
|
||||||
self.table_model.rowsInserted.connect(self.rows_inserted_after)
|
|
||||||
|
|
||||||
async def delete(self, rid, graceful):
|
async def delete(self, rid, graceful):
|
||||||
if graceful:
|
if graceful:
|
||||||
|
@ -118,3 +119,9 @@ class ScheduleDock(QtWidgets.QDockWidget):
|
||||||
msg = "Deleted RID {}".format(rid)
|
msg = "Deleted RID {}".format(rid)
|
||||||
self.status_bar.showMessage(msg)
|
self.status_bar.showMessage(msg)
|
||||||
asyncio.ensure_future(self.delete(rid, graceful))
|
asyncio.ensure_future(self.delete(rid, graceful))
|
||||||
|
|
||||||
|
def save_state(self):
|
||||||
|
return bytes(self.table.horizontalHeader().saveState())
|
||||||
|
|
||||||
|
def restore_state(self, state):
|
||||||
|
self.table.horizontalHeader().restoreState(QtCore.QByteArray(state))
|
||||||
|
|
Loading…
Reference in New Issue