# Virus Data Analysis Tool: a data reduction GUI for HETDEX/VIRUS data
# Copyright (C) 2016, 2017, 2018 "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/>.
from __future__ import (absolute_import, division, print_function,
unicode_literals)
import itertools as it
from pyhetdex.het.fplane import FPlane
from qtpy import QtCore, QtGui, QtWidgets
from qtpy.QtCore import Signal, Slot
import vdat.config as vconf
from . import ifu_widget
from .interface import FplaneTabTemplate
from .. import utils as gui_utils
[docs]class UpdateIFUTask(QtCore.QThread):
''':class:`PyQt5.QtCore.QThread` that runs ``ifu.prepare_image`` on every
of the ifus passed to the :meth:`init` method
.. list-table:: **Custom signals**
:header-rows: 1
* - Name
- Signature
- Description
* - :attr:`finished`
- bool
- Emitted when the :meth:`run` is stopped (``True``) or finishes
normally (``False``)
* - :attr:`ifu_ready`
- str
- after preparing the image(s) to show, emits the signal with the
ifu.ifuslot string. The main thread can then retrieve the ifu and
display it.
.. list-table:: **Custom slot**
:header-rows: 1
* - Name
- Signature
- Description
* - :meth:`stop`
-
- stop and exit from :meth:`run` before finishing the list of IFUs.
If the method is processing one IFU, finishes before exiting.
.. note::
It is not possible to update a pixmap from withing a running
``QThread``. Therefore we emit the :attr:`ifu_ready` signal when the
image is ready for display. It is responsibility of the process owning
the thread to pain the image into the pixmap and display it.
Parameters
----------
parent : :class:`QWidget` or derived instance, optional
the parent of the current widget
Attributes
----------
ifus : list
IFUs to process. Each object must implement the
:meth:`~vdat.gui.tabs.ifu_widget.BaseIFUWidget.prepare_image`
'''
finished = QtCore.Signal(bool)
ifu_ready = QtCore.Signal(str)
def __init__(self, parent=None):
super(UpdateIFUTask, self).__init__(parent=parent)
self.ifus = []
self._stop = False
[docs] def run(self): # pragma: no cover; run in a thread; tested anyway
'''Run the thread. Triggered by calling the :meth:`start`.
Loop through all the :attr:`ifus` and call the ``prepare_image``
method. After it returns, the ``ifuslot`` string attribute of the
processed ifu is emitted via the :attr:`ifu_ready` signal.
'''
stopped = False
for ifu in self.ifus:
if self._stop:
stopped = True
break
ifu.prepare_image()
self.ifu_ready.emit(ifu.ifuslot)
self.finished.emit(stopped)
self._stop = False
[docs] @QtCore.Slot()
def stop(self):
'''Make the :meth:`run` method stop when starting a new loop. This
method is also a PyQt slot.'''
self._stop = True
[docs]class BaseFplanePanel(FplaneTabTemplate):
'''Implementation of the :class:`~.interface.FplaneTabTemplate` that
creates a working fplane panel using the
:class:`.ifu_widget.BaseIFUWidget`. Connect all the relevant slots to be
able to select/deselect the IFUs.
.. list-table:: **Custom signals**
:header-rows: 1
* - Name
- Signature
- Description
* - :attr:`sig_thread_stop`
-
- Emitted to stop the :attr:`update_thread`
.. list-table:: **Custom slot**
:header-rows: 1
* - Name
- Signature
- Description
* - update_finished
- bool
- react to the :attr:`UpdateIFUTask.finished` signal. The current
implementation does nothing.
* - :meth:`ifuSelected`, :meth:`selectAllIFUs`, :meth:`deselectAllIFUs`
-
- see :class:`.interface.FplaneTabTemplate`
* - :meth:`update_ifus`
-
- If the :attr:`update_thread` runs, stop it; then pass to it the
IFUs list and start the thread.
.. list-table:: **Connections between custom signals and/or slots**.
:header-rows: 1
* - Signal
- Slot
* - :attr:`UpdateIFUTask.finished`
- :meth:`update_finished`
* - :attr:`UpdateIFUTask.ifu_ready`
- :meth:`show_ifu_image`
* - :attr:`sig_thread_stop`
- :meth:`UpdateIFUTask.stop`
* - :attr:`.ifu_widget.BaseIFUWidget.sig_ifuToggled`
- :attr:`sig_ifuToggled`
Parameters
----------
all :
see :class:`~.interface.FplaneTabTemplate`
Attributes
----------
update_thread : :class:`UpdateIFUTask`
QThread used to update the images to show in the IFUs
ifu_widget_class
fplane : :class:`~pyhetdex.het.fplane.FPlane`
instance of the focal plane constructed using the
:attr:`ifu_widget_class`
main_layout : :class:`PyQt5.QtWidgets.QVBoxLayout`
main layout of the widget.
fplane_layout : :class:`PyQt5.QtWidgets.QGridLayout`
layout containing the IFU widgets
initialized : bool
if ``False``, react to :meth:`showEvent` drawing the tab content. It is
set to ``False`` at build time and in :meth:`cleanup`
others
see :class:`~.interface.FplaneTabTemplate`
'''
sig_thread_stop = Signal()
def __init__(self, tab_type, parent=None):
super(BaseFplanePanel, self).__init__(tab_type, parent=parent)
self.use_cache = True
self.conf_main = vconf.get_config('main')
self.initialized = False
self.setup_qthread()
self.build_gui()
[docs] def setup_qthread(self):
'''Create and setup the :class:`UpdateIFUTask`. Saves it into the
:attr:`update_thread` attribute.
Overrides this method to use other QThread implementations. The rest of
the implementation assumes that :attr:`update_thread` has a
:meth:`start` method and a :attr:`ifus` attibute.
This method is called during the initialisation of the object
'''
self.update_thread = UpdateIFUTask(parent=self)
self.update_thread.finished.connect(self.update_finished)
self.update_thread.ifu_ready.connect(self.show_ifu_image)
self.sig_thread_stop.connect(self.update_thread.stop)
[docs] def build_gui(self):
'''Build the GUI calling :meth:`set_main_layout` and adding to the main
layout the fplane layout from :meth:`build_fplane`'''
self.set_main_layout()
self.fplane_layout = self.build_fplane()
self.main_layout.addLayout(self.fplane_layout)
[docs] def set_main_layout(self):
'''Create and set the default layout. The layout is saved into the
:attr:`main_layout`.
Default to :class:`PyQt5.QtWidgets.QVBoxLayout`. Override to use a
different layout.
'''
self.main_layout = QtWidgets.QVBoxLayout()
self.setLayout(self.main_layout)
[docs] def build_fplane(self):
'''Create a grid layout, fill it with the IFU widgets returned by
:attr:`ifu_widget_class`, connect the relevant signals and return the
layout.
The name of the fplane file is taken from the ``fp_filename`` option of
the ``fplane`` sectoin in the main vdat configuration file.
Returns
-------
fplane_layout : :class:`PyQt5.QtWidgets.QGridLayout` instance
layout containing the IFU widgets
'''
fplane_layout = QtWidgets.QGridLayout()
fplane_layout.setSizeConstraint(QtWidgets.QLayout.SetMinAndMaxSize)
fplane_layout.setObjectName("focal_plane_layout")
exclude_ifuslot = self.conf_main.get_list('fplane',
'exclude_ifuslot',
use_default=True)
self.fplane = FPlane(self.conf_main["fplane"]["fp_filename"],
ifu_class=self.ifu_widget_class,
exclude_ifuslot=exclude_ifuslot)
# loop through the IFUs, add them to the layout and connect the signals
xs, ys = set(), set()
for ifu in self.fplane.ifus:
xs.add(ifu.xid)
ys.add(ifu.yid)
fplane_layout.addWidget(ifu, ifu.yid, ifu.xid, 1, 1)
ifu.sig_ifuToggled.connect(self.sig_ifuToggled)
if self.conf_main['fplane'].getboolean('respect_empty',
fallback=True):
for i in range(min(xs), max(xs) + 1):
fplane_layout.setColumnStretch(i, 1)
for i in range(min(ys), max(ys) + 1):
fplane_layout.setRowStretch(i, 1)
return fplane_layout
@property
def ifu_widget_class(self):
'''Return the name of the class to use to represent IFUs in the focal
plane.
Override in derived classes to use a different IFU widget.
Returns
-------
:class:`.ifu_widget.BaseIFUWidget`
'''
return ifu_widget.BaseIFUWidget
[docs] @Slot(bool)
def update_finished(self, stopped):
'''Do nothing method. This method is also a pyqt Slot.
Override this method in derived classes to act upon the
:attr:`UpdateIFUTask.finished` signal
Parameters
----------
stopped : bool
``True`` if the updated has been forcefully stopped, ``False`` if
it finished.
'''
pass
[docs] @Slot(str)
def show_ifu_image(self, ifuslot):
'''Call the :meth:`show_image` of the ifu with ``ifuslot`` ID.
This method is also an pyqt slot.
Parameters
----------
ifuslot : str
ifuslot ID of the IFU to show
'''
self.fplane.by_ifuslot(ifuslot).show_image()
[docs] @Slot(str, bool)
def ifuSelected(self, ifuslot, val):
"""Select or deselect an IFU. This method is also a PyQt slot.
Parameters
----------
ifuslot : string
SLOTID of the selected IFU
val : bool
If ``True`` selected, otherwise deselected
"""
self.fplane.by_ifuslot(ifuslot).selected = val
[docs] @Slot()
def selectAllIFUs(self):
"""Select all IFU. This method is also a PyQt slot.
It emits the :attr:`sig_ifuToggled` signal to make sure that all the
ifus are registered as selected.
"""
selected = True
for ifuslot in self.fplane.ifuslots:
self.ifuSelected(ifuslot, selected)
self.sig_ifuToggled.emit(ifuslot, selected)
[docs] @Slot()
def deselectAllIFUs(self):
"""Deselect all IFU. This method is also a PyQt slot.
It emits the :attr:`sig_ifuToggled` signal to make sure that all the
ifus are registered as deselected.
"""
selected = False
for ifuslot in self.fplane.ifuslots:
self.ifuSelected(ifuslot, selected)
self.sig_ifuToggled.emit(ifuslot, selected)
[docs] def showEvent(self, e):
'''When the tab becomes visible, this method is triggered by Qt.
If :attr:`initialized` is False, calls :meth:`update_ifus` and then set
it to True.
Parameters
----------
e : :class:`PyQt5.QtGui.QShowEvent`
the tab is show
'''
if not self.initialized:
self.update_ifus()
self.initialized = True
[docs] @Slot()
def update_ifus(self):
"""If the :attr:`update_thread` is running, stop it. Pass to it the
IFUs list and start the thread.
This method is also a PyQt slot.
"""
if self.update_thread.isRunning():
self.stop_update_thread()
self.update_thread.ifus = list(self.fplane.ifus)
self.update_thread.start()
[docs] def stop_update_thread(self):
'''Stop the thread and wait for it to return'''
self.sig_thread_stop.emit()
self.update_thread.wait()
[docs] def cleanup(self):
'''Call the super method, stop the running thread, set
:attr:`initialized` to False and run the :meth:`cleanup` on all the
IFUs'''
super(BaseFplanePanel, self).cleanup()
if self.update_thread.isRunning():
self.sig_thread_stop.emit()
self.initialized = False
for ifu in self.fplane.ifus:
ifu.cleanup()
[docs]class BaseFplanePanelSetup(BaseFplanePanel):
'''Add a default setup implementation that set the title and the tooltip
and then call the setup method for the IFUs'''
[docs] def setup(self, target_dir, tab_dict, format_dict):
'''Informations to pass to the tab and to the IFUs.
The method uses directly the following keys of the ``tab_dict``
argument:
* title (mandatory): title of the current widget; the title is
formatted using ``format_dict`` and saved in :attr:`title`.
* tool_tip (optional): tool tip to show for the current widget; if
present, the string is formatted using ``format_dict`` and saved in
:attr:`tool_tip`.
Other keys might be used by the ifu widget setup method.
Parameters
----------
target_dir : string
directory selected by the user
tab_dict : dictionary
dictionary with the specifications to use to build the tabs
format_dict : string
dictionary with the fields and values that can be replaced in file
names
'''
# create title and tooltip
self.title = tab_dict['title'].format(**format_dict)
if 'tool_tip' in tab_dict:
self.tool_tip = tab_dict['tool_tip'].format(**format_dict)
# pass the information to the IFUs
for ifu in self.fplane.ifus:
ifu.setup(target_dir, tab_dict, format_dict)
[docs]class FitsFplanePanel(BaseFplanePanelSetup):
"""Class that shows fits files in the focal plane.
It allows to select the scaling for the IFUs.
Unless documented below, the class inherits signals, slots and connection
from :class:`BaseFplanePanel`.
.. list-table:: **Custom slot**
:header-rows: 1
* - Name
- Signature
- Description
* - :meth:`toggle_individual_scale`
- bool
- When the input is true, redraw the IFU with individual scaling
enabled
* - :meth:`toggle_global_scale`
- bool
- When the input is true, redraw the IFU with using a global scaling
enabled
* - :meth:`toggle_custom_scale`
- bool, optional
- When the input is true, redraw the IFU using a custom scaling
enabled
.. list-table:: **Connections between custom signals and/or slots**.
:header-rows: 1
* - Signal
- Slot
* - :attr:`toggled` of "Individual" radio button
- :meth:`toggle_individual_scale`
* - :attr:`toggled` of "Global" radio button
- :meth:`toggle_global_scale`
* - :attr:`toggled` of "Custom" radio button
- :meth:`toggle_custom_scale` (with bool argument)
* - :attr:`released` of custom "Rescale" button
- :meth:`toggle_custom_scale` (with no argument)
Parameters
----------
all :
see :class:`BaseFplanePanel`
Attributes
----------
scaling_bar : :class:`PyQt5.QtWidgets.QHBoxLayout`
layout containing the scaling bar
min_scale_custom
max_scale_custom
min_scale_global, max_scale_global : int
minimum and maximum global scale used when clicking on the global scale
button
radio_individual_scale
radio_global_scale
radio_custom_scale
"""
def __init__(self, tab_type, parent=None):
super(FitsFplanePanel, self).__init__(tab_type, parent=parent)
self.min_scale_global = self.max_scale_global = None
@property
def ifu_widget_class(self):
'''Return the name of the class to use to represent IFUs in the focal
plane.
Returns
-------
:class:`.ifu_widget.IFUFitsWidget`
'''
return ifu_widget.IFUFitsWidget
[docs] def build_gui(self):
'''Add extra functionalities to the basic gui'''
super(FitsFplanePanel, self).build_gui()
self.add_scaling_bar()
[docs] def add_scaling_bar(self):
'''Create a :class:`PyQt5.QtWidgets.QHBoxLayout`, add it to the
:attr:`main_layout`. On this layout add buttons and text boxes to chose
the scaling to use for the IFUs. Connect all the relevant signals and
slots.'''
scaling_bar = QtWidgets.QHBoxLayout()
self.scaling_bar = scaling_bar
scaling_bar.setAlignment(QtCore.Qt.AlignLeft)
self.main_layout.addLayout(scaling_bar)
# add the radio buttons
self._radio_individual_scale = self._radio_button('Individual',
scaling_bar)
scaling_bar.addWidget(gui_utils.line_separator(parent=self))
self._radio_global_scale = self._radio_button('Global', scaling_bar)
scaling_bar.addWidget(gui_utils.line_separator(parent=self))
self._radio_custom_scale = self._radio_button('Custom', scaling_bar)
self._radio_individual_scale.setChecked(True)
# connect the radio buttons
(self._radio_individual_scale.released
.connect(self.toggle_individual_scale))
self._radio_global_scale.toggled.connect(self.toggle_global_scale)
self._radio_custom_scale.toggled.connect(self.toggle_custom_scale)
# add the boxes, labels and button for the custom scales
self._custom_scale_setter(scaling_bar)
scaling_bar.addStretch()
[docs] def _scale_box(self):
'''Create and return a box where is possible to insert numbers.
Returns
-------
scale_box : :class:`PyQt5.QtWidgets.QLineEdit`
box with width of 80 and a double validator
'''
scale_box = QtWidgets.QLineEdit(parent=self)
scale_box.setFixedWidth(80)
scale_box.setValidator(QtGui.QDoubleValidator())
return scale_box
[docs] def _custom_scale_setter(self, layout):
'''Setup all the parts needed to set the custom scale.
Parameters
----------
layout : :class:`PyQt5.QtWidgets.QLayout`
layout to which the pieces are added
'''
# minimum label
self._min_label = QtWidgets.QLabel("Min: ")
layout.addWidget(self._min_label)
# minimum box
self._min_scale_box = self._scale_box()
layout.addWidget(self._min_scale_box)
# maximum lable
self._max_label = QtWidgets.QLabel("Max: ")
layout.addWidget(self._max_label)
# maximum box
self._max_scale_box = self._scale_box()
layout.addWidget(self._max_scale_box)
# add a button to do manual scaling
self._rescale_button = QtWidgets.QPushButton("Rescale")
self._rescale_button.released.connect(self.toggle_custom_scale)
layout.addWidget(self._rescale_button)
# disable all of them by default
self.custom_scale_set_enabled(False)
[docs] def custom_scale_set_enabled(self, enable):
'''Enable/disable the labels, input boxes and button used to set the
custom scaling.
Parameters
----------
enable : bool
whether to enable or disable the widgets.
'''
self._min_label.setEnabled(enable)
self._min_scale_box.setEnabled(enable)
self._max_label.setEnabled(enable)
self._max_scale_box.setEnabled(enable)
self._rescale_button.setEnabled(enable)
@QtCore.Property(float, doc='Float value shown in the minimum scale'
' box. Clear the text box with'
' ``del min_scale_custom``')
def min_scale_custom(self):
text = self._min_scale_box.text()
if text:
return float(text)
else:
return None
@min_scale_custom.setter
def min_scale_custom(self, value):
self._min_scale_box.setText('{:.3f}'.format(value))
@min_scale_custom.deleter
def min_scale_custom(self):
self._min_scale_box.setText('')
@QtCore.Property(float, doc='Float value shown in the maximum scale'
' box. Clear the text box with'
' ``del max_scale_custom``')
def max_scale_custom(self):
text = self._max_scale_box.text()
if text:
return float(text)
else:
return None
@max_scale_custom.setter
def max_scale_custom(self, value):
self._max_scale_box.setText('{:.3f}'.format(value))
@max_scale_custom.deleter
def max_scale_custom(self):
self._max_scale_box.setText('')
@QtCore.Property(QtWidgets.QRadioButton,
doc='Individual scale radio button')
def radio_individual_scale(self):
return self._radio_individual_scale
@QtCore.Property(QtWidgets.QRadioButton,
doc='Global scale radio button')
def radio_global_scale(self):
return self._radio_global_scale
@QtCore.Property(QtWidgets.QRadioButton,
doc='Custom scale radio button')
def radio_custom_scale(self):
return self._radio_custom_scale
[docs] @Slot()
@Slot(bool)
def toggle_individual_scale(self, enabled=True):
'''When ``enabled`` is ``True`` redraw the IFUs using the individual
scaling.
This method is also a PyQt slot.
Parameters
----------
enabled : bool
whether the individual scaling is enabled or not
'''
if enabled:
self._zscale_to_ifus(None, None)
self.update_ifus()
[docs] @Slot(bool)
def toggle_global_scale(self, enabled):
'''When ``enabled`` is ``True`` redraw the IFUs using the global
scaling, saved in :attr:`min_scale_global` and
:attr:`max_scale_global`.
This method is also a PyQt slot.
Parameters
----------
enabled : bool
whether the global scaling is enabled or not
'''
if enabled:
self._zscale_to_ifus(self.min_scale_global, self.max_scale_global)
self.update_ifus()
[docs] @Slot(bool)
@Slot()
def toggle_custom_scale(self, enabled=True):
'''When ``enabled`` is ``True`` redraw the IFUs using the minimum and
maximum values provided by the user.
This method is also a PyQt slot.
Parameters
----------
enabled : bool
whether the custom scaling is enabled or not
'''
self.custom_scale_set_enabled(enabled)
if enabled:
self._zscale_to_ifus(self.min_scale_custom, self.max_scale_custom)
self.update_ifus()
[docs] def _zscale_to_ifus(self, zmin, zmax):
'''Loop over all the IFUs setting :attr:`ifu.zmin_global` and
:attr:`ifu.zmax_global` to the input values.
Parameters
----------
zmin, zmax : float or None
values to set
'''
for ifu in self.fplane.ifus:
ifu.zmin_global = zmin
ifu.zmax_global = zmax
[docs] def reset_individual_scale(self):
'''Reset the individual scale to checked'''
self.radio_individual_scale.setChecked(True)
[docs] @Slot(bool)
def update_finished(self, stopped):
'''If the update of the IFUs finished without being stopped, collect
all the zmin/zmax from the IFUs and save it for the global scaling. If
none of the IFUs has such values disable the global scaling button.
This method is also a pyqt Slot.
Override this method in derived classes to act upon the
:attr:`UpdateIFUTask.finished` signal
Parameters
----------
stopped : bool
``True`` if the updated has been forcefully stopped, ``False`` if
it finished.
'''
if not stopped:
self.get_global_scale()
[docs] def get_global_scale(self):
'''Try to get the zmin and zmax from the IFUs.
If none of the IFU have those values, disable the global button.
Otherwise save the values in the :attr:`zmin_global` and
:attr:`zmax_global` and add their values into a tool tip and to the
text boxes if they are empty
'''
self.radio_global_scale.setCheckable(True)
self.radio_custom_scale.setCheckable(True)
try:
zmin = min(i.zmin_ifu for i in self.fplane.ifus if i.zmin_ifu)
zmax = max(i.zmax_ifu for i in self.fplane.ifus if i.zmax_ifu)
except ValueError: # no IFU shows a thumbnail
self.min_scale_global = self.max_scale_global = None
self.radio_global_scale.setToolTip('')
self.radio_global_scale.setCheckable(False)
self.radio_custom_scale.setCheckable(False)
return
self.min_scale_global = zmin
self.max_scale_global = zmax
if self.min_scale_custom is None:
self.min_scale_custom = zmin
if self.max_scale_custom is None:
self.max_scale_custom = zmax
self.radio_global_scale.setToolTip('zmin: {:.3f}; zmax:'
' {:.3f}'.format(zmin, zmax))
[docs] def cleanup(self):
'''Call the parent class implementation and unset all the min/max
scales'''
super(FitsFplanePanel, self).cleanup()
del self.min_scale_custom
del self.max_scale_custom
self.min_scale_global = self.max_scale_global = None
self.radio_global_scale.setCheckable(False)
self.radio_custom_scale.setCheckable(False)
self.reset_individual_scale()
try: # remove the tool_tip if present
del self.tool_tip
except AttributeError:
pass
[docs]class QuickReconFplanePanel(FitsFplanePanel):
'''Do a quick reconstruction with the input files
Unless documented below, the class inherits signals, slots and connection,
as well as attributes from :class:`FitsFplanePanel`.
Parameters
----------
all :
see :class:`BaseFplanePanel`
Attributes
----------
enabled : bool
if the reconstruction object exists, is set to True, else to False
'''
def __init__(self, tab_type, parent=None):
super(QuickReconFplanePanel, self).__init__(tab_type, parent=parent)
self.enabled = True
@property
def ifu_widget_class(self):
'''Return the name of the class to use to represent IFUs in the focal
plane.
Returns
-------
:class:`.ifu_widget.IFUQuickReconWidget`
'''
return ifu_widget.IFUQuickReconWidget
[docs] def basenames(self, basenames):
'''Set the basenames in all the ifus'''
for ifu in self.fplane.ifus:
ifu.basenames = basenames
[docs] def setup(self, target_dir, tab_dict, format_dict):
'''Informations to pass to the tab and to the IFUs.
See :meth:`FitsFplanePanel.setup` for the keys of ``tab_dict`` used
here.
Parameters
----------
target_dir : string
directory selected by the user
tab_dict : dictionary
dictionary with the specifications to use to build the tabs
format_dict : string
dictionary with the fields and values that can be replaced in file
names
'''
super(QuickReconFplanePanel, self).setup(target_dir, tab_dict,
format_dict)
self.enabled = gui_utils.get_reconstructed() is not None
if not self.enabled:
self.title = "Deactivated - " + self.title
[docs] def cleanup(self):
'''On top of what :meth:`FitsFplanePanel.cleanup` does, reset
:attr:`enabled` to True'''
super(QuickReconFplanePanel, self).cleanup()
self.enabled = True
[docs]class FitsAndReconFplanePanel(FplaneTabTemplate):
'''This class provides a tab grouping a :class:`FitsFplanePanel` and a
:class:`QuickReconFplanePanel` and providing a button to switch between
them.
Unless documented below, the class inherits signals, slots and connection
from :class:`.interface.FplaneTabTemplate`.
.. list-table:: **Custom slot**
:header-rows: 1
* - Name
- Signature
- Description
* - :meth:`next_widget`
-
- go to the next widget
.. list-table:: **Connections between custom signals and/or slots**.
:header-rows: 1
* - Signal
- Slot
* - "Reconstructed" checkbox added in the lower right corner of the
widgets
- :meth:`next_widget`
Parameters
----------
all :
see :class:`~.interface.FplaneTabTemplate`
Attributes
----------
others
see :class:`~.interface.FplaneTabTemplate`
'''
def __init__(self, tab_type, parent=None):
super(FitsAndReconFplanePanel, self).__init__(tab_type, parent=parent)
# cache the panel
self.use_cache = True
# add a stacked layout
stacked_layout = QtWidgets.QStackedLayout(self)
self.setLayout(stacked_layout)
# if the reconstruction is not there, disable the button
self.recon_enabled = True
# list of the widgets, used to propagate the information from the
# outside to the stacked widgets
self.widgets = []
self.buttons = []
self.widget_indeces = []
# create and add the widgets
fits_fplane, button = self._make_widget(FitsFplanePanel, tab_type,
False)
self.widgets.append(fits_fplane)
self.buttons.append(button)
recon_fplane, button = self._make_widget(QuickReconFplanePanel,
tab_type, True)
self.widgets.append(recon_fplane)
self.buttons.append(button)
[docs] def setup(self, target_dir, tab_dict, format_dict):
'''Informations to pass to the stacked widgets and to the IFUs.
Make sure that the :class:`FitsFplanePanel` is always shown first. If
the reconstruction object does not exist, disable the checkbox to
switch to the reconstructed widget. The title and tool tip, if present,
are taken from the first widget.
Parameters
----------
target_dir : string
directory selected by the user
tab_dict : dictionary
dictionary with the specifications to use to build the tabs
format_dict : string
dictionary with the fields and values that can be replaced in file
names
'''
for w in self.widgets:
w.setup(target_dir, tab_dict, format_dict)
self.widget_indeces.append(self.layout().addWidget(w))
first_w = self.widgets[0]
self.title = first_w.title
if hasattr(first_w, 'tool_tip'):
self.tool_tip = first_w.tool_tip
# reset the cycle to make sure that the first widget is shown
self.index_cycle = it.cycle(self.widget_indeces)
self.next_widget()
# if no reconstruction is available disable the switching
self.buttons[0].setEnabled(gui_utils.get_reconstructed() is not None)
[docs] def cleanup(self):
'''Cleanup the widgets'''
for w in self.widgets:
self.layout().removeWidget(w)
w.cleanup()
self.widget_indeces = []
try: # remove the tool_tip if present
del self.tool_tip
except AttributeError:
pass
[docs] @Slot(str, bool)
def ifuSelected(self, ifuslot, val):
"""Select or deselect an IFU. This method is also a PyQt slot.
Call the corresponding method in the stacked widgets.
Parameters
----------
ifuslot : string
SLOTID of the selected IFU
val : bool
If ``True`` selected, otherwise deselected
"""
for w in self.widgets:
w.ifuSelected(ifuslot, val)
[docs] @Slot()
def selectAllIFUs(self):
"""Select all IFU. This method is also a PyQt slot.
Call the corresponding method in the stacked widgets.
"""
for w in self.widgets:
w.selectAllIFUs()
[docs] @Slot()
def deselectAllIFUs(self):
"""Deselect all IFU. This method is also a PyQt slot.
Call the corresponding method in the stacked widgets.
"""
for w in self.widgets:
w.deselectAllIFUs()
[docs]class CubeFplanePanel(FitsFplanePanel):
"""Class that shows data cube fits files in the focal plane.
Unless documented below, the class inherits signals, slots and connection
from :class:`FitsFplanePanel`.
Parameters
----------
all :
see :class:`FitsFplanePanel`
Attributes
----------
all :
see :class:`FitsFplanePanel`
"""
@property
def ifu_widget_class(self):
'''Return the name of the class to use to represent IFUs in the focal
plane.
Returns
-------
:class:`.ifu_widget.IFUCubeWidget`
'''
return ifu_widget.IFUCubeWidget
[docs]class MultiExtFplanePanel(FitsFplanePanel):
"""Class that shows one desired extension from fits files in the focal
plane.
Unless documented below, the class inherits signals, slots and connection
from :class:`FitsFplanePanel`.
Parameters
----------
all :
see :class:`FitsFplanePanel`
Attributes
----------
all :
see :class:`FitsFplanePanel`
"""
@property
def ifu_widget_class(self):
'''Return the name of the class to use to represent IFUs in the focal
plane.
Returns
-------
:class:`.ifu_widget.IFUMultiExtWidget`
'''
return ifu_widget.IFUMultiExtWidget
[docs]class StaticCheckBox(QtWidgets.QCheckBox):
'''A checkbox always checked or un-checked
.. list-table:: **Custom slot**
:header-rows: 1
* - Name
- Signature
- Description
* - :meth:`stay_checked`
-
- Keep the "checked" state of the button always the same
.. list-table:: **Connections between custom signals and/or slots**.
:header-rows: 1
* - Signal
- Slot
* - :attr:`clicked`
- :meth:`stay_checked`
Parameters
----------
text : string
text showing in the button
is_checked : bool
whether the checkbox must stay checked or not
parent : :class:`QWidget` or derived instance, optional
the parent of the current widget
'''
def __init__(self, text, is_checked, parent=None):
super(StaticCheckBox, self).__init__(text, parent=parent)
if is_checked:
self._is_checked = QtCore.Qt.Checked
else:
self._is_checked = QtCore.Qt.Unchecked
self.stateChanged.connect(self.stay_checked)
self.setCheckState(self._is_checked)
[docs] @Slot(int)
def stay_checked(self, state):
'''Force the checkbox to stay checked or unchecked.
This method is also a Qt Slot.
'''
self.setCheckState(self._is_checked)
[docs]class TextFilesPanel(BaseFplanePanelSetup):
'''Tab to show the content of a text file.
The IFU in the focal plane shows the number of lines in the file. A double
click on the widget brings up a window with the content of the file
Unless documented below, the class inherits signals, slots and connection
from :class:`BaseFplanePanel`.
Parameters
----------
all :
see :class:`~.interface.FplaneTabTemplate`
'''
@property
def ifu_widget_class(self):
'''Return the class to use to represent IFUs in the focal plane.
Returns
-------
:class:`.ifu_widget.TextFileWidget`
'''
return ifu_widget.TextFileWidget
[docs]class DistPanel(BaseFplanePanelSetup):
'''Show the number of lines in the distortion file in the IFUs. A double
click on the widget brings up a window with the content of the file. From
the window, it is possible to send a region file computed from the
distortion to the ``ds9``.
Unless documented below, the class inherits signals, slots and connection
from :class:`BaseFplanePanel`.
Parameters
----------
all :
see :class:`~.interface.FplaneTabTemplate`
'''
@property
def ifu_widget_class(self):
'''Return the class to use to represent IFUs in the focal plane.
Returns
-------
:class:`.ifu_widget.BaseIFUWidget`
'''
return ifu_widget.DistWidget