Source code for vdat.gui.fplane
# Virus Data Analysis Tool: a data reduction GUI for HETDEX/VIRUS data
# Copyright (C) 2015, 2016, 2017 "The HETDEX collaboration"
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
"""Panel with the focal plane"""
from __future__ import (absolute_import, division, print_function,
unicode_literals)
from collections import defaultdict
import pprint
import traceback as tb
from qtpy.QtCore import Signal, Slot
from qtpy import QtWidgets
import pkg_resources as pkgr
from . import overlay as voverlay
TABS_ENTRY_POINT = 'vdat.tab_types'
[docs]class FplaneCache(object):
'''Cache of fplane objects.
This object allows to store and retrieve objects in dictionary of lists.
Each entry has the type of the object (as provided by the user) as key and
a list of object as value.
'''
def __init__(self):
self._cache = defaultdict(list)
[docs] def into_cache(self, fp_type, fp):
'''Store the object (a widget) into the cache.
Parameters
----------
fp_type : string
name of the type to store
fp : object
object to be stored.
'''
self._cache[fp_type].append(fp)
[docs] def from_cache(self, fp_type):
'''Returns one object stored under the name ``fp_type``.
Parameters
----------
fp_type : string
name of the type to store
Returns
-------
An object or ``None`` if the cache is empty.
'''
try:
return self._cache[fp_type].pop()
except IndexError:
return None
class OverlayTab(QtWidgets.QWidget):
'''Empty tab with overlay that is shown when no tab is added to
:class:`FplaneWidget`
Parameters
----------
parent : :class:`PyQt5.QtWidgets.QWidget` or derivate
parent object of the tree view model
'''
def __init__(self, msg, parent=None):
super(OverlayTab, self).__init__(parent=parent)
self._overlay = voverlay.Overlay(msg, parent=self)
self._overlay.show()
[docs]class FplaneWidget(QtWidgets.QTabWidget):
'''Widget containing the tabs showing the focal plane.
.. list-table:: **Custom signals**
:header-rows: 1
* - Name
- Signature
- Description
* - :attr:`sig_ifuToggled`
- str, bool
- emitted when an IFU is selected in a widget contained in this one;
the id of the IFU and whether is selected (``True``) or deselected
(``False``) are passed as arguments.
* - :attr:`sig_ifuSelected`
- str, bool
- emitted when an IFU is selected in a widget that contains this one;
the id of the IFU and whether is selected (``True``) or deselected
(``False``) are passed as arguments.
* - :attr:`sig_selectAllIFUs`
-
- emitted when all the IFUs are selected in a widget that contains
this one
* - :attr:`sig_deselectAllIFUs`
-
- emitted when all the IFUs are deselected in a widget that contains
this one
.. list-table:: **Custom slot**
:header-rows: 1
* - Name
- Signature
- Description
* - :func:`change_fplane`
- str, dict
- Upon selecting a new directory or task, update the tabs for the
directory passed as first argument according to the instruction in
the dictionary passed as second argument.
* - :func:`ifuToggled`
- str, bool
- Emit the :attr:`sig_ifuToggled` signal
* - :func:`ifuSelected`
- str, bool
- Emit the :attr:`sig_ifuSelected` signal
* - selectAllIFUs
-
- Emit the :attr:`sig_selectAllIFUs` signal
* - deselectAllIFUs
-
- Emit the :attr:`sig_deselectAllIFUs` signal
.. list-table:: **Connections between custom signals and/or slots**. If the
signal or slot belongs to ``tab``, it is connected in
:func:`change_fplane` and disconnected in :func:`empty_fplane`
:header-rows: 1
* - Signal
- Slot
* - tab.sig_ifuToggled
- :func:`ifuToggled`
* - :attr:`sig_selectAllIFUs`
- tab.selectAllIFUs
* - :attr:`sig_ifuSelected`
- tab.ifuSelected
* - :attr:`sig_deselectAllIFUs`
- tab.deselectAllIFUs
Parameters
----------
parent : :class:`PyQt5.QtWidgets.QWidget` or derivate
parent object of the tree view model
'''
sig_ifuToggled = Signal(str, bool)
sig_ifuSelected = Signal(str, bool)
sig_selectAllIFUs = Signal()
sig_deselectAllIFUs = Signal()
def __init__(self, parent=None):
super(FplaneWidget, self).__init__(parent=parent)
self._fplane_cache = FplaneCache()
self._q_error_message = QtWidgets.QErrorMessage(parent=self)
self.setUsesScrollButtons(True)
self.setStyleSheet("QTabBar::tab { min-width: 100px; }")
[docs] def empty_fplane(self):
'''Loop through the current tabs and for each one of them:
* remove from the :class:`FplaneWidget`
* disconnect all the signals connected in :meth:`change_fplane`
* if the ``use_cache`` property is ``True``, save the widget in the
cache
* if the ``use_cache`` property is ``False``, mark the widget for
deletion
'''
for i in range(self.count()-1, -1, -1):
fp = self.widget(i) # retrieve the tab
self.removeTab(i) # remove the tab from the stack
# disconnect the signals
if isinstance(fp, OverlayTab):
self.tabBar().show()
fp.deleteLater()
continue
fp.sig_ifuToggled.disconnect(self.ifuToggled)
self.sig_selectAllIFUs.disconnect(fp.selectAllIFUs)
self.sig_ifuSelected.disconnect(fp.ifuSelected)
self.sig_deselectAllIFUs.disconnect(fp.deselectAllIFUs)
fp.cleanup()
if fp.use_cache:
self._fplane_cache.into_cache(fp.tab_type, fp)
else:
fp.deleteLater()
[docs] @Slot(str, dict)
def change_fplane(self, target, task_dict):
"""Clear the old tabs and create a set of new ones.
It loops through the list in the ``tabs`` section of ``task_dict``,
load and call the plugins implementing each ``tab_type``. If the plugin
exists and the execution succeed, for each widget returned:
* connect the :attr:`sig_selectAllIFUs`, :attr:`sig_deselectAllIFUs`
and :attr:`sig_ifuSelected` signals and the :meth:`ifuToggled` slot
* add it as a tab
All errors when loading and calling the plugins are collected and shown
before return the method. If no tab is found or no tab could be plugged
in because of errors, show an overlay to notify the user.
Parameters
----------
target : string
path of the selected directory
task_dict : dict
dictionary with the configuration for the tabs and buttons to
display
"""
# First delete all tabs (if there are any around) and hide the overlay
# Work from the back to the front
self.empty_fplane()
# get the tabs configuration
tabs = task_dict.get('tabs')
step_name = task_dict['step_name']
if not tabs: # show an overlay and return
self.addTab(OverlayTab('No tab definition found', parent=self), '')
self.tabBar().hide()
return
errors = []
for t in tabs: # Loop over all tab types
try:
tab_type = t['tab_type']
except KeyError as e:
msg = ('The tab item is missing the mandatory "tab_type"'
' keyword.\nTab definition:\n')
msg += pprint.pformat(t)
errors.append(msg)
continue
# load the plugin from the entry point
i = -1 # if it stays -1 no plugin found
for i, ep in enumerate(pkgr.iter_entry_points(TABS_ENTRY_POINT,
name=tab_type)):
try:
func = ep.load()
except ImportError as e:
msg = ('The plugin "{ep}" could not be loaded because'
' of\n{e}'.format(ep=ep, e=e))
errors.append(msg)
continue
try:
tabs_list = func(target, t, step_name, self._fplane_cache,
self)
except Exception as e:
msg = ('The execution of the function linked to the plugin'
' "{ep}" failed because of\n{e}\n.'
' Traceback:\n{tb}'.format(ep=ep, e=e,
tb=tb.format_exc()))
errors.append(msg)
continue
if not tabs_list:
msg = 'No tabs returns by plugin "{ep}"'
errors.append(msg.format(ep=ep))
continue
for tab in tabs_list:
# add the tab to the widget, setting the title and the
# tooltip, if required
try:
tab.sig_ifuToggled.connect(self.ifuToggled)
self.sig_selectAllIFUs.connect(tab.selectAllIFUs)
self.sig_ifuSelected.connect(tab.ifuSelected)
self.sig_deselectAllIFUs.connect(tab.deselectAllIFUs)
idx = self.addTab(tab, tab.title)
self.setTabToolTip(idx, getattr(tab, 'tool_tip', ''))
self.setTabEnabled(idx, getattr(tab, 'enabled', True))
except Exception as e:
msg = ('It was not possible to plug (one of) the'
' tab(s) "{t}" returned by the plugin "{ep}".'
' Error:\n{e}\nTraceback:\n{tb}')
errors.append(msg.format(t=tab, ep=ep, e=e,
tb=tb.format_exc()))
if i == -1:
msg = 'No plugin found to deal with tab type "{tt}"'
errors.append(msg.format(tt=tab_type))
if errors:
self.show_tab_errors(errors)
if self.count() == 0: # add the overlay also if all tabs fail
self.addTab(OverlayTab('No tab added due to errors ({} errors'
' happened)'.format(len(errors)),
parent=self), '')
self.tabBar().hide()
else: # select the first enabled tab
for idx in range(self.count()):
if self.isTabEnabled(idx):
self.setCurrentIndex(idx)
break
[docs] def show_tab_errors(self, errors):
'''Shows the errors into a QErrorMessage window.
Parameters
----------
errors : list of strings
errors to report
'''
for e in errors:
self._q_error_message.showMessage(e)
[docs] @Slot(str, bool)
def ifuToggled(self, id_, val):
'''Emit the :attr:`sig_ifuToggled` signal. The method is also a PyQt
slot.
Parameters
----------
id_ : string
SLOTID of the ifu that is toggled
val : bool
``True`` if selected, ``False`` if deselected
'''
self.sig_ifuToggled.emit(id_, val)
[docs] @Slot(str, bool)
def ifuSelected(self, id_, val):
'''Emit the :attr:`sig_ifuSelected` signal. The method is also a PyQt
slot.
Parameters
----------
id_ : string
SLOTID of the ifu that is toggled
val : bool
``True`` if selected, ``False`` if deselected
'''
self.sig_ifuSelected.emit(id_, val)
[docs] @Slot()
def selectAllIFUs(self):
"""Emit the :attr:`sig_selectAllIFUs` signal. The method is also a PyQt
slot.
"""
self.sig_selectAllIFUs.emit()
[docs] @Slot()
def deselectAllIFUs(self):
"""Emit the :attr:`sig_selectAllIFUs` signal. The method is also a PyQt
slot.
"""
self.sig_deselectAllIFUs.emit()