"""
The view portion of the PypeIt Setup GUI. Responsible for displaying information to the user, and forwarding user input to the controller.
.. include common links, assuming primary doc root is up one directory
.. include:: ../include/links.rst
"""
from pathlib import Path
from qtpy.QtWidgets import QGroupBox, QHBoxLayout, QVBoxLayout, QComboBox, QToolButton, QFileDialog, QWidget, QGridLayout, QFormLayout
from qtpy.QtWidgets import QMainWindow, QMenu, QTabWidget, QTreeView, QLayout, QLabel, QScrollArea, QListView, QTableView, QPushButton, QStyleOptionButton, QProgressDialog, QDialog, QHeaderView, QSizePolicy, QCheckBox, QDialog
from qtpy.QtWidgets import QAction, QAbstractItemView, QStyledItemDelegate, QButtonGroup, QStyle, QTabBar,QAbstractItemDelegate, QSplitter
from qtpy.QtGui import QDesktopServices, QMouseEvent, QKeySequence, QPalette, QColor, QValidator, QFont, QFontDatabase, QFontMetrics, QTextCharFormat, QTextCursor
from qtpy.QtCore import Qt, QUrl, QObject, QEvent, QSize, Signal,QSettings, QStringListModel, QAbstractItemModel, QModelIndex, QMargins, QSortFilterProxyModel, QRect
from pypeit.spectrographs.util import available_spectrographs
from pypeit.setup_gui.model import ModelState, PypeItMetadataModel
from pypeit.setup_gui.text_viewer import LogWindow, TextViewerWindow
from pypeit.setup_gui.dialog_helpers import DialogResponses, FileDialog, PersistentStringListModel
from pypeit import log
[docs]
def debugSizeStuff(widget:QWidget, name="widget"):
"""Helper method for logging sizxing information about a wdiget and its layout."""
log.info(f"{name} (width/height): {widget.width()}/{widget.height()} geometry x/y/w/h: {widget.geometry().x()}/{widget.geometry().y()}/{widget.geometry().width()}/{widget.geometry().height()} min w/h {widget.minimumWidth()}/{widget.minimumHeight()} hint w/h {widget.sizeHint().width()}/{widget.sizeHint().height()} min hint w/h {widget.minimumSizeHint().width()}/{widget.minimumSizeHint().height()} cm tlbr: {widget.contentsMargins().top()}/{widget.contentsMargins().left()}/{widget.contentsMargins().bottom()}/{widget.contentsMargins().right()} frame w/h {widget.frameSize().width()}/{widget.frameSize().height()}")
layout = widget.layout()
if layout is None:
log.info(f"{name} layout is None")
else:
log.info(f"{name} layout size constraint {layout.sizeConstraint()} spacing: {layout.spacing()} cm: tlbr {layout.contentsMargins().top()}/{layout.contentsMargins().left()}/{layout.contentsMargins().bottom()}/{layout.contentsMargins().right()} totalMinSize (w/h): {layout.totalMinimumSize().width()}/{layout.totalMinimumSize().width()} totalMaxSize (w/h): {layout.totalMaximumSize().width()}/{layout.totalMaximumSize().width()} totalHint (w/h): {layout.totalSizeHint().width()}/{layout.totalSizeHint().width()}")
fm = widget.fontMetrics()
if fm is None:
log.info(f"{name} fm is None")
else:
log.info(f"{name} fm lineSpacing: {fm.lineSpacing()} maxWidth: {fm.maxWidth()}, avg width: {fm.averageCharWidth()}")
[docs]
class PathEditor(QWidget):
"""
A custon widget that allows for entering a path. It has an editable combo box for entering
file locations, a browse button to use and a file dialog to enter the file location.
The history of past locations is kept in the combo box list and file dialog history.
Args:
parent(QWidget): The parent of this widget.
browse_caption (str): The caption text to use when searching for locations, also used as place holder
text when no location has been set.
"""
pathEntered = Signal(str)
"""Signal sent when a path has been added."""
def __init__(self, browse_caption, parent=None):
super().__init__(parent=parent)
self.browse_caption = browse_caption
# Setup the combo box
layout = QHBoxLayout()
layout.setSpacing(0)
layout.setContentsMargins(0, 0, 0, 0)
self._path = QComboBox(parent=self)
self._path.setEditable(True)
self._path.setInsertPolicy(QComboBox.InsertAtTop)
# Setup history
self._history = QStringListModel()
self._path.setModel(self._history)
if browse_caption is not None:
self._path.lineEdit().setPlaceholderText(browse_caption)
self._path.setCurrentIndex(-1)
self._path.activated.connect(self._new_path)
layout.addWidget(self._path)
# Setup the browse button
self._browse_button = QToolButton(parent=self)
self._browse_button.setText(self.tr("Browse..."))
self._browse_button.clicked.connect(self.browse)
layout.addWidget(self._browse_button)
self.setLayout(layout)
[docs]
def _add_path(self, new_path):
"""Set the path, adding it to history and signaling listeners.
Args:
new_path (str): The new path to add.
"""
# Add to history if needed
if new_path not in self._history.stringList():
# It's not in the history, insert it into the
# combo box (which uses the history as its model)
self._path.insertItem(0, new_path)
self._path.setCurrentIndex(-1)
self.pathEntered.emit(new_path)
[docs]
def setHistory(self, history):
"""Sets the past history of the PathEditor widget.
Args:
history (QStringListModel): A string list model containing the history of the widget."""
self._history = history
self._path.setModel(history)
self._path.setCurrentIndex(-1)
[docs]
def history(self):
"""Returns the past history of the PathEditor widget.
Returns:
QStringListModel: A stringh list model with the history of the widget.
"""
return self._history
[docs]
def browse(self):
"""Opens up a :class:`FileDialog` requesting the user select a location.
Returns:
str: The new location, or None if Cancel was pressed.
"""
browse_dialog = FileDialog(self,
caption = self.browse_caption,
file_mode=QFileDialog.Directory,
history=self._history)
if browse_dialog.show() == DialogResponses.ACCEPT:
# the User selected a directory
# Add it to our location list
self._add_path(browse_dialog.selected_path)
return browse_dialog.selected_path
else:
return None
[docs]
def _new_path(self, new_index):
"""
Signal handler for when the combo box selects a new path.
Args:
new_index: The index within the combo box that was selected.
"""
# Forward the signal to clients with the actual string value of the new
# location
if new_index != -1:
new_path = self._path.currentText()
self._add_path(new_path)
[docs]
class PypeItEnumListEditor(QWidget):
"""Widget for editing a enumerated list of values by checking the values
on or off with a checkbox.
Args:
parent (QWidget):
The parent of the editor.
allowed_values (list of str):
The list of allowed values in the enumeration.
index (QModelIndex):
The index of the item being edited.
num_lines (int):
The number of lines to display. Any other lines
will be reachable by scrolling.
"""
closed = Signal(QWidget, bool)
"""
Signal sent when the user closes the editor with the OK or CANCEL button.
The signal will provide the editor that was closed and a boolean that will
be True if the change was accepted or False if it was canceled.
"""
def __init__(self, parent, allowed_values, index, num_lines):
super().__init__(parent)
self.index=index
self._values = set()
self._allowed_values = allowed_values
self._checkboxes = dict()
self.setBackgroundRole(QPalette.ColorRole.Window)
self.setAutoFillBackground(True)
layout = QVBoxLayout(self)
layout.setSpacing(0)
layout.setContentsMargins(0,0,0,0)
# Wrap the list in a scroll area
scroll_area = QScrollArea(parent=self)
layout.addWidget(scroll_area)
checkbox_container = QWidget()
# Make sure we have a pointer mouse cursor, rather than the cursor
# inherited from the table view
self.setCursor(Qt.ArrowCursor)
# Create the checkboxes for each allowable option
self._button_group = QButtonGroup()
self._button_group.setExclusive(False)
checkbox_container.setAutoFillBackground(True)
checkbox_layout=QVBoxLayout(checkbox_container)
checkbox_layout.setContentsMargins(0,0,0,0)
max_checkbox_width = 0
for value in self._allowed_values:
checkbox = QCheckBox(text=value, parent=checkbox_container)
self._checkboxes[value] = checkbox
self._button_group.addButton(checkbox)
checkbox_layout.addWidget(checkbox)
# Figure out the maximum size for a checkbox
if checkbox.width() > max_checkbox_width:
max_checkbox_width = checkbox.width()
log.info(f"Max checkbox width: {max_checkbox_width}")
scroll_area.setWidget(checkbox_container)
# Figure out the minimum width
min_width = max_checkbox_width
# Account for scrollbar size and margins
if scroll_area.verticalScrollBar():
min_width += scroll_area.verticalScrollBar().sizeHint().width()
min_width += scroll_area.contentsMargins().left() + scroll_area.contentsMargins().right()
# Add Ok and cancel buttons at the bottom, outside the scroll area
ok_cancel_container = QWidget(parent=checkbox_container)
ok_cancel_layout = QHBoxLayout()
accept_button=QPushButton(text="OK")
accept_button.setDefault(True)
accept_button.clicked.connect(self._accepted)
cancel_button=QPushButton(text="Cancel")
cancel_button.clicked.connect(self._canceled)
ok_cancel_layout.addWidget(accept_button)
ok_cancel_layout.addWidget(cancel_button)
ok_cancel_container.setLayout(ok_cancel_layout)
ok_cancel_layout_margins = ok_cancel_layout.contentsMargins()
# Use small margins along the left/right
ok_cancel_layout_margins.setRight(1)
ok_cancel_layout_margins.setLeft(1)
ok_cancel_layout.setContentsMargins(ok_cancel_layout_margins)
# Make sure the minimum width doesn't truncate the button's text
ok_button_min_size = calculateButtonMinSize(accept_button)
cancel_button_min_size = calculateButtonMinSize(cancel_button)
button_min_width = max(ok_button_min_size.width(), cancel_button_min_size.width())
ok_cancel_container_min_width = button_min_width*2 + ok_cancel_layout.spacing() + ok_cancel_layout_margins.left() + ok_cancel_layout_margins.right()
log.info(f"Okay cancel container min_width: {ok_cancel_container_min_width}")
if min_width < ok_cancel_container_min_width:
min_width = ok_cancel_container_min_width
# Set the okay/cancel button container's min width to keep Qt
# from making the buttons bigger than they need to be.
ok_cancel_container.setMinimumWidth(min_width)
layout.addWidget(ok_cancel_container)
# Set the minimum height for this widget given requested # of lines
# This assumes the checkboxes are the same height
min_height = checkbox_layout.spacing()*(num_lines-1) + checkbox.height()*num_lines
# Account for margins
min_height += scroll_area.contentsMargins().top() + scroll_area.contentsMargins().bottom()
# Account for buttons
min_height += ok_button_min_size.height() + ok_cancel_layout.contentsMargins().top() + ok_cancel_layout.contentsMargins().bottom()
self.setMinimumSize(min_width, min_height)
self._button_group.buttonToggled.connect(self._choiceChecked)
log.info(f"min_width/height: {min_width}/{min_height}")
debugSizeStuff(self, "Enum Editor")
debugSizeStuff(checkbox_container, "Checkbox Container")
debugSizeStuff(ok_cancel_container, "OK/Cancel Container")
debugSizeStuff(accept_button, "OK Button")
debugSizeStuff(cancel_button, "Cancel Button")
[docs]
def _accepted(self, *args):
"""Signal handler for when the "OK" button is clicked."""
self.closed.emit(self, True)
[docs]
def _canceled(self, *args):
"""Signal handler for when the "Cancel" button is clicked."""
self.closed.emit(self, False)
[docs]
def _choiceChecked(self, widget, checked):
"""Signal handler for when one of the enumerated values is checked on or off.
Args:
widget (QCheckBox):
The widget that checked or unchecked.
checked (bool):
True if the widget is checked, false if it is not.
"""
value = widget.text()
if checked:
self._values.add(value)
else:
self._values.discard(value)
[docs]
def setSelectedValues(self, values):
"""Set what values of the enumeration are selected.
Args:
values (list of str): The enum values that should be selected.
"""
if values is None:
self._values=set()
else:
self._values = set(values.split(","))
for value in self._allowed_values:
if value in self._values:
self._checkboxes[value].setChecked(True)
else:
self._checkboxes[value].setChecked(False)
[docs]
def selectedValues(self):
"""Return what values of the enumeration have been selected.
Return (list of str): A comma seperated list of the selected values.
"""
return ",".join(sorted(self._values))
[docs]
class PypeItCustomEditorDelegate(QStyledItemDelegate):
"""Custom item delegate for rows in a PypeItMetadataView."""
def __init__(self, parent):
self.metadata_view=parent
super().__init__(parent)
[docs]
def editorClosed(self,editor, accepted):
"""Signal handler that is notified when the PypeItEnumListEditor is closed.
Args:
editor (PypeItEnumListEditor): The editor that was closed.
accepted (bool): True if the value was accepted, false if it was canceled.
"""
# Notify any clients to accept or revert any cached values. This signal is inherited from
# parent classes.
if accepted:
self.closeEditor.emit(editor, QAbstractItemDelegate.EndEditHint.SubmitModelCache)
self.setModelData(editor, editor.index.model(), editor.index)
else:
self.closeEditor.emit(editor, QAbstractItemDelegate.EndEditHint.RevertModelCache)
[docs]
def paint(self, painter, option, index):
"""
Overridden version of paint for painting items in the PypeItMetadataView.
"""
# This method is overridden because we want to display commented out items
# as if the are disabled, without actually disabling them. This allows them to
# be selected and uncommented out.
# Get the source model of ProxyModel PypeItMetadataView uses to sort items.
# This source model is a PypeItMetadataModel
model=index.model().sourceModel()
source_index = index.model().mapToSource(index)
if model.isCommentedOut(source_index):
# Paint as disabled
option.state &= ~QStyle.StateFlag.State_Enabled
super().paint(painter,option, index)
[docs]
def createEditor(self, parent, option, index):
"""
Creates an editor widget for an item in the metadata table. This will be a
PypeItEnumListEditor for the columns that use one, or the Qt default for other editable columns.
Overriden from QStyledItemDelegate.
Args:
parent (QWidget): The parent widget of the new editor.
option (QtWidgets.QStyleOptionViewItem): Additional options for the editor.
index (QModelIndex): The index of the table cell being edited.
"""
model = index.model().sourceModel()
column_name = model.getColumnNameFromNum(index)
if column_name == "frametype":
log.info("Creating enum list editor for frametype")
editor= PypeItEnumListEditor(parent=parent, index=index, num_lines=5, allowed_values=model.getAllFrameTypes())
editor.closed.connect(self.editorClosed)
return editor
log.info(f"Creating default editor for {column_name}")
return super().createEditor(parent, option, index)
[docs]
def setEditorData(self, editor, index):
"""Sets the data being edited in the editor. Overriden from QStyledItemDelegate.
Args:
editor (QWidget): The editor widget (created by createEditor)
index (QModelIndex): The index of the item being edited.
"""
if isinstance(editor, PypeItEnumListEditor):
log.info(f"Setting editor data {index.data(Qt.EditRole)}")
editor.setSelectedValues(index.data(Qt.EditRole))
else:
log.info("Setting default editor data")
super().setEditorData(editor, index)
[docs]
def setModelData(self,editor,model,index):
"""Sets the edited data in the model post editing. Overriden from QStyledItemDelegate.
Args:
editor (QWidget): The editor widget (created by createEditor).
model (QAbstractItemModel): The model being edited.
index (QModelIndex): The index of the item being edited.
"""
if isinstance(editor, PypeItEnumListEditor):
log.info(f"Setting choice model data: {editor.selectedValues()}")
model.setData(index, editor.selectedValues())
else:
log.info("Setting default model data")
super().setModelData(editor,model,index)
[docs]
def updateEditorGeometry(self, editor, option, index):
"""Sets the editor's position and size in the GUI. Overriden from QStyledItemDelegate.
Args:
editor (QWidget): The editor widget (created by crateEditor). This widgets geometry
is set by this method.
model (QAbstractItemModel): The model being edited
option (QtWidgets.QStyleOptionViewItem): Options object containing the recommended rectangle for the editor.
index (QModelIndex): The index of the item being edited.
"""
if isinstance(editor, PypeItEnumListEditor):
# The upper left coordinate of the editor depends on how well it fits
# vertically in it's parent
parent_geometry = editor.parent().geometry()
editor_min_size = editor.minimumSize()
log.info(f"Given rect: {(option.rect.x(), option.rect.y(), option.rect.width(), option.rect.height())}")
log.info(f"parent_geometry: {(parent_geometry.x(), parent_geometry.y(), parent_geometry.width(), parent_geometry.height())}")
log.info(f"editor min size: {editor_min_size.width()}, {editor_min_size.height()}")
editor_x = option.rect.x()
editor_y = option.rect.y()
# Let the editor fill up the size of the cell if that's larger than
# it's minimum width
editor_width = max(editor_min_size.width(), option.rect.width())
# Because the parent may be in a scrollable, the x,y could be negative,
# or underneath the header row. Set those so that the editor doesn't appear outside of the widget
if editor_x < 0:
editor_x = 0
min_y = self.metadata_view.verticalHeader().sectionSize(0)
if editor_y < min_y:
editor_y = 0
# Adjust the editor'x upper left corner so that the editor is vislbe for
# cells along the bottom or right of the parent table
right_x = self.metadata_view.viewport().geometry().bottomRight().x() - self.metadata_view.viewportMargins().right()
bottom_y = self.metadata_view.viewport().geometry().bottomRight().y() - self.metadata_view.viewportMargins().bottom()
# The bottom x,y of the view port is measured without the margins, but the editor is placed relative to those
# margins, so we have to include the left/top margins in the below calculations
if editor_x + self.metadata_view.viewportMargins().left() + editor_width > right_x:
editor_x = right_x - (editor_width + self.metadata_view.viewportMargins().left())
if editor_y + self.metadata_view.viewportMargins().top() + editor_min_size.height() > bottom_y:
editor_y = bottom_y - (editor_min_size.height() + self.metadata_view.viewportMargins().top())
geometry = QRect(editor_x, editor_y, editor_width, editor_min_size.height())
log.info(f"Updating editor geometry to {(geometry.x(), geometry.y(), geometry.width(), geometry.height())}")
editor.setGeometry(geometry)
else:
super().updateEditorGeometry(editor, option, index)
[docs]
class ConfigValuesPanel(QGroupBox):
"""Scrollable panel to display configuration values for one or more frames.
Args:
spec_name (str): Name of spectrograph for the configuration.
config (dict): The name/value pairs for the configuration keys defined by the spectrograph.
lines_to_display (int): How many lines to display initially.
parent (QWidget, Optional): The parent widget, defaults to None.
"""
def __init__(self, spec_name, config, lines_to_display, parent=None):
super().__init__(parent=parent)
self.setTitle(self.tr("Setup"))
# Set our configuration, adding the spectrograph name as the first item.
self.lines_to_display = lines_to_display
# Put everything in a scroll area
layout = QHBoxLayout(self)
self._scroll_area = QScrollArea(parent=self)
self._scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
# A widget using a form layout to display the spectrograph + configuration values
self._form_widget = QWidget()
self._form_widget_layout = QFormLayout(self._form_widget)
# Add margins to the right to avoid needing a horizontal scroll bar
fm = self.fontMetrics()
max_char_width = fm.maxWidth()
self._form_widget_layout.setContentsMargins(0, 0, max_char_width, 0)
# Keep this from expanding too large.
self._form_widget_layout.setSizeConstraint(QLayout.SizeConstraint.SetMinimumSize)
# Add a "Spectrograph" line to the form layout
self._config_labels = {}
label = QLabel(spec_name)
self._config_labels['Spectrograph'] = label
self._form_widget_layout.addRow('Spectrograph', label)
# Add values to form layout
for key, value in config.items():
label = QLabel(str(value))
self._form_widget_layout.addRow(key, label)
self._config_labels[key] = label
self._scroll_area.setWidget(self._form_widget)
# Set the minimum width of the form widget
self._form_widget.setMinimumWidth(self._getMinWidth())
layout.addWidget(self._scroll_area)
# Set margins within the group box
group_box_margin = int(fm.height()/2)
layout.setContentsMargins(group_box_margin, group_box_margin, group_box_margin, group_box_margin)
# Figure out the correct height for this panel, so that only the spectrograph and self.number_of_lines
# config keys are visible
log.info(f"font height: {fm.height()} vertical spacing {self._form_widget_layout.verticalSpacing()}")
self.setMaximumHeight(self.computeHeight(max(self.lines_to_display, len(self._config_labels))))
[docs]
def computeHeight(self, lines_to_display:int) ->int:
"""Compute the height needed to display a given number of lines
Args:
lines_to_display: The number of lines to display
Return:
The vertical size in pixels needed to display the given number of configuration lines
"""
fm = self.fontMetrics()
verticalSpacing = self._form_widget_layout.verticalSpacing()
if verticalSpacing == -1:
verticalSpacing = fm.leading()
self._form_widget_layout.setVerticalSpacing(fm.leading())
log.info(f"Set vertical spacing to {verticalSpacing}")
min_fw_height = (verticalSpacing)*(lines_to_display-1) + fm.height()*lines_to_display
# The height of this panel is that height plus the margins + the group box title
scroll_area_margins = self._scroll_area.contentsMargins()
group_box_margins = self.contentsMargins()
form_widget_margins = self._form_widget.contentsMargins()
layout_margins = self.layout().contentsMargins()
log.info(f"verticalSpacing: {self._form_widget_layout.verticalSpacing()}")
log.info(f"fontMetrics height/leading: {fm.height()}/{fm.leading()}")
log.info(f"group_box_margins (t/b) ({group_box_margins.top()}/{group_box_margins.bottom()})")
log.info(f"scroll_area_margins (t/b) ({scroll_area_margins.top()}/{scroll_area_margins.bottom()})")
log.info(f"layout_margins (t/b) ({layout_margins.top()}/{layout_margins.bottom()})")
log.info(f"form_widget_margins (t/b) ({form_widget_margins.top()}/{form_widget_margins.bottom()})")
computedHeight = (min_fw_height +
# fm.height() + # Group Box Title
group_box_margins.top() + group_box_margins.bottom() +
scroll_area_margins.top() + scroll_area_margins.bottom() +
layout_margins.top() + layout_margins.bottom() +
form_widget_margins.top() + form_widget_margins.bottom())
log.info(f"computedHeight: {computedHeight}")
return computedHeight
[docs]
def setNewValues(self, config_dict: dict) -> None:
"""Update the panel to display new configuration values.
Args:
config_dict: A dict of the new values.
"""
for key, value in config_dict.items():
label = self._config_labels.get(key,None)
if label is None:
label = QLabel(str(value))
self._config_labels[key] = label
self._form_widget_layout.addRow(key,label)
else:
label.setText(str(value))
# Reset the minimum width for the new values
self._form_widget.setMinimumWidth(self._getMinWidth())
# Reset the maximum height based on the new values.
self.setMaximumHeight(self.computeHeight(max(self.lines_to_display, len(self._config_labels))))
[docs]
def _getMinWidth(self) -> int:
"""Calculate the minimum width needed to display the configuration values."""
# We calculate our width to just fit around the text so we can stop the scroll area from
# covering it with a scrollbar.
fm = self.fontMetrics()
max_key_width = 0
max_value_width = 0
for key in self._config_labels:
value = self._config_labels[key].text()
key_width = fm.horizontalAdvance(key)
value_width = fm.horizontalAdvance(value)
if key_width > max_key_width:
max_key_width = key_width
if value_width > max_value_width:
max_value_width = value_width
margins = self._form_widget_layout.contentsMargins()
min_width = self._form_widget_layout.horizontalSpacing() + max_key_width + max_value_width + margins.left() + margins.right()
# Account for the scroll bar if needed
if len(self._config_labels) > self.lines_to_display:
if self._scroll_area.verticalScrollBar():
min_width += self._scroll_area.verticalScrollBar().sizeHint().width()
log.info(f"new minWidth: {min_width} max key width: {max_key_width} max_value width {max_value_width} horizontal spacing {self._form_widget_layout.horizontalSpacing()} margins left: {margins.left()} margins right: {margins.right()}")
return min_width
[docs]
class TabManagerBaseTab(QWidget):
"""Widget that acts as a tab for :class:`TabManagerWidget`. This defines the interface needed by TabManagerWidget
and provides a default implementation of it.
Args:
state (:obj:`pypeit.setup_gui.model.ModelState`): The state of the tab as displayed in the TabBar.
"""
stateChanged = Signal(str, ModelState)
"""Signal(str): Signal sent when the state of the tab changes. The the name of the tab and its state is passed."""
def __init__(self, parent=None, name="", closeable=False, state=ModelState.UNCHANGED):
super().__init__(parent)
self._name = name
self._closeable=closeable
self._state = state
@property
def name(self):
"""str: The name of the tab."""
return self._name
@property
def state(self):
"""ModelState: The state of the tab."""
return self._state
@property
def closeable(self):
"""bool: Whether the tab can be closed."""
return self._closeable
[docs]
class PypeItFileView(TabManagerBaseTab):
"""Widget for displaying the information needed for one pypeit file. This includes
the spectrograph, the configuration keys and values, the files that share that
configuration, and the PypeIt parameters.
Args:
(pypeit.setup_gui.model.PypeItFileModel): The model representing all the information needed for a .pypeit file.
(pypeit.setup_gui.model.PypeItFileController): The controller for managing the user's interaction with a PypeIt file)
"""
def __init__(self, model, controller):
# Allow file tabs to be closed
super().__init__(closeable=True)
layout = QVBoxLayout(self)
self.model = model
# Connect the model's state change signal to our own
self.model.stateChanged.connect(self.stateChanged)
# Add the file name as the first row and second rows
filename_label = QLabel(self.tr("Filename:"))
filename_label.setAlignment(Qt.AlignLeft)
layout.addWidget(filename_label)
self.filename_value = QLabel(model.filename)
self.filename_value.setAlignment(Qt.AlignLeft)
layout.addWidget(self.filename_value)
# The third row consists of a splitter, allowing the user to
# decide how much space to divide between the portions of a PypeIt file.
# These are displayed in the same order as in a .pypeit file:
# PypeIt Parameters
# Setup (or Config Valeus)
# Raw data paths
# File metadata
# Create a group box and a tree view for the pypeit parameters
params_group = QGroupBox(self.tr("PypeIt Parameters"))
params_group_layout = QHBoxLayout()
self.params_tree = QTreeView(params_group)
self.params_tree.setModel(model.params_model)
self.params_tree.header().setSectionResizeMode(QHeaderView.ResizeToContents)
self.params_tree.expandToDepth(1)
params_group_layout.addWidget(self.params_tree)
fm = params_group.fontMetrics()
group_box_padding = int(fm.height()/2)
params_group_layout.setContentsMargins(group_box_padding,group_box_padding,group_box_padding,group_box_padding)
params_group.setLayout(params_group_layout)
pg_cm = params_group.contentsMargins()
pt_cm = self.params_tree.contentsMargins()
# Compute the initial height to use in the q splitter
pg_init_height = (fm.lineSpacing() + # Title
pg_cm.top() + pg_cm.bottom() + # Group Box margin
pt_cm.top() + pt_cm.bottom() + # Params Tree margin
self.params_tree.header().sizeHint().height() + # Params tree header
3 * fm.lineSpacing() # desired # of rows
)
# The minimum height is always the height of the title, so the section can be hidden
# by the user
params_group.setMinimumHeight(fm.lineSpacing())
# Create the ConfigValuesPanel, displaying the spectrograph + config keys.
# We default to displaying only 3 lines of the configuration.
self.config_panel = ConfigValuesPanel(model.spec_name, model.config_values, 3, parent=self)
fm = self.config_panel.fontMetrics()
config_panel_init_height = self.config_panel.computeHeight(3)
self.config_panel.setMinimumHeight(fm.lineSpacing())
# Create the Raw Data directory panel
# This is not editable, because the user can add/remove directories by adding/removing individual
# files in the metadata_file_table
paths_group = QGroupBox(self.tr("Raw Data Directories"),self)
paths_group_layout = QVBoxLayout(paths_group)
paths_viewer = QListView(paths_group)
paths_viewer.setModel(model.paths_model)
paths_viewer.setSelectionMode(QAbstractItemView.SelectionMode.NoSelection)
paths_group_layout.setContentsMargins(group_box_padding,group_box_padding,group_box_padding,group_box_padding)
paths_group_layout.addWidget(paths_viewer)
pg_fm = paths_group.fontMetrics()
pv_fm = paths_viewer.fontMetrics()
pg_cm = paths_group.contentsMargins()
pv_cm = paths_viewer.contentsMargins()
# We set the initial height to be able to display two paths
paths_group_init_height = (pg_fm.lineSpacing() + # title line
pg_cm.top() + pg_cm.bottom() + # group widget margins
pv_cm.top() + pv_cm.bottom() + # list margins
2 * pv_fm.lineSpacing() ) # Two paths in the list
paths_group.setMinimumHeight(pg_fm.lineSpacing())
# Create a group box and metadata view for the file metadata table
file_group = QGroupBox(self.tr("File Metadata"))
file_group_layout = QVBoxLayout()
self.file_metadata_table = PypeItMetadataView(self, model.metadata_model, controller.getMetadataController(model.metadata_model))
file_group_layout.addWidget(self.file_metadata_table)
file_group.setLayout(file_group_layout)
self.file_group = file_group
fm = file_group.fontMetrics()
# The file metadata is allowed to stretch, so its initial height can start as its preferred size
file_group_init_height = file_group.sizeHint().height()
file_group.setMinimumHeight(fm.lineSpacing())
# Create the splitter to separate the four items
splitter = QSplitter(self)
splitter.setOrientation(Qt.Orientation.Vertical)
# Do not allow children to be collapsed beyond their minimimum size, so that a title section
# is always visible
splitter.setChildrenCollapsible(False)
splitter.addWidget(params_group)
splitter.addWidget(self.config_panel)
splitter.addWidget(paths_group)
splitter.addWidget(file_group)
layout.addWidget(splitter)
# Set the stretch factors to fixed for everything but the file metadata group
pg_index = splitter.indexOf(params_group)
cfg_index = splitter.indexOf(self.config_panel)
paths_index = splitter.indexOf(paths_group)
fg_index = splitter.indexOf(file_group)
splitter.setStretchFactor(pg_index, 0)
splitter.setStretchFactor(cfg_index, 0)
splitter.setStretchFactor(paths_index, 0)
splitter.setStretchFactor(fg_index, 1)
# Set the initial sizes of the four sections within the splitter
splitter.setSizes([pg_init_height,config_panel_init_height,paths_group_init_height,file_group_init_height])
# Monitor the model for updates
self.model.stateChanged.connect(self.update_from_model)
debugSizeStuff(self.config_panel,"Config Panel")
log.info(f"config panel flat: {self.config_panel.isFlat()}")
[docs]
def update_from_model(self):
"""
Signal handler that updates view when the underlying model changes.
"""
# update the filename if it changed from saving
self.filename_value.setText(self.model.filename)
# Update the config values
self.config_panel.setNewValues(self.model.config_values)
@property
def name(self):
"""
str: The configuration name.
"""
return self.model.name_stem
@property
def state(self):
"""
:class:`pypeit.setup_gui.model.ModelState`): The state of this configuration's model. NEW, CHANGED, or UNCHANGED.
"""
return self.model.state
[docs]
def selectedRows(self):
""" Return the selected rows in the metadata table.
Return:
List of row indexes of the selected rows in the metadata table.
"""
return self.file_metadata_table.selectedRows()
[docs]
class ObsLogView(TabManagerBaseTab):
"""Widget for displaying the observation log for raw data files for the same spectrograph but
potentially different observing configurations.
Args:
model (:class:`pypeit.setup_gui.model.PypeItObsLogModel`): Model object for a PypeIt Setup GUI.
controller (:class:`pypeit.setup_gui.controller.PypeItObsLogController`): Controller object for the PypeIt Setup GUI.
parent (QWidget): The parent widget of the tab.
"""
"""Signal sent when items are selected in the metadata table."""
def __init__(self, model, controller, parent=None):
super().__init__(parent=parent,name="ObsLog")
self._controller = controller
layout = QVBoxLayout(self)
# We use a splitter to separate the spectrograph/raw data paths from the file metadata
# Create the splitter to hold both rows
splitter=QSplitter(self)
splitter.setOrientation(Qt.Orientation.Vertical)
layout.addWidget(splitter)
# Do not allow children to be collapsed beyond their minimimum size, so that a title section
# is always visible
splitter.setChildrenCollapsible(False)
# First build a widget to contain the spectrograph/ raw data paths
spec_paths_widget = QWidget()
# Place the spectrograph group box and combo box in the first row, first column
spec_paths_layout = QHBoxLayout(spec_paths_widget)
# No Margins, this is just a container
spec_paths_layout.setContentsMargins(0,0,0,0)
spectrograph_box = QGroupBox(title=self.tr("Spectrograph"), parent=self)
spectrograph_layout = QHBoxLayout()
self.spectrograph = QComboBox(spectrograph_box)
self.spectrograph.addItems(available_spectrographs)
self.spectrograph.setCurrentIndex(-1)
self.spectrograph.setEditable(True)
self.spectrograph.lineEdit().setPlaceholderText(self.tr("Select a spectrograph"))
self.spectrograph.setInsertPolicy(QComboBox.NoInsert)
self.spectrograph.setValidator(SpectrographValidator())
fm = self.fontMetrics()
group_box_padding=int(fm.height()/2)
spectrograph_layout.setContentsMargins(group_box_padding,group_box_padding,group_box_padding,group_box_padding)
spectrograph_layout.addWidget(self.spectrograph)
spectrograph_layout.setAlignment(self.spectrograph, Qt.AlignTop)
spectrograph_box.setLayout(spectrograph_layout)
spec_paths_layout.addWidget(spectrograph_box)
# Create a Group Box to group the paths editor and viewer
paths_group = QGroupBox(self.tr("Raw Data Directories"),self)
paths_group_layout = QVBoxLayout(paths_group)
self.paths_editor = PathEditor(browse_caption=self.tr("Choose raw data directory"), parent=self)
self.paths_editor.setHistory(PersistentStringListModel("RawDataDirectory", "History"))
self.paths_editor.setEnabled(False)
self.paths_editor.pathEntered.connect(controller.addNewPath)
paths_group_layout.addWidget(self.paths_editor)
self._paths_viewer = QListView(paths_group)
self._paths_viewer.setModel(model.paths_model)
paths_group_layout.addWidget(self._paths_viewer)
paths_group_layout.setContentsMargins(group_box_padding,group_box_padding,group_box_padding,group_box_padding)
# The initial height of the first row in the splitter. The raw data paths will be larger
# so we use its size for the row. We start with it displaying 2 paths
initial_lines = 2
viewer_margins = self._paths_viewer.contentsMargins()
path_group_margins = paths_group.contentsMargins()
spec_paths_init_height = (fm.lineSpacing() + # Group titles
path_group_margins.top() + path_group_margins.bottom() + # Groupbox margins
group_box_padding + group_box_padding + # Group box layout margins
self.paths_editor.sizeHint().height() + # Path editor
paths_group_layout.spacing() + # Gap between editor and viewer
viewer_margins.top() + viewer_margins.bottom() + # viewer margins
fm.height()*initial_lines+self._paths_viewer.spacing()*(initial_lines-1) # Number of lines desired
)
# Add action for removing a path
self._paths_viewer.setSelectionMode(QAbstractItemView.SelectionMode.ExtendedSelection)
delete_action = QAction(self._paths_viewer, text=self.tr("Delete selected"))
delete_action.setToolTip(self.tr("Delete selected path"))
delete_action.setShortcut(QKeySequence.StandardKey.Delete)
delete_action.triggered.connect(self._deletePaths)
self._paths_viewer.addAction(delete_action)
self._paths_viewer.setContextMenuPolicy(Qt.ContextMenuPolicy.ActionsContextMenu)
# Add the Raw Data directory panel to the first row, second column
spec_paths_layout.addWidget(paths_group)
# Make the raw data paths wider than the spectrograph
spec_paths_layout.setStretch(1, 2)
# The second row consists of a group box containing the metadata view
file_group_widget = QGroupBox(self.tr("File Metadata"))
file_group_layout = QHBoxLayout()
self.obslog_table = PypeItMetadataView(file_group_widget, model.metadata_model, controller.getMetadataController(model.metadata_model))
file_group_layout.addWidget(self.obslog_table)
file_group_widget.setLayout(file_group_layout)
# The initial height for the second row, which will be allowed to stretch to fill the tab
file_group_init_height = file_group_widget.sizeHint().height()
# Set minimum height to the height of one line of text for the widgets inside the splitter.
# This prevents the splitter from hiding the titles of each section.
spectrograph_box.setMinimumHeight(spectrograph_box.fontMetrics().lineSpacing())
paths_group.setMinimumHeight(paths_group.fontMetrics().lineSpacing())
file_group_widget.setMinimumHeight(file_group_widget.fontMetrics().lineSpacing())
# Add the widgets to the splitter, and set the stretch factor such that the metadata will stretch to
# fill the available space, but the spectrograph/raw data paths will only stretch if the user decides to
# resize them.
splitter.addWidget(spec_paths_widget)
splitter.addWidget(file_group_widget)
spec_paths_index = splitter.indexOf(spec_paths_widget)
file_group_index = splitter.indexOf(file_group_widget)
splitter.setStretchFactor(spec_paths_index, 0)
splitter.setStretchFactor(file_group_index, 1)
# Set the initial heights of the splitter children
splitter.setSizes([spec_paths_init_height, file_group_init_height])
# Connect with the model
self.setModel(model)
# Update model with new spectrograph/data paths
self.spectrograph.textActivated.connect(controller.setSpectrograph)
self.spectrograph.textActivated.connect(self.update_raw_data_paths_state)
[docs]
def _deletePaths(self, parent):
"""Signal handler that removes raw data paths from the obslog"""
log.info(f"Delete selection")
selection = self._paths_viewer.selectedIndexes()
rows = [index.row() for index in selection]
self._controller.removePaths(rows)
[docs]
def setModel(self,model):
"""Set a new model for file metadata.
Args:
model (:class:`pypeit.setup_gui.model.PypeItSetupModel`): The new metadata model
"""
self.model=model
if model.spec_name is not None:
self.spectrograph.setCurrentIndex(self.spectrograph.findText(model.spec_name))
log.info(f"Set current text to {model.spec_name}, current index {self.spectrograph.currentIndex()}")
self.update_raw_data_paths_state()
self.obslog_table.setModel(model.metadata_model)
self._controller.setModel(model)
# Update based on changes to the model
model.spectrograph_changed.connect(self.update_from_model)
model.paths_model.dataChanged.connect(self.update_from_model)
model.paths_model.rowsRemoved.connect(self.update_from_model)
model.paths_model.modelReset.connect(self.update_from_model)
[docs]
def update_raw_data_paths_state(self):
"""Enable/Disable the raw data paths location panel based on the model state. """
if self.model.state == ModelState.NEW:
if self.spectrograph.currentIndex() == -1:
self.paths_editor.setEnabled(False)
else:
self.paths_editor.setEnabled(True)
else:
self.paths_editor.setEnabled(True)
[docs]
def update_from_model(self):
"""
Updates the spectrograph and raw data location widgets based on model updates.
"""
# Update the current spectrograph
if self.model.spec_name is None:
self.spectrograph.setCurrentIndex(-1)
else:
self.spectrograph.setCurrentText(self.model.spec_name)
# Disable changing the spectrograph if the model isn't in the new state
if self.model.state == ModelState.NEW:
self.spectrograph.setEnabled(True)
else:
self.spectrograph.setEnabled(False)
# Get the raw data paths state
self.update_raw_data_paths_state()
[docs]
def selectedRows(self):
""" Return the selected rows in the metadata table.
Return:
List of row indexes of the selected rows in the metadata table.
"""
# Return the metadata view's selected items
return self.obslog_table.selectedRows()
[docs]
class SpectrographValidator(QValidator):
"""Validator to check whether a spectrograph name is valid, or potentially valid.
This is used by the spectrograph combo box to allow tab completion without
allowing invalid names to be entered."""
def __init__(self):
super().__init__()
[docs]
def validate(self, str_input, int_input):
"""Validates whether a string is a valid spectrograph name, a
prefix of a valid spectrograph name, or invalid. The comparison is
case insensitive because the combo box QCompleter object will convert it to
the appropriate case. Overriden method from QValidator.
Args:
str_input: A string input to validate.
int_input: An integer input to validate (not used).
Returns:
QValidator.State: Acceptable, Intermediate, or Invalid based on the input.
"""
if str_input is not None:
if str_input.lower() in available_spectrographs:
return QValidator.Acceptable, str_input, int_input
else:
for spectrograph in available_spectrographs:
if spectrograph.startswith(str_input.lower()):
return QValidator.Intermediate, str_input, int_input
return QValidator.Invalid, str_input, int_input
[docs]
class SetupGUIMainWindow(QMainWindow):
"""Main window widget for the PypeIt Setup GUI
Args:
model (:class:`pypeit.setup_gui.model.SetupGUIStateModel`): The model for the PypeitSetupGUI.
controller (:class:`pypeit.setup_gui.controller.SetupGUIController`): The controller for the PypeitSetupGUI.
"""
helpURL = QUrl("https://pypeit.readthedocs.io/en/stable/tutorials/setup_gui.html")
def __init__(self, model, controller):
super().__init__(parent=None)
self.model = model
self.controller = controller
# Create the initial observation log tab
self._obs_log_tab = ObsLogView(model = model.obslog_model,
controller = controller.getObsLogController(model.obslog_model))
# Layout the main window
self.tab_widget = TabManagerWidget(parent=self)
index = self.tab_widget.addNewTab(self._obs_log_tab)
self.tab_widget.setCurrentIndex(index)
self.tab_widget.tabCreateRequest.connect(self.controller.createNewPypeItFile)
self.tab_widget.tabCloseRequested.connect(self._closeRequest)
# enable/disable adding new files based on whether the spectrograph has been set
self.model.obslog_model.spectrograph_changed.connect(self.update_new_file_allowed)
self.update_new_file_allowed()
self.setCentralWidget(self.tab_widget)
# Monitor the current tab
self._current_tab = self._obs_log_tab
self._tool_bar = self.addToolBar("")
self._tool_bar.setFloatable(False)
self._tool_bar.setAllowedAreas(Qt.TopToolBarArea | Qt.BottomToolBarArea)
self._populate_toolbar_and_menu()
# Setup may have already been run from the command line, so update button status on init
self.update_setup_button()
self.update_buttons_from_model_state()
# For viewing the log
self._logWindow = None
# For displaying operation progress
self.current_op_progress_dialog = None
# Monitor the model for new or closed files, and update the tab widget accordingly
self.model.filesAdded.connect(self.create_file_tabs)
self.model.filesDeleted.connect(self.delete_tabs)
self.setWindowTitle(self.tr("PypeIt Setup"))
self.resize(1650,900)
[docs]
def update_new_file_allowed(self):
"""Signal handler to enable/disable adding a new file based on whether the spectrograph has been set."""
self.tab_widget.setNewTabsEnabled(self.model.obslog_model.spec_name is not None)
[docs]
def create_progress_dialog(self, op_caption, max_progress_value, cancel_func):
"""Start displaying progress information for an operation. This uses the QProgressDialog, which will not
display itself until a minimum amount of time has passed (currently 1s)
Args:
op_caption (str): The name of the operation.
max_progress_value (int): The maximum progress value (i.e. the value when done).
cancel_func (:class:`collections.abc.Callable`): A callable to deal with cancel being pressed in the
progress dialog.
"""
log.info(f"Starting operation {op_caption} max progress: {max_progress_value}")
self.current_op_progress_dialog = QProgressDialog(self.tr(op_caption), self.tr("Cancel"), 0, max_progress_value, parent=self)
self.current_op_progress_dialog.setMinimumWidth(380)
self.current_op_progress_dialog.setWindowTitle(op_caption)
self.current_op_progress_dialog.setMinimumDuration(500)
self.current_op_progress_dialog.setValue(0)
self.current_op_progress_dialog.setWindowModality(Qt.WindowModal)
self.current_op_progress_dialog.canceled.connect(cancel_func)
[docs]
def show_operation_progress(self, increase, message=None):
"""
Increase the amount of progress being displayed in a progress dialog.
Args:
increase (int): How much to increase the current progress by.
message (str, Optional): A message indicating what step has been performed.
"""
log.info(f"dialog is none {self.current_op_progress_dialog is None}")
if self.current_op_progress_dialog is not None:
log.info(f"increase {increase} message{message} current value {self.current_op_progress_dialog.value()}")
if self.current_op_progress_dialog.maximum() > 0:
self.current_op_progress_dialog.setValue(self.current_op_progress_dialog.value() + increase)
if message is not None and message != self.current_op_progress_dialog.labelText():
self.current_op_progress_dialog.setLabelText(message)
[docs]
def operation_complete(self):
"""
Stop displaying progress for an operation because it has completed..
"""
log.info(f"Ending operation, dialog is none {self.current_op_progress_dialog is None}")
if self.current_op_progress_dialog is not None:
self.current_op_progress_dialog.done(QDialog.Accepted)
self.current_op_progress_dialog.close()
self.current_op_progress_dialog = None
[docs]
def update_save_tab_button(self):
"""Update the enabled/disabled state of the save tab button based on the
current selected tab."""
tab = self.tab_widget.currentWidget()
if tab.name == "ObsLog":
self.saveTabAction.setEnabled(False)
else:
self.saveTabAction.setEnabled(tab.state != ModelState.UNCHANGED)
[docs]
def update_setup_button(self):
"""Enable/disable the setup button based on whether a spectrograph and raw
data directories have been selected."""
# Setup can only be run if the spectrograph is set and there's at least one
# raw data directory
log.info(f"Checking setup button status spec: {self.model.obslog_model.spec_name} dirs {self.model.obslog_model.raw_data_directories}")
if (self.model.obslog_model.spec_name is not None and
len(self.model.obslog_model.raw_data_directories) > 0):
self.setupAction.setEnabled(True)
else:
self.setupAction.setEnabled(False)
[docs]
def update_buttons_from_model_state(self):
"""Update the enabled/disabled state of buttons based on the model state."""
self.saveAllAction.setEnabled(self.model.state==ModelState.CHANGED)
self.clearAction.setEnabled(self.model.state!=ModelState.NEW)
self.update_save_tab_button()
[docs]
def _closeRequest(self, index):
"""Called when the user tries to close a tab"""
tab = self.tab_widget.widget(index)
if tab.closeable:
self.controller.close(tab.model)
[docs]
def _showLog(self):
"""Signal handler that opens the log window."""
if self._logWindow is not None:
self._logWindow.activateWindow()
self._logWindow.raise_()
else:
self._logWindow = LogWindow(self.model.log_buffer)
self._logWindow.closed.connect(self._logClosed)
self._logWindow.show()
[docs]
def _logClosed(self):
"""Signal handler that clears the log window when it closes."""
self._logWindow = None
[docs]
def _helpButton(self):
"""Signal handler that responds to the help button being pressed."""
result = QDesktopServices.openUrl(self.helpURL)
if result:
log.info("Opened PypeIT docs.")
else:
log.warning(f"Failed to open PypeIt docs at '{self.helpURL}'")
[docs]
def _populate_toolbar_and_menu(self):
"""Populate the tool bar and menu bar by creating actions for the functionality accessible from the main window."""
# Create buttons for the toolbar and associated actions
# We use the QPushButton instead of the QToolButton because it looks better.
button = QPushButton(self.tr("&Open"))
button.setToolTip(self.tr("Open a .pypeit file."))
self.openAction = self._tool_bar.addWidget(button)
self.openAction.setText(button.text())
self.openAction.setToolTip(button.toolTip())
button.clicked.connect(self.openAction.triggered)
self.openAction.triggered.connect(self.controller.open_pypeit_file)
button = QPushButton(self.tr("&Clear"))
button.setToolTip(self.tr("Clear everything and start with a blank slate."))
self.clearAction = self._tool_bar.addWidget(button)
self.clearAction.setText(button.text())
self.clearAction.setToolTip(button.toolTip())
self.clearAction.setEnabled(False)
button.clicked.connect(self.clearAction.triggered)
self.clearAction.triggered.connect(self.controller.clear)
button = QPushButton(self.tr('&Run Setup'))
button.setToolTip(self.tr("Scan the raw data and create a .pypeit file for each unique configuration."))
self.setupAction = self._tool_bar.addWidget(button)
self.setupAction.setText(button.text())
self.setupAction.setToolTip(button.toolTip())
self.setupAction.setEnabled(False)
button.clicked.connect(self.setupAction.triggered)
self.setupAction.triggered.connect(self.controller.run_setup)
button = QPushButton(self.tr('&Save Tab'))
button.setToolTip(self.tr("Save the current tab."))
self.saveTabAction = self._tool_bar.addWidget(button)
self.saveTabAction.setText(button.text())
self.saveTabAction.setToolTip(button.toolTip())
self.saveTabAction.setEnabled(False)
button.clicked.connect(self.saveTabAction.triggered)
self.saveTabAction.triggered.connect(self.controller.save_one)
button = QPushButton(self.tr('Save &All'))
button.setToolTip(self.tr("Save all tabs with unsaved changes."))
self.saveAllAction = self._tool_bar.addWidget(button)
self.saveAllAction.setText(button.text())
self.saveAllAction.setToolTip(button.toolTip())
self.saveAllAction.setEnabled(False)
button.clicked.connect(self.saveAllAction.triggered)
self.saveAllAction.triggered.connect(self.controller.save_all)
# Create a spacer widget with horizontal size policy set to stretch, so that the
# remainder of the buttons are on the right side of the window
spacerWidget = QWidget()
spacerWidget.setSizePolicy(QSizePolicy.Policy.Expanding,QSizePolicy.Policy.Preferred)
self._tool_bar.addWidget(spacerWidget)
button = QPushButton(self.tr('&Help'))
button.setToolTip(self.tr("Opens PypeIt online documentation."))
self.helpAction = self._tool_bar.addWidget(button)
self.helpAction.setText(button.text())
self.helpAction.setToolTip(button.toolTip())
button.clicked.connect(self.helpAction.triggered)
self.helpAction.triggered.connect(self._helpButton)
button = QPushButton(self.tr('View &log'))
button.setToolTip(self.tr("Opens a window containing the log."))
self.viewLogAction = self._tool_bar.addWidget(button)
self.viewLogAction.setText(button.text())
self.viewLogAction.setToolTip(button.toolTip())
button.clicked.connect(self.viewLogAction.triggered)
self.viewLogAction.triggered.connect(self._showLog)
button = QPushButton(self.tr('E&xit'))
button.setToolTip(self.tr("Quits this application."))
self.exitAction = self._tool_bar.addWidget(button)
self.exitAction.setText(button.text())
self.exitAction.setToolTip(button.toolTip())
button.clicked.connect(self.exitAction.triggered)
self.exitAction.triggered.connect(self.controller.exit)
# Now create the menu bar, using the already created actions
self._menu = self.menuBar()
file_menu = self._menu.addMenu("&File")
file_menu.addAction(self.openAction)
file_menu.addAction(self.saveTabAction)
file_menu.addAction(self.saveAllAction)
file_menu.addSeparator()
file_menu.addAction(self.clearAction)
file_menu.addSeparator()
file_menu.addAction(self.exitAction)
run_menu = self._menu.addMenu("&Run")
run_menu.addAction(self.setupAction)
help_menu = self._menu.addMenu("&Help")
help_menu.addAction(self.helpAction)
help_menu.addAction(self.viewLogAction)
# Monitor when new files are added and removed,
# so we can update buttons.
self.model.obslog_model.paths_model.dataChanged.connect(self.update_setup_button)
self.model.obslog_model.paths_model.rowsRemoved.connect(self.update_setup_button)
self.model.obslog_model.paths_model.modelReset.connect(self.update_setup_button)
# Monitor spectrograph changes
self.model.obslog_model.spectrograph_changed.connect(self.update_setup_button)
# Monitor the current tab to update the save_tab button
self.tab_widget.currentChanged.connect(self.update_save_tab_button)
# Monitor the application's NEW/CHANGED/UNCHANGED state to enable/disable buttons
self.model.stateChanged.connect(self.update_buttons_from_model_state)
[docs]
def create_file_tabs(self, pypeit_file_models):
"""
Create new tabs for new unique configurations found either by openeing a pypeit file or
by reading raw data directories.
Args:
pypeit_file_models (list of :class:`pypeit.setup_gui.model.PypeItFileModel`): Models for the tabs to add.
"""
log.info(f"create_file_tabs for {len(pypeit_file_models)} unique configs")
try:
self.tab_widget.setUpdatesEnabled(False) # To prevent flickering when updating
for model in pypeit_file_models:
new_tab = PypeItFileView(model, self.controller.getPypeItFileController(model))
self.tab_widget.addNewTab(new_tab)
finally:
self.tab_widget.setUpdatesEnabled(True) # To allow redrawing after updating
[docs]
def delete_tabs(self, file_list):
"""
Delete any tabs no longer in the model.
Args:
tab_list (list of str): List of the configuration names removed.
"""
log.info(f"View Deleting tabs {file_list}")
if len(file_list) == 0:
return
for file_name in file_list:
self.tab_widget.closeTab(file_name)