Source code for vdat.gui.tabs.tab_widget

# 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 _radio_button(self, label, layout): '''Create a radio button, add it to ``layout`` and return it. The buttons are not marked by default. Parameters ---------- label : string label of the button layout : :class:`PyQt5.QtWidgets.QLayout` layout to which the button is added Returns ------- button : :class:`PyQt5.QtWidgets.QRadioButton` ''' button = QtWidgets.QRadioButton(label, parent=self) button.setChecked(False) layout.addWidget(button) return button
[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 _make_widget(self, Widget, tab_type, is_checked): '''Create a ``Widget`` instance, connect the :attr:`sig_ifuToggled`, add a check box with "Reconstructed" as text and connect it to a :meth:`next_widget`. Parameters ---------- Widget : class derived from :class:`BaseFplanePanel` the widget to create tab_type : string name of the tab is_checked : bool whether the checkbox needs to be always checked or not Returns ------- w : instance of Widget button : :class:`PyQt5.QtWidgets.QPushButton` button added to the Widget ''' w = Widget(tab_type, parent=self) # connect the widget signal with the current one to allow propagating # it outside w.sig_ifuToggled.connect(self.sig_ifuToggled) # create the button and connect it button = StaticCheckBox('Reconstructed', is_checked, parent=self) button.clicked.connect(self.next_widget) # add two spacers, one stretch stronger than the one in the # scaling_bar and the button w.scaling_bar.addWidget(gui_utils.line_separator(parent=self)) w.scaling_bar.addStretch(1) # this takes over the other one w.scaling_bar.addWidget(gui_utils.line_separator(parent=self)) w.scaling_bar.addWidget(button) return w, 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() def next_widget(self): '''Switch to the next widget. This method is also a Qt slot. ''' self.layout().setCurrentIndex(next(self.index_cycle))
[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