Skip to content

Commit

Permalink
Merge pull request #120 from SiLab-Bonn/gui
Browse files Browse the repository at this point in the history
Gui improvements
  • Loading branch information
laborleben committed May 9, 2018
2 parents aeef30f + 421cbc0 commit 2b89b2c
Show file tree
Hide file tree
Showing 12 changed files with 499 additions and 39 deletions.
31 changes: 22 additions & 9 deletions docs/GUI.rst
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ in the *tesbeam analysis* folder, the GUI can be opened from the shell via
File selection
**************

The file selection tab provides a table in order to display and handle the input files of all *devices under test* (:DUT:).
The file selection tab provides a table in order to display and handle the input files of all *devices under test* (*DUT*).
The input files can be selected via a button click or *dragged & dropped* onto the table area on the left-hand side of the file selection tab.
After file selection, the path to each input file, the DUT name for subsequent analysis as well as the file status (each input file is checked for required information)
are displayed in the table. The table entries can be moved or deleted by usinf the buttons in the *Navigation* column.
Expand All @@ -68,12 +68,13 @@ Example of the file selection for FE-I4 telescope data, consisting of 4 FE-I4 pi
Setup
*****

The setup tab provides a plotting area on the left-hand side of the tab in order to plot the schematic test setup as well as a tab to input setup information for each DUT on the right-hand side.
The setup tab provides a plotting area on the left-hand side of the tab in order to plot the schematic test beam setup as well as a tab to input setup information for each DUT on the right-hand side.
The telescope setup is plotted from the top- (upper) and side-view (lower) with rotations shown multiplied by a factor of 10 and correct relative distances in between DUTs.
Information for each DUT can be input manually or selected from a list with predefined DUT-types. This list can be extended by the current, complete information via entering a *new* name into
the respective field and pressing the respective button. DUT-types can be overwritten or removed from the list by typing `name` or `:name` respectively, into the respective field and pressing
the button. Dead material (*scatter planes*) in the setup can be added by clicking the button in the upper-right corner.
the button. Dead material (*scatter planes*) in the setup can be added by clicking the button in the upper-right corner.
To proceed the analyis (e.g. press the 'Ok' button), all required information regarding the setup of each DUT must be filled in.
A complete setup can be saved from and loaded into the setup tab by using the respective buttons on the bottom right-hand side.
An example of the setup tab is shown below.

.. image:: _static/gui/setup.png
Expand All @@ -89,14 +90,11 @@ The plotting area contains the result plots of the analysis step. Result plots c

The option input area contains three different types of options:

- :needed:
Options that must be set. The default value of the option is pre-set.
- :needed: Options that must be set. The default value of the option is pre-set.

- :optional:
Options that can be set but are not required. The default value is `None`. To effectively set the option, the corresponding `check box` has to be checked.
- :optional: Options that can be set but are not required. The default value is `None`. To effectively set the option, the corresponding `check box` has to be checked.

- :fixed:
Options that must not be changed. They are displayed as text at the bottom of the option area.
- :fixed: Options that must not be changed. They are displayed as text at the bottom of the option area.

All options are documented and their widgets created via `introspection <http://book.pythontips.com/en/latest/object_introspection.html#inspect-module>`_ of the corresponding function.
The documentation is shown as a *tooltip* when hovering over the respective option name. Furthermore, the current value of the option is shown as a *tooltip*
Expand Down Expand Up @@ -133,6 +131,21 @@ Re-running a tab requires to reset all subsequent analysis tabs. A complete, con

Logging console on Noisy Pixel tab

Exception handling
******************

Exceptions which are thrown by an analysis or plotting function are handled via a separate window. The window shows the type of exceptions and allows the user to switch between the exception message
and the full traceback which also can be saved. Upon an analysis exception, the user may decide whether to reset the current tabs input to default or keep the input configuration in order to only change a single/few
input options by clicking the respective buttons. An example of the exception window can be seen below.

.. image: _static/gui/exception_1.png
:width: 45%
.. image: _static/gui/exception_2.png
:width: 45%
Exception window with error message (left) and full traceback (right).

Saving/Loading sessions
***********************

Expand Down
Binary file added docs/_static/gui/exception_1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/_static/gui/exception_2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/_static/gui/setup.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
12 changes: 9 additions & 3 deletions testbeam_analysis/gui/gui_widgets/analysis_widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -335,9 +335,15 @@ def _select_widget(self, dtype, name, default_value, optional, tooltip, func):
if ('scalar' in dtype and ('tuple' in dtype or 'iterable' in dtype) or
'int' in dtype and ('tuple' in dtype or 'iterable' in dtype) or
('iterable' in dtype and 'iterable of iterable' not in dtype and 'duts' not in name)) and 'quality' not in name:
widget = option_widgets.OptionMultiSlider(name=name, labels=self.setup['dut_names'],
default_value=default_value, optional=optional,
dtype=dtype, tooltip=tooltip, parent=self)
if 'range' not in name:
widget = option_widgets.OptionMultiSlider(name=name, labels=self.setup['dut_names'],
default_value=default_value, optional=optional,
dtype=dtype, tooltip=tooltip, parent=self)
else:
widget = option_widgets.OptionMultiRangeBox(name=name, labels=self.setup['dut_names'],
default_value=default_value, optional=optional,
dtype=dtype, tooltip=tooltip, parent=self)

elif ('iterable of iterable' in dtype or 'iterable' in dtype) and ('duts' in name or 'quality' in name):

# Init labels
Expand Down
231 changes: 231 additions & 0 deletions testbeam_analysis/gui/gui_widgets/option_widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -260,6 +260,237 @@ def _emit_value(self):
self.valueChanged.emit(self.rb_t.isChecked())


class OptionRangeBox(QtWidgets.QWidget):
"""
Option range boxes for floats and ints. Shows the value
"""

valueChanged = QtCore.pyqtSignal(list) # Either int or float

def __init__(self, name, default_value, optional, tooltip, dtype, parent=None):
super(OptionRangeBox, self).__init__(parent)

# Store dtype
self._dtype = dtype
self.default_value = default_value
self.update_tooltip(default_value)

# Slider with textbox to the right
layout_2 = QtWidgets.QHBoxLayout()
label_min = QtWidgets.QLabel('min.')
self.min_box = QtWidgets.QSpinBox() if 'float' not in self._dtype else QtWidgets.QDoubleSpinBox()
label_max = QtWidgets.QLabel('max.')
self.max_box = QtWidgets.QSpinBox() if 'float' not in self._dtype else QtWidgets.QDoubleSpinBox()
self.min_box.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
self.max_box.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
layout_min = QtWidgets.QHBoxLayout()
layout_min.setAlignment(QtCore.Qt.AlignCenter)
layout_max = QtWidgets.QHBoxLayout()
layout_max.setAlignment(QtCore.Qt.AlignCenter)
layout_min.addWidget(label_min)
layout_min.addWidget(self.min_box)
layout_min.addStretch()
layout_max.addWidget(label_max)
layout_max.addWidget(self.max_box)
layout_max.addStretch()
layout_2.addLayout(layout_min)
layout_2.addLayout(layout_max)

# Option name with spinboxes below
layout = QtWidgets.QVBoxLayout(self)
text = QtWidgets.QLabel(name)
if tooltip:
text.setToolTip(tooltip)
if optional:
layout_1 = QtWidgets.QHBoxLayout()
layout_1.addWidget(text)
layout_1.addStretch(0)
check_box = QtWidgets.QCheckBox()
layout_1.addWidget(check_box)
layout.addLayout(layout_1)
check_box.stateChanged.connect(lambda v: self._set_readonly(v == 0))
self._set_readonly()
else:
layout.addWidget(text)
layout.addLayout(layout_2)

if default_value is not None:
self.min_box.setMinimum(0)
self.min_box.setMaximum(default_value[-1] - 1)
self.min_box.setValue(0)
self.max_box.setMinimum(self.min_box.minimum() + 1)
self.max_box.setMaximum(default_value[-1])
self.max_box.setValue(default_value[-1])

self.min_box.valueChanged.connect(lambda _: self._emit_value())
self.max_box.valueChanged.connect(lambda v: self.min_box.setMaximum(v - 1))
self.max_box.valueChanged.connect(lambda _: self._emit_value())

def update_tooltip(self, val):
self.setToolTip('Current value: {}, (default value: {})'.format(val, self.default_value))

def load_value(self, value):

if value is not None: # value can be None
self.min_box.setValue(value[0])
self.max_box.setValue(value[-1])
self.update_tooltip(value)

def _set_readonly(self, value=True):

palette = QtGui.QPalette()
if value:
palette.setColor(QtGui.QPalette.Base, QtCore.Qt.gray)
palette.setColor(QtGui.QPalette.Text, QtCore.Qt.darkGray)
else:
palette.setColor(QtGui.QPalette.Base, QtCore.Qt.white)
palette.setColor(QtGui.QPalette.Text, QtCore.Qt.black)

self.min_box.setReadOnly(value)
self.max_box.setReadOnly(value)
self.min_box.setPalette(palette)
self.max_box.setPalette(palette)
self._emit_value()

def _emit_value(self):
if self.min_box.isReadOnly() and self.max_box.isReadOnly():
value = [None] #if self.default_value is None else self.default_value
self.update_tooltip(value)
self.valueChanged.emit(value)
else:
# Separate options that need int dtypes e.g. range(int) from floats
value = [self.min_box.value(), self.max_box.value()]
self.update_tooltip(value)
self.valueChanged.emit(value)


class OptionMultiRangeBox(QtWidgets.QWidget):
"""
Option range boxes for floats and ints for several ranges.
"""

valueChanged = QtCore.pyqtSignal(list)

def __init__(self, name, labels, default_value, optional, tooltip, dtype, parent=None):
super(OptionMultiRangeBox, self).__init__(parent)

# Store dtype
self._dtype = dtype
self.default_value = default_value
self.labels = labels
self.update_tooltip(default_value)

# Check default value
if default_value is None: # None is only supported for all values
default_value = 1
if not isinstance(default_value, collections.Iterable):
default_value = [[0, default_value]] * len(labels)
if len(labels) != len(default_value):
raise ValueError('Number of default values does not match number of parameters')

# Option name with range boxes
layout = QtWidgets.QVBoxLayout(self)
text = QtWidgets.QLabel(name)
if tooltip:
text.setToolTip(tooltip)
if optional: # Values can be unset
layout_1 = QtWidgets.QHBoxLayout()
layout_1.addWidget(text)
layout_1.addStretch(0)
check_box = QtWidgets.QCheckBox()
layout_1.addWidget(check_box)
layout.addLayout(layout_1)
else:
layout.addWidget(text)

# Dict for range boxes
self.range_boxes = {}

for i, label in enumerate(labels): # Create one range box per label
# Two boxes for min/max plus label on the left
layout_2 = QtWidgets.QHBoxLayout()
layout_2.addWidget(QtWidgets.QLabel(' ' + label))
layout_2.addStretch()
label_min = QtWidgets.QLabel('min.')
min_box = QtWidgets.QSpinBox() if 'float' not in self._dtype else QtWidgets.QDoubleSpinBox()
label_max = QtWidgets.QLabel('max.')
max_box = QtWidgets.QSpinBox() if 'float' not in self._dtype else QtWidgets.QDoubleSpinBox()
min_box.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
max_box.setSizePolicy(QtWidgets.QSizePolicy.Expanding, QtWidgets.QSizePolicy.Fixed)
layout_min = QtWidgets.QHBoxLayout()
layout_min.setAlignment(QtCore.Qt.AlignCenter)
layout_max = QtWidgets.QHBoxLayout()
layout_max.setAlignment(QtCore.Qt.AlignCenter)
layout_min.addWidget(label_min)
layout_min.addWidget(min_box)
layout_min.addStretch()
layout_max.addWidget(label_max)
layout_max.addWidget(max_box)
layout_max.addStretch()
layout_2.addLayout(layout_min)
layout_2.addLayout(layout_max)

if default_value[i] is not None:
min_box.setMinimum(0)
min_box.setMaximum(default_value[i][-1] - 1)
min_box.setValue(0)
max_box.setMinimum(min_box.minimum() + 1)
max_box.setMaximum(default_value[i][-1])
max_box.setValue(default_value[i][-1])

min_box.valueChanged.connect(lambda _: self._emit_value())
max_box.valueChanged.connect(lambda v, mb=min_box: mb.setMaximum(v - 1))
max_box.valueChanged.connect(lambda _: self._emit_value())

self.range_boxes[label] = [min_box, max_box]

layout.addLayout(layout_2)

if optional:
check_box.stateChanged.connect(lambda v: self._set_readonly(v == 0))
self._set_readonly()

def update_tooltip(self, val):
self.setToolTip('Current value: {}, (default value: {})'.format(val, self.default_value))

def load_value(self, value):

if value is not None and isinstance(value, collections.Iterable):
for i, label in enumerate(self.labels):
min_box, max_box = self.range_boxes[label]
min_box.setValue(value[i][0])
max_box.setValue(value[i][-1])

self.update_tooltip(value)

def _set_readonly(self, value=True):

palette = QtGui.QPalette()
if value:
palette.setColor(QtGui.QPalette.Base, QtCore.Qt.gray)
palette.setColor(QtGui.QPalette.Text, QtCore.Qt.darkGray)
else:
palette.setColor(QtGui.QPalette.Base, QtCore.Qt.white)
palette.setColor(QtGui.QPalette.Text, QtCore.Qt.black)

for key in self.range_boxes.keys():
min_box, max_box = self.range_boxes[key]
min_box.setReadOnly(value)
max_box.setReadOnly(value)
min_box.setPalette(palette)
max_box.setPalette(palette)

self._emit_value()

def _emit_value(self):
if not any([self.range_boxes[key][0].isReadOnly() for key in self.range_boxes.keys()]):
values = [[self.range_boxes[key][0].value(), self.range_boxes[key][-1].value()] for key in self.labels]
else:
values = [None] # if self.default_value is None else self.default_value
self.update_tooltip(values)
self.valueChanged.emit(values)


class OptionMultiSlider(QtWidgets.QWidget):
"""
Option sliders for several ints or floats. Shows the value as text and can increase range
Expand Down
16 changes: 12 additions & 4 deletions testbeam_analysis/gui/gui_widgets/sub_windows.py
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,7 @@ def _update_settings(self):

class ExceptionWindow(QtWidgets.QMainWindow):

resetTab = QtCore.pyqtSignal()
exceptionRead = QtCore.pyqtSignal()

def __init__(self, exception, trace_back, tab=None, cause=None, parent=None):
Expand All @@ -194,7 +195,8 @@ def __init__(self, exception, trace_back, tab=None, cause=None, parent=None):

# Make main message and label
msg = "The following exception occurred during %s: %s.\n" \
"Try changing the input parameters. %s tab will be reset!" % (cause, self.exc_type, tab)
"Try changing the input parameters. To reset %s tab press 'Reset tab'," \
" to keep the current selection press 'Ok' !" % (cause, self.exc_type, tab)

self.label = QtWidgets.QLabel(msg)
self.label.setWordWrap(True)
Expand Down Expand Up @@ -248,16 +250,22 @@ def _init_UI(self):
btn_safe.setToolTip('Safe traceback to file')
btn_safe.clicked.connect(self.safe_traceback)

# Reset button
btn_reset = QtWidgets.QPushButton('Reset tab')
btn_reset.setToolTip('Reset current analysis tab')
btn_reset.clicked.connect(self.resetTab.emit)
btn_reset.clicked.connect(self.close)

# Ok button
btn_ok = QtWidgets.QPushButton('Ok')
btn_ok.setToolTip('Reset current tab')
btn_ok.setToolTip('Restore current analysis tab (No reset).')
btn_ok.clicked.connect(self.close)

# Add buttons to layout
layout_buttons.addWidget(self.btn_switch)
layout_buttons.addStretch(1)
layout_buttons.addWidget(btn_safe)
layout_buttons.addSpacing(h_space)
layout_buttons.addStretch(1)
layout_buttons.addWidget(btn_reset)
layout_buttons.addWidget(btn_ok)

# Dock in which text browser is placed
Expand Down
Loading

0 comments on commit 2b89b2c

Please sign in to comment.