Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: m-labs/artiq
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 358d2a6ba38c
Choose a base ref
...
head repository: m-labs/artiq
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: 4e0e8341ca19
Choose a head ref
  • 5 commits
  • 6 files changed
  • 1 contributor

Commits on Mar 25, 2016

  1. Copy the full SHA
    5d5a443 View commit details
  2. Copy the full SHA
    7bdec1b View commit details
  3. Copy the full SHA
    74b3c47 View commit details
  4. Copy the full SHA
    69a531e View commit details
  5. Copy the full SHA
    4e0e834 View commit details
Showing with 154 additions and 85 deletions.
  1. +3 −1 artiq/frontend/artiq_gui.py
  2. +6 −2 artiq/gui/datasets.py
  3. +121 −69 artiq/gui/log.py
  4. +17 −10 artiq/gui/schedule.py
  5. +1 −1 artiq/master/worker_impl.py
  6. +6 −2 artiq/protocols/pc_rpc.py
4 changes: 3 additions & 1 deletion artiq/frontend/artiq_gui.py
Original file line number Diff line number Diff line change
@@ -130,6 +130,7 @@ def main():

d_datasets = datasets.DatasetsDock(sub_clients["datasets"],
rpc_clients["dataset_db"])
smgr.register(d_datasets)

d_applets = applets.AppletsDock(main_window, sub_clients["datasets"])
atexit_register_coroutine(d_applets.stop)
@@ -142,6 +143,7 @@ def main():

d_schedule = schedule.ScheduleDock(
status_bar, rpc_clients["schedule"], sub_clients["schedule"])
smgr.register(d_schedule)

logmgr = log.LogDockManager(main_window, sub_clients["log"])
smgr.register(logmgr)
@@ -172,7 +174,7 @@ def main():
# create first log dock if not already in state
d_log0 = logmgr.first_log_dock()
if d_log0 is not None:
main_window.tabifyDockWidget(d_shortcuts, d_log0)
main_window.tabifyDockWidget(d_schedule, d_log0)

# run
main_window.show()
8 changes: 6 additions & 2 deletions artiq/gui/datasets.py
Original file line number Diff line number Diff line change
@@ -47,8 +47,6 @@ def __init__(self, datasets_sub, dataset_ctl):
self.table = QtWidgets.QTreeView()
self.table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
self.table.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection)
self.table.header().setSectionResizeMode(
QtWidgets.QHeaderView.ResizeToContents)
grid.addWidget(self.table, 1, 0)

self.table.setContextMenuPolicy(QtCore.Qt.ActionsContextMenu)
@@ -79,3 +77,9 @@ def delete_clicked(self):
key = self.table_model.index_to_key(idx)
if key is not None:
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))
190 changes: 121 additions & 69 deletions artiq/gui/log.py
Original file line number Diff line number Diff line change
@@ -10,20 +10,24 @@
QDockWidgetCloseDetect)


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]
class ModelItem:
def __init__(self, parent, row):
self.parent = parent
self.row = row
self.children_by_row = []


class Model(QtCore.QAbstractTableModel):
class Model(QtCore.QAbstractItemModel):
def __init__(self, init):
QtCore.QAbstractTableModel.__init__(self)

self.headers = ["Source", "Message"]
self.children_by_row = []

self.entries = list(map(_make_wrappable, init))
self.entries = []
self.pending_entries = []
for entry in init:
self.append(entry)
self.depth = 1000
timer = QtCore.QTimer(self)
timer.timeout.connect(self.timer_tick)
@@ -44,7 +48,11 @@ def headerData(self, col, orientation, role):
return None

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):
return len(self.headers)
@@ -53,61 +61,96 @@ def __delitem__(self, k):
pass

def append(self, v):
self.pending_entries.append(_make_wrappable(v))

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()
severity, source, timestamp, message = v
self.pending_entries.append((severity, source, timestamp,
message.splitlines()))

def timer_tick(self):
if not self.pending_entries:
return
nrows = len(self.entries)
records = self.pending_entries
self.pending_entries = []

self.beginInsertRows(QtCore.QModelIndex(), nrows, nrows+len(records)-1)
self.entries.extend(records)
self.insertRows(nrows, len(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.endInsertRows()

if len(self.entries) > self.depth:
start = len(self.entries) - self.depth
self.beginRemoveRows(QtCore.QModelIndex(), 0, start-1)
self.entries = self.entries[start:]
self.removeRows(0, start)
self.children_by_row = self.children_by_row[start:]
for child in self.children_by_row:
child.row -= start
self.endRemoveRows()

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 data(self, index, role):
def parent(self, index):
if index.isValid():
if (role == QtCore.Qt.FontRole
and index.column() == 1):
return self.fixed_font
elif role == QtCore.Qt.TextAlignmentRole:
return QtCore.Qt.AlignLeft | QtCore.Qt.AlignTop
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()
parent = index.internalPointer().parent
if parent is self:
return QtCore.QModelIndex()
else:
return self.createIndex(parent.row, 0, parent)
else:
return QtCore.QModelIndex()

def data(self, index, role):
if not index.isValid():
return

item = index.internalPointer()
if item.parent is self:
msgnum = item.row
else:
msgnum = item.parent.row

if role == QtCore.Qt.FontRole and index.column() == 1:
return self.fixed_font
elif role == QtCore.Qt.BackgroundRole:
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:
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])))
return v[3][0]
else:
if column == 0:
return ""
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):
@@ -118,14 +161,19 @@ def __init__(self, min_level, freetext):

def filterAcceptsRow(self, sourceRow, sourceParent):
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:
data_source = model.entries[sourceRow][1]
data_message = model.entries[sourceRow][3]
data_source = model.entries[msgnum][1]
data_message = model.entries[msgnum][3]
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:
accepted_freetext = True

@@ -176,26 +224,30 @@ def __init__(self, manager, name, log_sub):
grid.addWidget(newdock, 0, 4)
grid.layout.setColumnStretch(2, 1)

self.log = QtWidgets.QTableView()
self.log = QtWidgets.QTreeView()
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(
QtWidgets.QAbstractItemView.ScrollPerPixel)
self.log.setVerticalScrollMode(
QtWidgets.QAbstractItemView.ScrollPerPixel)
self.log.setShowGrid(False)
self.log.setTextElideMode(QtCore.Qt.ElideNone)
self.log.setHorizontalScrollBarPolicy(QtCore.Qt.ScrollBarAsNeeded)
grid.addWidget(self.log, 1, 0, colspan=5)
self.scroll_at_bottom = False
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)

cw = QtGui.QFontMetrics(self.font()).averageCharWidth()
self.log.header().resizeSection(0, 26*cw)

def filter_level_changed(self):
if not hasattr(self, "table_model_filter"):
return
@@ -219,21 +271,13 @@ def rows_inserted_after(self):
if self.scroll_at_bottom:
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:
# 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.
# TODO: check if this is still required after moving to QTreeView
def rows_removed(self):
if self.scroll_at_bottom:
self.log.scrollToBottom()
@@ -257,7 +301,8 @@ def set_model(self, model):
def save_state(self):
return {
"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):
@@ -279,6 +324,13 @@ def restore_state(self, state):
# manually here, unlike for the combobox.
self.filter_freetext_changed()

try:
header = state["header"]
except KeyError:
pass
else:
self.log.header().restoreState(QtCore.QByteArray(header))


class LogDockManager:
def __init__(self, main_window, log_sub):
27 changes: 17 additions & 10 deletions artiq/gui/schedule.py
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@
import time
from functools import partial

from PyQt5 import QtCore, QtWidgets
from PyQt5 import QtCore, QtWidgets, QtGui

from artiq.gui.models import DictSyncModel
from artiq.tools import elide
@@ -67,8 +67,6 @@ def __init__(self, status_bar, schedule_ctl, schedule_sub):
self.table = QtWidgets.QTableView()
self.table.setSelectionBehavior(QtWidgets.QAbstractItemView.SelectRows)
self.table.setSelectionMode(QtWidgets.QAbstractItemView.SingleSelection)
self.table.horizontalHeader().setSectionResizeMode(
QtWidgets.QHeaderView.ResizeToContents)
self.table.verticalHeader().setSectionResizeMode(
QtWidgets.QHeaderView.ResizeToContents)
self.table.verticalHeader().hide()
@@ -89,17 +87,20 @@ def __init__(self, status_bar, schedule_ctl, schedule_sub):
self.table_model = Model(dict())
schedule_sub.add_setmodel_callback(self.set_model)

def rows_inserted_after(self):
# HACK:
# workaround the usual Qt layout bug when the first row is inserted
# (columns are undersized if an experiment with a due date is scheduled
# and the schedule was empty)
self.table.horizontalHeader().reset()
cw = QtGui.QFontMetrics(self.font()).averageCharWidth()
h = self.table.horizontalHeader()
h.resizeSection(0, 7*cw)
h.resizeSection(1, 12*cw)
h.resizeSection(2, 16*cw)
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):
self.table_model = model
self.table.setModel(self.table_model)
self.table_model.rowsInserted.connect(self.rows_inserted_after)

async def delete(self, rid, graceful):
if graceful:
@@ -118,3 +119,9 @@ def delete_clicked(self, graceful):
msg = "Deleted RID {}".format(rid)
self.status_bar.showMessage(msg)
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))
2 changes: 1 addition & 1 deletion artiq/master/worker_impl.py
Original file line number Diff line number Diff line change
@@ -255,7 +255,7 @@ def main():
short_exc_info = type(exc).__name__
exc_str = str(exc)
if exc_str:
short_exc_info += ": " + exc_str
short_exc_info += ": " + exc_str.splitlines()[0]
lines = ["Terminating with exception ("+short_exc_info+")\n"]
if hasattr(exc, "artiq_core_exception"):
lines.append(str(exc.artiq_core_exception))
Loading