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()