Пример #1
0
 def __init__(self, parent=None, logname=None, level=logging.NOTSET):
     QWidget.__init__(self, parent=parent)
     # Create Widgets
     self.label = QLabel('Minimum displayed log level: ', parent=self)
     self.combo = QComboBox(parent=self)
     self.text = QPlainTextEdit(parent=self)
     self.text.setReadOnly(True)
     self.clear_btn = QPushButton("Clear", parent=self)
     # Create layout
     layout = QVBoxLayout()
     level_control = QHBoxLayout()
     level_control.addWidget(self.label)
     level_control.addWidget(self.combo)
     layout.addLayout(level_control)
     layout.addWidget(self.text)
     layout.addWidget(self.clear_btn)
     self.setLayout(layout)
     # Allow QCombobox to control log level
     for log_level, value in LogLevels.as_dict().items():
         self.combo.addItem(log_level, value)
     self.combo.currentIndexChanged[str].connect(self.setLevel)
     # Allow QPushButton to clear log text
     self.clear_btn.clicked.connect(self.clear)
     # Create a handler with the default format
     self.handler = GuiHandler(level=level, parent=self)
     self.logFormat = self.default_format
     self.handler.message.connect(self.write)
     # Create logger. Either as a root or given logname
     self.log = None
     self.level = None
     self.logName = logname or ''
     self.logLevel = level
     self.destroyed.connect(functools.partial(logger_destroyed, self.log))
Пример #2
0
    def __init__(self, parent=None):
        QPlainTextEdit.__init__(self, parent)
        BaseEditMixin.__init__(self)
        self.setAttribute(Qt.WA_DeleteOnClose)

        self.extra_selections_dict = {}

        self.textChanged.connect(self.changed)
        self.cursorPositionChanged.connect(self.cursor_position_changed)

        self.indent_chars = " "*4
        self.tab_stop_width_spaces = 4

        # Code completion / calltips
        if parent is not None:
            mainwin = parent
            while not isinstance(mainwin, QMainWindow):
                mainwin = mainwin.parent()
                if mainwin is None:
                    break
            if mainwin is not None:
                parent = mainwin

        self.completion_widget = CompletionWidget(self, parent)
        self.codecompletion_auto = False
        self.setup_completion()

        self.calltip_widget = CallTipWidget(self, hide_timer_on=False)
        self.calltip_position = None
        self.tooltip_widget = ToolTipWidget(parent, as_tooltip=True)

        self.has_cell_separators = False
        self.highlight_current_cell_enabled = False

        # The color values may be overridden by the syntax highlighter
        # Highlight current line color
        self.currentline_color = QColor(Qt.red).lighter(190)
        self.currentcell_color = QColor(Qt.red).lighter(194)

        # Brace matching
        self.bracepos = None
        self.matched_p_color = QColor(Qt.green)
        self.unmatched_p_color = QColor(Qt.red)

        self.last_cursor_cell = None

        self.decorations = TextDecorationsManager(self)
Пример #3
0
 def wheelEvent(self, event):
     """Reimplemented to emit zoom in/out signals when Ctrl is pressed"""
     # This feature is disabled on MacOS, see Issue 1510
     if sys.platform != 'darwin':
         if event.modifiers() & Qt.ControlModifier:
             if hasattr(event, 'angleDelta'):
                 if event.angleDelta().y() < 0:
                     self.zoom_out.emit()
                 elif event.angleDelta().y() > 0:
                     self.zoom_in.emit()
             elif hasattr(event, 'delta'):
                 if event.delta() < 0:
                     self.zoom_out.emit()
                 elif event.delta() > 0:
                     self.zoom_in.emit()
             return
     QPlainTextEdit.wheelEvent(self, event)
     self.highlight_current_cell()
Пример #4
0
class DisplayDialog(QDialog):
    """
    This is a simple dialog display which can be configured by users
    """
    def __init__(self, parent=None, name='Test'):
        """ init
        :param parent:
        """
        super(DisplayDialog, self).__init__(parent)

        layout = QVBoxLayout(self)

        # nice widget for editing the date
        self.message_edit = QPlainTextEdit(self)
        self.message_edit.setReadOnly(True)
        layout.addWidget(self.message_edit)

        self.setWindowTitle('Merged Scans Workspace Names')

        # OK and Cancel buttons
        buttons = QDialogButtonBox(QDialogButtonBox.Ok, QtCore.Qt.Horizontal, self)

        buttons.accepted.connect(self.accept)
        layout.addWidget(buttons)

        self.name = name

        return

    def set_name(self, new_name):

        self.name = new_name

    def show_message(self, message):
        """
        show message
        :param message:
        :return:
        """
        self.message_edit.setPlainText(message)

        return
Пример #5
0
class OutputPreviewWidget(QWidget):
    """
    A .py preview widget that appears as a drawer on the side of the main
    import widget.
    """

    def __init__(self, parent=None):
        super(OutputPreviewWidget, self).__init__(parent=parent)
        self.setWindowFlags(Qt.Sheet)
        self.text_editor = QPlainTextEdit()
        self.close_button = QPushButton('Close')
        self.layout = QVBoxLayout()
        self.layout.addWidget(self.text_editor)
        self.layout.addWidget(self.close_button)
        self.setLayout(self.layout)
        self.close_button.clicked.connect(self.hide)
        self.resize(400, 500)
        font = QFont('Courier')
        self.text_editor.setFont(font)
        self.text_editor.setReadOnly(True)

    def set_text(self, text):
        """
        Set the text in the loader preview widget. This will display the text that
        will be saved to the output loader python file.

        Parameters
        ----------
        text: str
            Text that will be saved to the final loader file
        """

        self.text_editor.setPlainText(text)
Пример #6
0
 def __init__(self, parent=None):
     super(OutputPreviewWidget, self).__init__(parent=parent)
     self.setWindowFlags(Qt.Sheet)
     self.text_editor = QPlainTextEdit()
     self.close_button = QPushButton('Close')
     self.layout = QVBoxLayout()
     self.layout.addWidget(self.text_editor)
     self.layout.addWidget(self.close_button)
     self.setLayout(self.layout)
     self.close_button.clicked.connect(self.hide)
     self.resize(400, 500)
     font = QFont('Courier')
     self.text_editor.setFont(font)
     self.text_editor.setReadOnly(True)
Пример #7
0
    def __init__(self, parent=None, label_name=''):
        """
        :param parent:
        :param label_name
        """
        super(GetValueDialog, self).__init__(parent)

        layout = QVBoxLayout(self)

        # details information
        self.info_line = QPlainTextEdit(self)
        self.info_line.setEnabled(False)
        layout.addWidget(self.info_line)

        # input
        self.label = QLabel(self)
        self.value_edit = QLineEdit(self)
        layout.addWidget(self.label)
        layout.addWidget(self.value_edit)
        # END-IF-ELSE

        # nice widget for editing the date
        # self.datetime = QDateTimeEdit(self)
        # self.datetime.setCalendarPopup(True)
        # self.datetime.setDateTime(QDateTime.currentDateTime())
        # layout.addWidget(self.datetime)

        # OK and Cancel buttons
        buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel,
                                   QtCore.Qt.Horizontal, self)

        buttons.accepted.connect(self.accept)
        buttons.rejected.connect(self.reject)
        layout.addWidget(buttons)

        # set some values
        self.setWindowTitle('Get user input')
        self.label.setText(label_name)

        return
Пример #8
0
 def create_textedit(self, text, option, default=NoDefault,
                     tip=None, restart=False):
     label = QLabel(text)
     label.setWordWrap(True)
     edit = QPlainTextEdit()
     edit.setWordWrapMode(QTextOption.WordWrap)
     layout = QVBoxLayout()
     layout.addWidget(label)
     layout.addWidget(edit)
     layout.setContentsMargins(0, 0, 0, 0)
     if tip:
         edit.setToolTip(tip)
     self.textedits[edit] = (option, default)
     widget = QWidget(self)
     widget.label = label
     widget.textbox = edit
     widget.setLayout(layout)
     edit.restart_required = restart
     edit.label_text = text
     return widget
Пример #9
0
    def __init__(self, parent=None, name='Test'):
        """ init
        :param parent:
        """
        super(DisplayDialog, self).__init__(parent)

        layout = QVBoxLayout(self)

        # nice widget for editing the date
        self.message_edit = QPlainTextEdit(self)
        self.message_edit.setReadOnly(True)
        layout.addWidget(self.message_edit)

        self.setWindowTitle('Merged Scans Workspace Names')

        # OK and Cancel buttons
        buttons = QDialogButtonBox(QDialogButtonBox.Ok, QtCore.Qt.Horizontal, self)

        buttons.accepted.connect(self.accept)
        layout.addWidget(buttons)

        self.name = name

        return
Пример #10
0
 def mousePressEvent(self, event):
     """Reimplement Qt method"""
     if sys.platform.startswith('linux') and event.button() == Qt.MidButton:
         self.calltip_widget.hide()
         self.setFocus()
         event = QMouseEvent(QEvent.MouseButtonPress, event.pos(),
                             Qt.LeftButton, Qt.LeftButton, Qt.NoModifier)
         QPlainTextEdit.mousePressEvent(self, event)
         QPlainTextEdit.mouseReleaseEvent(self, event)
         # Send selection text to clipboard to be able to use
         # the paste method and avoid the strange Issue 1445
         # NOTE: This issue seems a focusing problem but it
         # seems really hard to track
         mode_clip = QClipboard.Clipboard
         mode_sel = QClipboard.Selection
         text_clip = QApplication.clipboard().text(mode=mode_clip)
         text_sel = QApplication.clipboard().text(mode=mode_sel)
         QApplication.clipboard().setText(text_sel, mode=mode_clip)
         self.paste()
         QApplication.clipboard().setText(text_clip, mode=mode_clip)
     else:
         self.calltip_widget.hide()
         QPlainTextEdit.mousePressEvent(self, event)
Пример #11
0
 def focusInEvent(self, event):
     """Reimplemented to handle focus"""
     self.focus_changed.emit()
     self.focus_in.emit()
     self.highlight_current_cell()
     QPlainTextEdit.focusInEvent(self, event)
Пример #12
0
 def focusOutEvent(self, event):
     """Reimplemented to handle focus"""
     self.focus_changed.emit()
     QPlainTextEdit.focusOutEvent(self, event)
Пример #13
0
    def __init__(self, settings: PartSettings):
        super().__init__()
        self._settings = settings
        self.export_btn = QPushButton("Export profile")
        self.export_btn.clicked.connect(self.export_profile)
        self.import_btn = QPushButton("Import profile")
        self.import_btn.clicked.connect(self.import_profiles)
        self.export_pipeline_btn = QPushButton("Export pipeline")
        self.export_pipeline_btn.clicked.connect(self.export_pipeline)
        self.import_pipeline_btn = QPushButton("Import pipeline")
        self.import_pipeline_btn.clicked.connect(self.import_pipeline)
        self.delete_btn = QPushButton("Delete profile")
        self.delete_btn.setDisabled(True)
        self.delete_btn.clicked.connect(self.delete_profile)
        self.multiple_files_chk = QCheckBox("Show multiple files panel")
        self.multiple_files_chk.setChecked(
            self._settings.get("multiple_files_widget", False))
        self.multiple_files_chk.stateChanged.connect(
            self.multiple_files_visibility)
        self.rename_btn = QPushButton("Rename profile")
        self.rename_btn.clicked.connect(self.rename_profile)
        self.rename_btn.setDisabled(True)
        self.voxel_size_label = QLabel()
        self.info_label = QPlainTextEdit()
        self.info_label.setReadOnly(True)
        self.profile_list = SearchableListWidget()
        self.profile_list.currentTextChanged.connect(self.profile_chosen)
        self.pipeline_list = SearchableListWidget()
        self.pipeline_list.currentTextChanged.connect(self.profile_chosen)
        self.spacing = [QDoubleSpinBox() for _ in range(3)]
        self.lock_spacing = LockCheckBox()
        self.lock_spacing.stateChanged.connect(self.spacing[1].setDisabled)
        self.lock_spacing.stateChanged.connect(self.synchronize_spacing)
        # noinspection PyUnresolvedReferences
        self.spacing[2].valueChanged.connect(self.synchronize_spacing)
        units_value = self._settings.get("units_value", Units.nm)
        for el in self.spacing:
            el.setAlignment(Qt.AlignRight)
            el.setButtonSymbols(QAbstractSpinBox.NoButtons)
            el.setRange(0, 1000000)
            # noinspection PyUnresolvedReferences
            el.valueChanged.connect(self.image_spacing_change)
        self.units = EnumComboBox(Units)
        self.units.set_value(units_value)
        # noinspection PyUnresolvedReferences
        self.units.currentIndexChanged.connect(self.update_spacing)

        spacing_layout = QHBoxLayout()
        spacing_layout.addWidget(self.lock_spacing)
        for txt, el in zip(["x", "y", "z"], self.spacing[::-1]):
            spacing_layout.addWidget(QLabel(txt + ":"))
            spacing_layout.addWidget(el)
        spacing_layout.addWidget(self.units)
        spacing_layout.addStretch(1)
        voxel_size_layout = QHBoxLayout()
        voxel_size_layout.addWidget(self.voxel_size_label)
        voxel_size_layout.addSpacing(30)
        profile_layout = QGridLayout()
        profile_layout.setSpacing(0)
        profile_layout.addWidget(QLabel("Profiles:"), 0, 0)
        profile_layout.addWidget(self.profile_list, 1, 0)
        profile_layout.addWidget(QLabel("Pipelines:"), 2, 0)
        profile_layout.addWidget(self.pipeline_list, 3, 0, 4, 1)
        profile_layout.addWidget(self.info_label, 1, 1, 3, 2)
        profile_layout.addWidget(self.export_btn, 4, 1)
        profile_layout.addWidget(self.import_btn, 4, 2)
        profile_layout.addWidget(self.export_pipeline_btn, 5, 1)
        profile_layout.addWidget(self.import_pipeline_btn, 5, 2)
        profile_layout.addWidget(self.delete_btn, 6, 1)
        profile_layout.addWidget(self.rename_btn, 6, 2)
        layout = QVBoxLayout()
        layout.addLayout(spacing_layout)
        layout.addLayout(voxel_size_layout)
        layout.addWidget(self.multiple_files_chk)

        layout.addLayout(profile_layout, 1)
        self.setLayout(layout)
Пример #14
0
class PyDMLogDisplay(QWidget, LogLevels):
    """
    Standard display for Log Output

    This widget handles instantating a ``GuiHandler`` and displaying log
    messages to a ``QPlainTextEdit``. The level of the log can be changed from
    inside the widget itself, allowing users to select from any of the
    ``.levels`` specified by the widget.

    Parameters
    ----------
    parent : QObject, optional

    logname : str
        Name of log to display in widget

    level : logging.Level
        Initial level of log display

    """
    Q_ENUMS(LogLevels)
    LogLevels = LogLevels
    terminator = '\n'
    default_format = '%(asctime)s %(message)s'
    default_level = logging.INFO

    def __init__(self, parent=None, logname=None, level=logging.NOTSET):
        QWidget.__init__(self, parent=parent)
        # Create Widgets
        self.label = QLabel('Minimum displayed log level: ', parent=self)
        self.combo = QComboBox(parent=self)
        self.text = QPlainTextEdit(parent=self)
        self.text.setReadOnly(True)
        self.clear_btn = QPushButton("Clear", parent=self)
        # Create layout
        layout = QVBoxLayout()
        level_control = QHBoxLayout()
        level_control.addWidget(self.label)
        level_control.addWidget(self.combo)
        layout.addLayout(level_control)
        layout.addWidget(self.text)
        layout.addWidget(self.clear_btn)
        self.setLayout(layout)
        # Allow QCombobox to control log level
        for log_level, value in LogLevels.as_dict().items():
            self.combo.addItem(log_level, value)
        self.combo.currentIndexChanged[str].connect(self.setLevel)
        # Allow QPushButton to clear log text
        self.clear_btn.clicked.connect(self.clear)
        # Create a handler with the default format
        self.handler = GuiHandler(level=level, parent=self)
        self.logFormat = self.default_format
        self.handler.message.connect(self.write)
        # Create logger. Either as a root or given logname
        self.log = None
        self.level = None
        self.logName = logname or ''
        self.logLevel = level

    def sizeHint(self):
        return QSize(400, 300)

    @Property(LogLevels)
    def logLevel(self):
        return self.level

    @logLevel.setter
    def logLevel(self, level):
        if level != self.level:
            self.level = level
            idx = self.combo.findData(level)
            self.combo.setCurrentIndex(idx)

    @Property(str)
    def logName(self):
        """Name of associated log"""
        return self.log.name

    @logName.setter
    def logName(self, name):
        # Disconnect prior log from handler
        if self.log:
            self.log.removeHandler(self.handler)
        # Reattach handler to new handler
        self.log = logging.getLogger(name)
        # Ensure that the log matches level of handler
        # only if the handler level is less than the log.
        if self.log.level < self.handler.level:
            self.log.setLevel(self.handler.level)
        # Attach preconfigured handler
        self.log.addHandler(self.handler)

    @Property(str)
    def logFormat(self):
        """Format for log messages"""
        return self.handler.formatter._fmt

    @logFormat.setter
    def logFormat(self, fmt):
        self.handler.setFormatter(logging.Formatter(fmt))

    @Slot(str)
    def write(self, message):
        """Write a message to the log display"""
        # We split the incoming message by new lines. In prior iterations of
        # this widget it was discovered that large blocks of text cause issues
        # at the Qt level.
        for msg in message.split(self.terminator):
            self.text.appendPlainText(msg)

    @Slot()
    def clear(self):
        """Clear the text area."""
        self.text.clear()

    @Slot(str)
    def setLevel(self, level):
        """Set the level of the contained logger"""
        # Get the level from the incoming string specification
        try:
            level = getattr(logging, level.upper())
        except AttributeError as exc:
            logger.exception("Invalid logging level specified %s",
                             level.upper())
        else:
            # Set the existing handler and logger to this level
            self.handler.setLevel(level)
            if self.log.level > self.handler.level or self.log.level == logging.NOTSET:
                self.log.setLevel(self.handler.level)
Пример #15
0
class MOSVizViewer(DataViewer):

    LABEL = "MOSViz Viewer"
    window_closed = Signal()
    _toolbar_cls = MOSViewerToolbar
    tools = []
    subtools = []

    def __init__(self, session, parent=None):
        super(MOSVizViewer, self).__init__(session, parent=parent)

        self.slit_controller = SlitController(self)

        self.load_ui()

        # Define some data containers
        self.filepath = None
        self.savepath = None
        self.data_idx = None
        self.comments = False
        self.textChangedAt = None
        self.mask = None
        self.cutout_wcs = None
        self.level2_data = None
        self.spec2d_data = None

        self.catalog = None
        self.current_row = None
        self._specviz_instance = None
        self._loaded_data = {}
        self._primary_data = None
        self._layer_view = SimpleLayerWidget(parent=self)
        self._layer_view.layer_combo.currentIndexChanged.connect(self._selection_changed)
        self.resize(800, 600)

        self.image_viewer_hidden = False

    def load_ui(self):
        """
        Setup the MOSView viewer interface.
        """
        self.central_widget = QWidget(self)

        path = os.path.join(UI_DIR, 'mos_widget.ui')
        loadUi(path, self.central_widget)

        self.image_widget = DrawableImageWidget(slit_controller=self.slit_controller)
        self.spectrum2d_widget = Spectrum2DWidget()

        self._specviz_viewer = Workspace()
        self._specviz_viewer.add_plot_window()
        self.spectrum1d_widget = self._specviz_viewer.current_plot_window
        self.spectrum1d_widget.plot_widget.getPlotItem().layout.setContentsMargins(45, 0, 25, 0)

        # Set up helper for sharing axes. SharedAxisHelper defaults to no sharing
        # and we control the sharing later by setting .sharex and .sharey on the
        # helper
        self.spectrum2d_image_share = SharedAxisHelper(self.spectrum2d_widget._axes,
                                                       self.image_widget._axes)

        # We only need to set the image widget to keep the same aspect ratio
        # since the two other viewers don't require square pixels, so the axes
        # should not change shape.
        self.image_widget._axes.set_adjustable('datalim')

        self.meta_form_layout = self.central_widget.meta_form_layout
        self.meta_form_layout.setFieldGrowthPolicy(self.meta_form_layout.ExpandingFieldsGrow)
        self.central_widget.left_vertical_splitter.insertWidget(0, self.image_widget)
        self.central_widget.right_vertical_splitter.addWidget(self.spectrum2d_widget)
        self.central_widget.right_vertical_splitter.addWidget(self.spectrum1d_widget.widget())

        # Set the splitter stretch factors
        self.central_widget.left_vertical_splitter.setStretchFactor(0, 1)
        self.central_widget.left_vertical_splitter.setStretchFactor(1, 8)

        self.central_widget.right_vertical_splitter.setStretchFactor(0, 1)
        self.central_widget.right_vertical_splitter.setStretchFactor(1, 2)

        self.central_widget.horizontal_splitter.setStretchFactor(0, 1)
        self.central_widget.horizontal_splitter.setStretchFactor(1, 2)

        # Keep the left and right splitters in sync otherwise the axes don't line up
        self.central_widget.left_vertical_splitter.splitterMoved.connect(self._left_splitter_moved)
        self.central_widget.right_vertical_splitter.splitterMoved.connect(self._right_splitter_moved)

        # Set the central widget
        self.setCentralWidget(self.central_widget)

        self.central_widget.show()
        # Define the options widget
        self._options_widget = OptionsWidget()

    def show(self, *args, **kwargs):
        super(MOSVizViewer, self).show(*args, **kwargs)
        # Trigger a sync between the splitters
        self._left_splitter_moved()

        if self.image_viewer_hidden:
            self.image_widget.hide()
        else:
            self.image_widget.show()

    @avoid_circular
    def _right_splitter_moved(self, *args, **kwargs):
        if self.image_widget.isHidden():
            return
        sizes = self.central_widget.right_vertical_splitter.sizes()
        if sizes == [0, 0]:
            sizes = [230, 230]
        self.central_widget.left_vertical_splitter.setSizes(sizes)

    @avoid_circular
    def _left_splitter_moved(self, *args, **kwargs):
        if self.image_widget.isHidden():
            return
        sizes = self.central_widget.left_vertical_splitter.sizes()
        if sizes == [0, 0]:
            sizes = [230, 230]
        self.central_widget.right_vertical_splitter.setSizes(sizes)

    def setup_connections(self):
        """
        Connects gui elements to event calls.
        """
        # Connect the selection event for the combo box to what's displayed
        self.toolbar.source_select.currentIndexChanged[int].connect(
            lambda ind: self.load_selection(self.catalog[ind]))

        self.toolbar.source_select.currentIndexChanged[int].connect(
            lambda ind: self._set_navigation(ind))

        # Connect the exposure selection event
        self.toolbar.exposure_select.currentIndexChanged[int].connect(
            lambda ind: self.load_exposure(ind))

        self.toolbar.exposure_select.currentIndexChanged[int].connect(
            lambda ind: self._set_exposure_navigation(ind))

        # Connect the specviz button
        if SpecvizDataViewer is not None:
            self.toolbar.open_specviz.triggered.connect(
                lambda: self._open_in_specviz())
        else:
            self.toolbar.open_specviz.setDisabled(True)

        # Connect slit previous and next buttons
        self.toolbar.cycle_next_action.triggered.connect(
            lambda: self._set_navigation(
                self.toolbar.source_select.currentIndex() + 1))
        self.toolbar.cycle_previous_action.triggered.connect(
            lambda: self._set_navigation(
                self.toolbar.source_select.currentIndex() - 1))

        # Connect exposure previous and next buttons
        self.toolbar.exposure_next_action.triggered.connect(
            lambda: self._set_exposure_navigation(
                self.toolbar.exposure_select.currentIndex() + 1))
        self.toolbar.exposure_previous_action.triggered.connect(
            lambda: self._set_exposure_navigation(
                self.toolbar.exposure_select.currentIndex() - 1))

        # Connect the toolbar axes setting actions
        self.toolbar.lock_x_action.triggered.connect(
            lambda state: self.set_locked_axes(x=state))

        self.toolbar.lock_y_action.triggered.connect(
            lambda state: self.set_locked_axes(y=state))

    def options_widget(self):
        return self._options_widget

    def initialize_toolbar(self):
        """
        Initialize the custom toolbar for the MOSViz viewer.
        """
        from glue.config import viewer_tool

        self.toolbar = self._toolbar_cls(self)

        for tool_id in self.tools:
            mode_cls = viewer_tool.members[tool_id]
            mode = mode_cls(self)
            self.toolbar.add_tool(mode)

        self.addToolBar(self.toolbar)

        self.setup_connections()

    def register_to_hub(self, hub):
        super(MOSVizViewer, self).register_to_hub(hub)

        def has_data_or_subset(x):
            if x.sender is self._primary_data:
                return True
            elif isinstance(x.sender, Subset) and x.sender.data is self._primary_data:
                return True
            else:
                return False

        hub.subscribe(self, msg.SubsetCreateMessage,
                      handler=self._add_subset,
                      filter=has_data_or_subset)

        hub.subscribe(self, msg.SubsetUpdateMessage,
                      handler=self._update_subset,
                      filter=has_data_or_subset)

        hub.subscribe(self, msg.SubsetDeleteMessage,
                      handler=self._remove_subset,
                      filter=has_data_or_subset)

        hub.subscribe(self, msg.DataUpdateMessage,
                      handler=self._update_data,
                      filter=has_data_or_subset)

    def add_data(self, data):
        """
        Processes data message from the central communication hub.

        Parameters
        ----------
        data : :class:`glue.core.data.Data`
            Data object.
        """

        # Check whether the data is suitable for the MOSViz viewer - basically
        # we expect a table of 1D columns with at least three string and four
        # floating-point columns.

        if data.ndim != 1:
            QMessageBox.critical(self, "Error", "MOSViz viewer can only be used "
                                 "for data with 1-dimensional components",
                                 buttons=QMessageBox.Ok)
            return False

        components = [data.get_component(cid) for cid in data.main_components]

        categorical = [c for c in components if c.categorical]
        if len(categorical) < 3:
            QMessageBox.critical(self, "Error", "MOSViz viewer expected at least "
                                 "three string components/columns, representing "
                                 "the filenames of the 1D and 2D spectra and "
                                 "cutouts", buttons=QMessageBox.Ok)
            return False

        # We can relax the following requirement if we make the slit parameters
        # optional
        numerical = [c for c in components if c.numeric]
        if len(numerical) < 4:
            QMessageBox.critical(self, "Error", "MOSViz viewer expected at least "
                                 "four numerical components/columns, representing "
                                 "the slit position, length, and position angle",
                                 buttons=QMessageBox.Ok)
            return False

        # Make sure the loaders and column names are correct
        result = confirm_loaders_and_column_names(data)
        if not result:
            return False

        self._primary_data = data
        self._layer_view.data = data
        self._unpack_selection(data)
        return True

    def add_data_for_testing(self, data):
        """
        Processes data message from the central communication hub.

        Parameters
        ----------
        data : :class:`glue.core.data.Data`
            Data object.
        """

        # Check whether the data is suitable for the MOSViz viewer - basically
        # we expect a table of 1D columns with at least three string and four
        # floating-point columns.
        if data.ndim != 1:
            QMessageBox.critical(self, "Error", "MOSViz viewer can only be used "
                                 "for data with 1-dimensional components",
                                 buttons=QMessageBox.Ok)
            return False

        components = [data.get_component(cid) for cid in data.main_components]
        categorical = [c for c in components if c.categorical]
        if len(categorical) < 3:
            QMessageBox.critical(self, "Error", "MOSViz viewer expected at least "
                                 "three string components/columns, representing "
                                 "the filenames of the 1D and 2D spectra and "
                                 "cutouts", buttons=QMessageBox.Ok)
            return False

        # We can relax the following requirement if we make the slit parameters
        # optional
        numerical = [c for c in components if c.numeric]
        if len(numerical) < 4:
            QMessageBox.critical(self, "Error", "MOSViz viewer expected at least "
                                 "four numerical components/columns, representing "
                                 "the slit position, length, and position angle",
                                 buttons=QMessageBox.Ok)
            return False

        # Block of code to bypass the loader_selection gui
        #########################################################
        if 'loaders' not in data.meta:
            data.meta['loaders'] = {}

        # Deimos data
        data.meta['loaders']['spectrum1d'] = "DEIMOS 1D Spectrum"
        data.meta['loaders']['spectrum2d'] = "DEIMOS 2D Spectrum"
        data.meta['loaders']['cutout'] = "ACS Cutout Image"

        if 'special_columns' not in data.meta:
            data.meta['special_columns'] = {}

        data.meta['special_columns']['spectrum1d'] = 'spectrum1d'
        data.meta['special_columns']['spectrum2d'] = 'spectrum2d'
        data.meta['special_columns']['source_id'] = 'id'
        data.meta['special_columns']['cutout'] = 'cutout'
        data.meta['special_columns']['slit_ra'] = 'ra'
        data.meta['special_columns']['slit_dec'] = 'dec'
        data.meta['special_columns']['slit_width'] = 'slit_width'
        data.meta['special_columns']['slit_length'] = 'slit_length'

        data.meta['loaders_confirmed'] = True
        #########################################################

        self._primary_data = data
        self._layer_view.data = data
        self._unpack_selection(data)
        return True

    def add_subset(self, subset):
        """
        Processes subset messages from the central communication hub.

        Parameters
        ----------
        subset :
            Subset object.
        """
        self._layer_view.refresh()
        index = self._layer_view.layer_combo.findData(subset)
        self._layer_view.layer_combo.setCurrentIndex(index)
        return True


    def _update_data(self, message):
        """
        Update data message.

        Parameters
        ----------
        message : :class:`glue.core.message.Message`
            Data message object.
        """
        self._layer_view.refresh()

    def _add_subset(self, message):
        """
        Add subset message.

        Parameters
        ----------
        message : :class:`glue.core.message.Message`
            Subset message object.
        """
        self._layer_view.refresh()

    def _update_subset(self, message):
        """
        Update subset message.

        Parameters
        ----------
        message : :class:`glue.core.message.Message`
            Update message object.
        """
        self._layer_view.refresh()
        self._unpack_selection(message.subset)

    def _remove_subset(self, message):
        """
        Remove subset message.

        Parameters
        ----------
        message : :class:`glue.core.message.Message`
            Subset message object.
        """
        self._layer_view.refresh()
        self._unpack_selection(message.subset.data)

    def _selection_changed(self):
        self._unpack_selection(self._layer_view.layer_combo.currentData())

    def _unpack_selection(self, data):
        """
        Interprets the :class:`glue.core.data.Data` object by decomposing the
        data elements, extracting relevant data, and recomposing a
        package-agnostic dictionary object containing the relevant data.

        Parameters
        ----------
        data : :class:`glue.core.data.Data`
            Glue data object to decompose.

        """

        mask = None

        if isinstance(data, Subset):
            try:
                mask = data.to_mask()
            except IncompatibleAttribute:
                return

            if not np.any(mask):
                return

            data = data.data
        self.mask = mask

        # Clear the table
        self.catalog = Table()
        self.catalog.meta = data.meta

        self.comments = False
        col_names = data.components
        for att in col_names:
            cid = data.id[att]
            component = data.get_component(cid)

            if component.categorical:
                comp_labels = component.labels[mask]

                if comp_labels.ndim > 1:
                    comp_labels = comp_labels[0]

                if str(att) in ["comments", "flag"]:
                    self.comments = True
                elif str(att) in ['level2', 'spectrum1d', 'spectrum2d', 'cutout']:
                    self.filepath = component._load_log.path
                    p = Path(self.filepath)
                    path = os.path.sep.join(p.parts[:-1])
                    self.catalog[str(att)] = [os.path.join(path, x)
                                              for x in comp_labels]
                else:
                    self.catalog[str(att)] = comp_labels
            else:
                comp_data = component.data[mask]

                if comp_data.ndim > 1:
                    comp_data = comp_data[0]

                self.catalog[str(att)] = comp_data

        if len(self.catalog) > 0:
            if not self.comments:
                self.comments = self._load_comments(data.label) #Returns bool
            else:
                self._data_collection_index(data.label)
                self._get_save_path()
            # Update gui elements
            self._update_navigation(select=0)

    def _update_navigation(self, select=0):
        """
        Updates the :class:`qtpy.QtWidgets.QComboBox` widget with the
        appropriate source `id`s from the MOS catalog.
        """

        if self.toolbar is None:
            return

        self.toolbar.source_select.blockSignals(True)

        self.toolbar.source_select.clear()
        if len(self.catalog) > 0 and self.catalog.meta["special_columns"]["source_id"] in self.catalog.colnames:
            self.toolbar.source_select.addItems(self.catalog[self.catalog.meta["special_columns"]["source_id"]][:])

        self.toolbar.source_select.setCurrentIndex(select)

        self.toolbar.source_select.blockSignals(False)

        self.toolbar.source_select.currentIndexChanged.emit(select)

    def _set_navigation(self, index):

        if len(self.catalog) < index:
            return

        if 0 <= index < self.toolbar.source_select.count():
            self.toolbar.source_select.setCurrentIndex(index)

        if index <= 0:
            self.toolbar.cycle_previous_action.setDisabled(True)
        else:
            self.toolbar.cycle_previous_action.setDisabled(False)

        if index >= self.toolbar.source_select.count() - 1:
            self.toolbar.cycle_next_action.setDisabled(True)
        else:
            self.toolbar.cycle_next_action.setDisabled(False)

    def _set_exposure_navigation(self, index):

        # For level 3-only data.
        if index == None:
            # for some unknown reason (related to layout
            # managers perhaps?), the combo box does not
            # disappear from screen even when forced to
            # hide. Next best solution is to disable it.
            self.toolbar.exposure_select.setEnabled(False)

            self.toolbar.exposure_next_action.setEnabled(False)
            self.toolbar.exposure_previous_action.setEnabled(False)
            return

        if index > self.toolbar.exposure_select.count():
            return

        if 0 <= index < self.toolbar.exposure_select.count():
            self.toolbar.exposure_select.setCurrentIndex(index)

        if index < 1:
            self.toolbar.exposure_previous_action.setEnabled(False)
        else:
            self.toolbar.exposure_previous_action.setEnabled(True)

        if index >= self.toolbar.exposure_select.count() - 1:
            self.toolbar.exposure_next_action.setEnabled(False)
        else:
            self.toolbar.exposure_next_action.setEnabled(True)

    def _open_in_specviz(self):
        if self._specviz_instance is None:
            # Store a reference to the currently opened data viewer. This means
            # new "open in specviz" events will be added to the current viewer
            # as opposed to opening a new viewer.
            self._specviz_instance = self.session.application.new_data_viewer(
                SpecvizDataViewer)

            # Clear the reference to ensure no qt dangling pointers
            def _clear_instance_reference():
                self._specviz_instance = None

            self._specviz_instance.window_closed.connect(
                _clear_instance_reference)

        # Create a new Spectrum1D object from the flux data attribute of
        # the incoming data
        spec = glue_data_to_spectrum1d(self._loaded_data['spectrum1d'], 'Flux')

        # Create a DataItem from the Spectrum1D object, which adds the data
        # to the internel specviz model
        data_item = self._specviz_instance.current_workspace.model.add_data(
            spec, 'Spectrum1D')
        self._specviz_instance.current_workspace.force_plot(data_item)

    def load_selection(self, row):
        """
        Processes a row in the MOS catalog by first loading the data set,
        updating the stored data components, and then rendering the data on
        the visible MOSViz viewer plots.

        Parameters
        ----------
        row : `astropy.table.Row`
            A row object representing a row in the MOS catalog. Each key
            should be a column name.
        """

        self.current_row = row

        # Get loaders
        loader_spectrum1d = SPECTRUM1D_LOADERS[self.catalog.meta["loaders"]["spectrum1d"]]
        loader_spectrum2d = SPECTRUM2D_LOADERS[self.catalog.meta["loaders"]["spectrum2d"]]
        loader_cutout = CUTOUT_LOADERS[self.catalog.meta["loaders"]["cutout"]]

        # Get column names
        colname_spectrum1d = self.catalog.meta["special_columns"]["spectrum1d"]
        colname_spectrum2d = self.catalog.meta["special_columns"]["spectrum2d"]
        colname_cutout = self.catalog.meta["special_columns"]["cutout"]

        level2_data = None
        if "level2" in self.catalog.meta["loaders"]:
            loader_level2 = LEVEL2_LOADERS[self.catalog.meta["loaders"]["level2"]]
            colname_level2 = self.catalog.meta["special_columns"]["level2"]

            level2_basename = os.path.basename(row[colname_level2])
            if level2_basename != "None":
                level2_data = loader_level2(row[colname_level2])

        spec1d_basename = os.path.basename(row[colname_spectrum1d])
        if spec1d_basename == "None":
            spec1d_data = None
        else:
            spec1d_data = loader_spectrum1d(row[colname_spectrum1d])

        spec2d_basename = os.path.basename(row[colname_spectrum2d])
        if spec2d_basename == "None":
            spec2d_data = None
        else:
            spec2d_data = loader_spectrum2d(row[colname_spectrum2d])

        image_basename = os.path.basename(row[colname_cutout])
        if image_basename == "None":
            image_data = None
        else:
            image_data = loader_cutout(row[colname_cutout])

        self._update_data_components(spec1d_data, key='spectrum1d')
        self._update_data_components(spec2d_data, key='spectrum2d')
        self._update_data_components(image_data, key='cutout')

        self.level2_data = level2_data
        self.spec2d_data = spec2d_data

        self.render_data(row, spec1d_data, spec2d_data, image_data, level2_data)

    def load_exposure(self, index):
        '''
        Loads the level 2 exposure into the 2D spectrum plot widget.

        It can also load back the level 3 spectrum.
        '''
        name = self.toolbar.exposure_select.currentText()
        if 'Level 3' in name:
            self.spectrum2d_widget.set_image(
                image = self.spec2d_data.get_component(self.spec2d_data.id['Flux']).data,
                interpolation = 'none',
                aspect = 'auto',
                extent = self.extent,
                origin='lower')
        else:
            if name in [component.label for component in self.level2_data.components]:
                self.spectrum2d_widget.set_image(
                    image = self.level2_data.get_component(self.level2_data.id[name]).data,
                    interpolation = 'none',
                    aspect = 'auto',
                    extent = self.extent, origin='lower')

    def _update_data_components(self, data, key):
        """
        Update the data components that act as containers for the displayed
        data in the MOSViz viewer. This obviates the need to keep creating new
        data components.
        Parameters
        ----------
        data : :class:`glue.core.data.Data`
            Data object to replace within the component.
        key : str
            References the particular data set type.
        """
        cur_data = self._loaded_data.get(key, None)

        if cur_data is not None and data is None:
            self._loaded_data[key] = None
            self.session.data_collection.remove(cur_data)
        elif cur_data is None and data is not None:
            self._loaded_data[key] = data
            self.session.data_collection.append(data)
        elif data is not None:
            cur_data.update_values_from_data(data)
        else:
            return

    def add_slit(self, row=None, width=None, length=None):
        if row is None:
            row = self.current_row

        wcs = self.cutout_wcs
        if wcs is None:
            raise Exception("Image viewer has no WCS information")

        ra = row[self.catalog.meta["special_columns"]["slit_ra"]]
        dec = row[self.catalog.meta["special_columns"]["slit_dec"]]

        if width is None:
            width = row[self.catalog.meta["special_columns"]["slit_width"]]
        if length is None:
            length = row[self.catalog.meta["special_columns"]["slit_length"]]

        self.slit_controller.add_rectangle_sky_slit(wcs, ra, dec, width, length)

    def render_data(self, row, spec1d_data=None, spec2d_data=None,
                    image_data=None, level2_data=None):
        """
        Render the updated data sets in the individual plot widgets within the
        MOSViz viewer.
        """
        self._check_unsaved_comments()

        self.image_viewer_hidden = image_data is None

        if spec1d_data is not None:
            # TODO: This should not be needed. Must explore why the core model
            # is out of sync with the proxy model.
            self.spectrum1d_widget.plot_widget.clear_plots()

            # Clear the specviz model of any rendered plot items
            self._specviz_viewer.model.clear()

            # Create a new Spectrum1D object from the flux data attribute of
            # the incoming data
            spec = glue_data_to_spectrum1d(spec1d_data, 'Flux')

            # Create a DataItem from the Spectrum1D object, which adds the data
            # to the internel specviz model
            data_item = self._specviz_viewer.model.add_data(spec, 'Spectrum1D')

            # Get the PlotDataItem rendered via the plot's proxy model and
            # ensure that it is visible in the plot
            plot_data_item = self.spectrum1d_widget.proxy_model.item_from_id(data_item.identifier)
            plot_data_item.visible = True
            plot_data_item.color = "#000000"

            # Explicitly let the plot widget know that data items have changed
            self.spectrum1d_widget.plot_widget.on_item_changed(data_item)

        if not self.image_viewer_hidden:
            if not self.image_widget.isVisible():
                self.image_widget.setVisible(True)
            wcs = image_data.coords.wcs
            self.cutout_wcs = wcs

            array = image_data.get_component(image_data.id['Flux']).data

            # Add the slit patch to the plot
            self.slit_controller.clear_slits()
            if "slit_width" in self.catalog.meta["special_columns"] and \
                    "slit_length" in self.catalog.meta["special_columns"] and \
                    wcs is not None:
                self.add_slit(row)
                self.image_widget.draw_slit()
            else:
                self.image_widget.reset_limits()

            self.image_widget.set_image(array, wcs=wcs, interpolation='none', origin='lower')

            self.image_widget.axes.set_xlabel("Spatial X")
            self.image_widget.axes.set_ylabel("Spatial Y")
            if self.slit_controller.has_slits:
                self.image_widget.set_slit_limits()

            self.image_widget._redraw()
        else:
            self.cutout_wcs = None

        # Plot the 2D spectrum data last because by then we can make sure that
        # we set up the extent of the image appropriately if the cutout and the
        # 1D spectrum are present so that the axes can be locked.

        # We are repurposing the spectrum 2d widget to handle the display of both
        # the level 3 and level 2 spectra.
        if spec2d_data is not None or level2_data is not None:
            self._load_spectrum2d_widget(spec2d_data, level2_data)
        else:
            self.spectrum2d_widget.no_data()

        # Clear the meta information widget
        # NOTE: this process is inefficient
        for i in range(self.meta_form_layout.count()):
            wid = self.meta_form_layout.itemAt(i).widget()
            label = self.meta_form_layout.labelForField(wid)

            if label is not None:
                label.deleteLater()

            wid.deleteLater()

        # Repopulate the form layout
        # NOTE: this process is inefficient
        for col in row.colnames:
            if col.lower() not in ["comments", "flag"]:
                line_edit = QLineEdit(str(row[col]),
                                      self.central_widget.meta_form_widget)
                line_edit.setReadOnly(True)

                self.meta_form_layout.addRow(col, line_edit)

        # Set up comment and flag input/display boxes
        if self.comments:
            if self.savepath is not None:
                if self.savepath == -1:
                    line_edit = QLineEdit(os.path.basename("Not Saving to File."),
                                      self.central_widget.meta_form_widget)
                    line_edit.setReadOnly(True)
                    self.meta_form_layout.addRow("Save File", line_edit)
                else:
                    line_edit = QLineEdit(os.path.basename(self.savepath),
                                      self.central_widget.meta_form_widget)
                    line_edit.setReadOnly(True)
                    self.meta_form_layout.addRow("Save File", line_edit)

            self.input_flag = QLineEdit(self.get_flag(),
                self.central_widget.meta_form_widget)
            self.input_flag.textChanged.connect(self._text_changed)
            self.input_flag.setStyleSheet("background-color: rgba(255, 255, 255);")
            self.meta_form_layout.addRow("Flag", self.input_flag)

            self.input_comments = QPlainTextEdit(self.get_comment(),
                self.central_widget.meta_form_widget)
            self.input_comments.textChanged.connect(self._text_changed)
            self.input_comments.setStyleSheet("background-color: rgba(255, 255, 255);")
            self.meta_form_layout.addRow("Comments", self.input_comments)

            self.input_save = QPushButton('Save',
                self.central_widget.meta_form_widget)
            self.input_save.clicked.connect(self.update_comments)
            self.input_save.setDefault(True)

            self.input_refresh = QPushButton('Reload',
                self.central_widget.meta_form_widget)
            self.input_refresh.clicked.connect(self.refresh_comments)

            self.meta_form_layout.addRow(self.input_save, self.input_refresh)

        if not self.isHidden() and self.image_viewer_hidden:
            self.image_widget.setVisible(False)

    def _load_spectrum2d_widget(self, spec2d_data, level2_data):

        if not spec2d_data:
            return

        xp2d = np.arange(spec2d_data.shape[1])
        yp2d = np.repeat(0, spec2d_data.shape[1])

        spectrum2d_disp, spectrum2d_offset = spec2d_data.coords.pixel2world(xp2d, yp2d)

        x_min = spectrum2d_disp.min()
        x_max = spectrum2d_disp.max()

        if self.slit_controller.has_slits and \
                        None not in self.slit_controller.y_bounds:
            y_min, y_max = self.slit_controller.y_bounds
        else:
            y_min = -0.5
            y_max = spec2d_data.shape[0] - 0.5

        self.extent = [x_min, x_max, y_min, y_max]

        # By default, displays the level 3 spectrum. The level 2
        # data is plotted elsewhere, driven by the exposure_select
        # combo box signals.
        self.spectrum2d_widget.set_image(
            image=spec2d_data.get_component(spec2d_data.id['Flux']).data,
            interpolation='none',
            aspect='auto',
            extent=self.extent,
            origin='lower')

        self.spectrum2d_widget.axes.set_xlabel("Wavelength")
        self.spectrum2d_widget.axes.set_ylabel("Spatial Y")
        self.spectrum2d_widget._redraw()

        # If the axis are linked between the 1d and 2d views, setting the data
        # often ignores the initial bounds and instead uses the bounds of the
        # 2d data until the 1d view is moved. Force the 2d viewer to honor
        # the 1d view bounds.
        self.spectrum1d_widget.plot_widget.getPlotItem().sigXRangeChanged.emit(
            None, self.spectrum1d_widget.plot_widget.viewRange()[0])

        # Populates the level 2 exposures combo box
        if level2_data:
            self.toolbar.exposure_select.clear()
            self.toolbar.exposure_select.addItems(['Level 3'])
            components = level2_data.main_components + level2_data.derived_components
            self.toolbar.exposure_select.addItems([component.label for component in components])
            self._set_exposure_navigation(0)
        else:
            self._set_exposure_navigation(None)

    @defer_draw
    def set_locked_axes(self, x=None, y=None):

        # Here we only change the setting if x or y are not None
        # since if set_locked_axes is called with eg. x=True, then
        # we shouldn't change the y setting.

        if x is not None:
            if x:  # Lock the x axis if x is True
                def on_x_range_changed(xlim):
                    self.spectrum2d_widget.axes.set_xlim(*xlim)
                    self.spectrum2d_widget._redraw()

                self.spectrum1d_widget.plot_widget.getPlotItem().sigXRangeChanged.connect(
                    lambda a, b: on_x_range_changed(b))

                # Call the slot to update the axis linking initially
                # FIXME: Currently, this does not work for some reason.
                on_x_range_changed(self.spectrum1d_widget.plot_widget.viewRange()[0])
            else:  # Unlock the x axis if x is False
                self.spectrum1d_widget.plot_widget.getPlotItem().sigXRangeChanged.disconnect()

        if y is not None:
            self.spectrum2d_image_share.sharey = y

        self.spectrum2d_widget._redraw()
        self.image_widget._redraw()

    def layer_view(self):
        return self._layer_view

    def _text_changed(self):
        if self.textChangedAt is None:
            i = self.toolbar.source_select.currentIndex()
            self.textChangedAt = self._index_hash(i)

    def _check_unsaved_comments(self):
        if self.textChangedAt is None:
            return #Nothing to be changed
        i = self.toolbar.source_select.currentIndex()
        i = self._index_hash(i)
        if self.textChangedAt == i:
            self.textChangedAt = None
            return #This is a refresh
        info = "Comments or flags changed but were not saved. Would you like to save them?"
        reply = QMessageBox.question(self, '', info, QMessageBox.Yes | QMessageBox.No)
        if reply == QMessageBox.Yes:
            self.update_comments(True)
        self.textChangedAt = None

    def _data_collection_index(self, label):
        idx = -1
        for i, l in enumerate(self.session.data_collection):
            if l.label == label:
                idx = i
                break
        if idx == -1:
            return -1
        self.data_idx = idx
        return idx

    def _index_hash(self, i):
        """Local selection index -> Table index"""
        if self.mask is not None:
            size = self.mask.size
            temp = np.arange(size)
            return temp[self.mask][i]
        else:
            return i

    def _id_to_index_hash(self, ID, l):
        """Object Name -> Table index"""
        for i, name in enumerate(l):
            if name == ID:
                return i
        return None

    def get_slit_dimensions_from_file(self):
        if self.catalog is None:
            return None
        if "slit_width" in self.catalog.meta["special_columns"] and \
                "slit_length" in self.catalog.meta["special_columns"]:
            width = self.current_row[self.catalog.meta["special_columns"]["slit_width"]]
            length = self.current_row[self.catalog.meta["special_columns"]["slit_length"]]
            return [length, width]
        return None

    def get_slit_units_from_file(self):
        # TODO: Update once units infrastructure is in place
        return ["arcsec", "arcsec"]

    def get_comment(self):
        idx = self.data_idx
        i = self.toolbar.source_select.currentIndex()
        i = self._index_hash(i)
        comp = self.session.data_collection[idx].get_component("comments")
        return comp.labels[i]

    def get_flag(self):
        idx = self.data_idx
        i = self.toolbar.source_select.currentIndex()
        i = self._index_hash(i)
        comp = self.session.data_collection[idx].get_component("flag")
        return comp.labels[i]

    def send_NumericalDataChangedMessage(self):
        idx = self.data_idx
        data = self.session.data_collection[idx]
        data.hub.broadcast(msg.NumericalDataChangedMessage(data, "comments"))

    def refresh_comments(self):
        self.input_flag.setText(self.get_flag())
        self.input_comments.setPlainText(self.get_comment())
        self.input_flag.setStyleSheet("background-color: rgba(255, 255, 255);")
        self.textChangedAt = None

    def _get_save_path(self):
        """
        Try to get save path from other MOSVizViewer instances
        """
        for v in self.session.application.viewers[0]:
            if isinstance(v, MOSVizViewer):
                if v.savepath is not None:
                    if v.data_idx == self.data_idx:
                        self.savepath = v.savepath
                        break

    def _setup_save_path(self):
        """
        Prompt the user for a file to save comments and flags into.
        """
        fail = True
        success = False
        info = "Where would you like to save comments and flags?"
        option = pick_item([0, 1],
            [os.path.basename(self.filepath), "New MOSViz Table file"],
            label=info,  title="Comment Setup")
        if option == 0:
            self.savepath = self.filepath
        elif option == 1:
            dirname = os.path.dirname(self.filepath)
            path = compat.getsavefilename(caption="New MOSViz Table File",
                basedir=dirname, filters="*.txt")[0]
            if path == "":
                return fail
            self.savepath = path
        else:
            return fail

        for v in self.session.application.viewers[0]:
            if isinstance(v, MOSVizViewer):
                if v.data_idx == self.data_idx:
                    v.savepath = self.savepath
        self._layer_view.refresh()
        return success

    def update_comments(self, pastSelection = False):
        """
        Process comment and flag changes and save to file.

        Parameters
        ----------
        pastSelection : bool
            True when updating past selections. Used when
            user forgets to save.
        """
        if self.input_flag.text() == "":
            self.input_flag.setStyleSheet("background-color: rgba(255, 0, 0);")
            return

        i = None
        try:
            i = int(self.input_flag.text())
        except ValueError:
            self.input_flag.setStyleSheet("background-color: rgba(255, 0, 0);")
            info = QMessageBox.information(self, "Status:", "Flag must be an int!")
            return
        self.input_flag.setStyleSheet("background-color: rgba(255, 255, 255);")

        idx = self.data_idx
        if pastSelection:
            i = self.textChangedAt
            self.textChangedAt = None
        else:
            i = self.toolbar.source_select.currentIndex()
            i = self._index_hash(i)
        data = self.session.data_collection[idx]

        comp = data.get_component("comments")
        comp.labels.flags.writeable = True
        comp.labels[i] = self.input_comments.toPlainText()

        comp = data.get_component("flag")
        comp.labels.flags.writeable = True
        comp.labels[i] = self.input_flag.text()

        self.send_NumericalDataChangedMessage()
        self.write_comments()

        self.textChangedAt = None

    def _load_comments(self, label):
        """
        Populate the comments and flag columns.
        Attempt to load comments from file.

        Parameters
        ----------
        label : str
            The label of the data in
            session.data_collection.
        """

        #Make sure its the right data
        #(beacuse subset data is masked)
        idx = self._data_collection_index(label)
        if idx == -1:
            return False
        data = self.session.data_collection[idx]

        #Fill in default comments:
        length = data.shape[0]
        new_comments = np.array(["" for i in range(length)], dtype=object)
        new_flags = np.array(["0" for i in range(length)], dtype=object)

        #Fill in any saved comments:
        meta = data.meta
        obj_names = data.get_component(self.catalog.meta["special_columns"]["source_id"]).labels

        if "MOSViz_comments" in meta.keys():
            try:
                comments = meta["MOSViz_comments"]
                for key in comments.keys():
                    index = self._id_to_index_hash(key, obj_names)
                    if index is not None:
                        line = comments[key]
                        new_comments[index] = line
            except Exception as e:
                print("MOSViz Comment Load Failed: ", e)

        if "MOSViz_flags" in meta.keys():
            try:
                flags = meta["MOSViz_flags"]
                for key in flags.keys():
                    index = self._id_to_index_hash(key, obj_names)
                    if index is not None:
                        line = flags[key]
                        new_flags[index] = line
            except Exception as e:
                print("MOSViz Flag Load Failed: ", e)

        #Send to DC
        data.add_component(CategoricalComponent(new_flags, "flag"), "flag")
        data.add_component(CategoricalComponent(new_comments, "comments"), "comments")
        return True

    def write_comments(self):
        """
        Setup save file. Write comments and flags to file
        """

        if self.savepath is None:
            fail = self._setup_save_path()
            if fail: return
        if self.savepath == -1:
            return #Do not save to file option

        idx = self.data_idx
        data = self.session.data_collection[idx]
        save_comments = data.get_component("comments").labels
        save_flag = data.get_component("flag").labels
        obj_names = data.get_component(self.catalog.meta["special_columns"]["source_id"]).labels

        fn = self.savepath
        folder = os.path.dirname(fn)

        t = astropy_table.data_to_astropy_table(data)

        #Check if load and save dir paths match
        temp = os.path.dirname(self.filepath)
        if not  os.path.samefile(folder, temp):
            t['spectrum1d'].flags.writeable = True
            t['spectrum2d'].flags.writeable = True
            t['cutout'].flags.writeable = True
            for i in range(len(t)):
                t['spectrum1d'][i] = os.path.abspath(t['spectrum1d'][i])
                t['spectrum2d'][i] = os.path.abspath(t['spectrum2d'][i])
                t['cutout'][i] = os.path.abspath(t['cutout'][i])
        try:
            t.remove_column("comments")
            t.remove_column("flag")

            keys = t.meta.keys()

            if "MOSViz_comments" in keys:
                t.meta.pop("MOSViz_comments")

            if "MOSViz_flags" in keys:
                t.meta.pop("MOSViz_flags")

            comments = OrderedDict()
            flags = OrderedDict()

            for i, line in enumerate(save_comments):
                if line != "":
                    line = line.replace("\n", " ")
                    key = str(obj_names[i])
                    comments[key] = line

            for i, line in enumerate(save_flag):
                if line != "0" and line != "":
                    line = comments.replace("\n", " ")
                    key = str(obj_names[i])
                    flags[key] = line

            if len(comments) > 0:
                t.meta["MOSViz_comments"] = comments
            if len(flags) > 0:
                t.meta["MOSViz_flags"] = flags

            t.write(fn, format="ascii.ecsv", overwrite=True)
        except Exception as e:
            print("Comment write failed:", e)

    def closeEvent(self, event):
        """
        Clean up the extraneous data components created when opening the
        MOSViz viewer by overriding the parent class's close event.
        """
        super(MOSVizViewer, self).closeEvent(event)

        for data in self._loaded_data.values():
            self.session.data_collection.remove(data)
Пример #16
0
class PyDMLogDisplay(QWidget, LogLevels):
    """
    Standard display for Log Output

    This widget handles instantating a ``GuiHandler`` and displaying log
    messages to a ``QPlainTextEdit``. The level of the log can be changed from
    inside the widget itself, allowing users to select from any of the
    ``.levels`` specified by the widget.

    Parameters
    ----------
    parent : QObject, optional

    logname : str
        Name of log to display in widget

    level : logging.Level
        Initial level of log display

    """
    Q_ENUMS(LogLevels)
    LogLevels = LogLevels
    terminator = '\n'
    default_format = '%(asctime)s %(message)s'
    default_level = logging.INFO

    def __init__(self, parent=None, logname=None, level=logging.NOTSET):
        QWidget.__init__(self, parent=parent)
        # Create Widgets
        self.label = QLabel('Minimum displayed log level: ', parent=self)
        self.combo = QComboBox(parent=self)
        self.text = QPlainTextEdit(parent=self)
        self.text.setReadOnly(True)
        self.clear_btn = QPushButton("Clear", parent=self)
        # Create layout
        layout = QVBoxLayout()
        level_control = QHBoxLayout()
        level_control.addWidget(self.label)
        level_control.addWidget(self.combo)
        layout.addLayout(level_control)
        layout.addWidget(self.text)
        layout.addWidget(self.clear_btn)
        self.setLayout(layout)
        # Allow QCombobox to control log level
        for log_level, value in LogLevels.as_dict().items():
            self.combo.addItem(log_level, value)
        self.combo.currentIndexChanged[str].connect(self.setLevel)
        # Allow QPushButton to clear log text
        self.clear_btn.clicked.connect(self.clear)
        # Create a handler with the default format
        self.handler = GuiHandler(level=level, parent=self)
        self.logFormat = self.default_format
        self.handler.message.connect(self.write)
        # Create logger. Either as a root or given logname
        self.log = None
        self.level = None
        self.logName = logname or ''
        self.logLevel = level
        self.destroyed.connect(functools.partial(logger_destroyed, self.log))

    def sizeHint(self):
        return QSize(400, 300)

    @Property(LogLevels)
    def logLevel(self):
        return self.level

    @logLevel.setter
    def logLevel(self, level):
        if level != self.level:
            self.level = level
            idx = self.combo.findData(level)
            self.combo.setCurrentIndex(idx)

    @Property(str)
    def logName(self):
        """Name of associated log"""
        return self.log.name

    @logName.setter
    def logName(self, name):
        # Disconnect prior log from handler
        if self.log:
            self.log.removeHandler(self.handler)
        # Reattach handler to new handler
        self.log = logging.getLogger(name)
        # Ensure that the log matches level of handler
        # only if the handler level is less than the log.
        if self.log.level < self.handler.level:
            self.log.setLevel(self.handler.level)
        # Attach preconfigured handler
        self.log.addHandler(self.handler)

    @Property(str)
    def logFormat(self):
        """Format for log messages"""
        return self.handler.formatter._fmt

    @logFormat.setter
    def logFormat(self, fmt):
        self.handler.setFormatter(logging.Formatter(fmt))

    @Slot(str)
    def write(self, message):
        """Write a message to the log display"""
        # We split the incoming message by new lines. In prior iterations of
        # this widget it was discovered that large blocks of text cause issues
        # at the Qt level.
        for msg in message.split(self.terminator):
            self.text.appendPlainText(msg)

    @Slot()
    def clear(self):
        """Clear the text area."""
        self.text.clear()

    @Slot(str)
    def setLevel(self, level):
        """Set the level of the contained logger"""
        # Get the level from the incoming string specification
        try:
            level = getattr(logging, level.upper())
        except AttributeError as exc:
            logger.exception("Invalid logging level specified %s",
                             level.upper())
        else:
            # Set the existing handler and logger to this level
            self.handler.setLevel(level)
            if self.log.level > self.handler.level or self.log.level == logging.NOTSET:
                self.log.setLevel(self.handler.level)
Пример #17
0
class ConfigurePresentationWindow(QWidget):
    def __init__(self, parent):
        super().__init__()
        self.parent = parent
        # set title
        self.setWindowTitle("Configure Presentation")
        # set variables
        self.setupVariables()
        # setup Hymn Lyrics
        from glob import glob
        from pathlib import Path
        self.books = sorted([
            Path(filename).stem
            for filename in glob(r"./marvelData/books/Hymn Lyrics*.book")
        ])
        if len(self.books) > 0:
            self.setMinimumHeight(550)
        # setup interface
        self.setupUI()

    def setupVariables(self):
        pass

    def setupUI(self):

        from functools import partial
        from qtpy.QtCore import Qt
        from qtpy.QtWidgets import QHBoxLayout, QFormLayout, QSlider, QPushButton, QPlainTextEdit, QCheckBox, QComboBox
        from qtpy.QtWidgets import QRadioButton, QWidget, QVBoxLayout, QListView, QSpacerItem, QSizePolicy

        layout = QHBoxLayout()

        layout1 = QFormLayout()

        self.fontsizeslider = QSlider(Qt.Horizontal)
        self.fontsizeslider.setMinimum(1)
        self.fontsizeslider.setMaximum(12)
        self.fontsizeslider.setTickInterval(2)
        self.fontsizeslider.setSingleStep(2)
        self.fontsizeslider.setValue(config.presentationFontSize / 0.5)
        self.fontsizeslider.setToolTip(str(config.presentationFontSize))
        self.fontsizeslider.valueChanged.connect(
            self.presentationFontSizeChanged)
        layout1.addRow("Font Size", self.fontsizeslider)

        self.changecolorbutton = QPushButton()
        buttonStyle = "QPushButton {0}background-color: {2}; color: {3};{1}".format(
            "{", "}", config.presentationColorOnDarkTheme if config.theme
            == "dark" else config.presentationColorOnLightTheme,
            "white" if config.theme == "dark" else "black")
        self.changecolorbutton.setStyleSheet(buttonStyle)
        self.changecolorbutton.setToolTip("Change Color")
        self.changecolorbutton.clicked.connect(self.changeColor)
        layout1.addRow("Font Color", self.changecolorbutton)

        self.marginslider = QSlider(Qt.Horizontal)
        self.marginslider.setMinimum(0)
        self.marginslider.setMaximum(200)
        self.marginslider.setTickInterval(50)
        self.marginslider.setSingleStep(50)
        self.marginslider.setValue(config.presentationMargin)
        self.marginslider.setToolTip(str(config.presentationMargin))
        self.marginslider.valueChanged.connect(self.presentationMarginChanged)
        layout1.addRow("Margin", self.marginslider)

        self.verticalpositionslider = QSlider(Qt.Horizontal)
        self.verticalpositionslider.setMinimum(10)
        self.verticalpositionslider.setMaximum(90)
        self.verticalpositionslider.setTickInterval(10)
        self.verticalpositionslider.setSingleStep(10)
        self.verticalpositionslider.setValue(
            config.presentationVerticalPosition)
        self.verticalpositionslider.setToolTip(
            str(config.presentationVerticalPosition))
        self.verticalpositionslider.valueChanged.connect(
            self.presentationVerticalPositionChanged)
        layout1.addRow("Vertical Position", self.verticalpositionslider)

        self.horizontalpositionslider = QSlider(Qt.Horizontal)
        self.horizontalpositionslider.setMinimum(10)
        self.horizontalpositionslider.setMaximum(90)
        self.horizontalpositionslider.setTickInterval(10)
        self.horizontalpositionslider.setSingleStep(10)
        self.horizontalpositionslider.setValue(
            config.presentationHorizontalPosition)
        self.horizontalpositionslider.setToolTip(
            str(config.presentationHorizontalPosition))
        self.horizontalpositionslider.valueChanged.connect(
            self.presentationHorizontalPositionChanged)
        layout1.addRow("Horizontal Position", self.horizontalpositionslider)

        self.showBibleSelection = QRadioButton()
        self.showBibleSelection.setChecked(True)
        self.showBibleSelection.clicked.connect(
            lambda: self.selectRadio("bible"))
        layout1.addRow("Bible", self.showBibleSelection)

        if len(self.books) > 0:
            self.showHymnsSelection = QRadioButton()
            self.showHymnsSelection.setChecked(False)
            self.showHymnsSelection.clicked.connect(
                lambda: self.selectRadio("hymns"))
            layout1.addRow("Hymns", self.showHymnsSelection)

        # Second column

        layout2 = QVBoxLayout()

        self.bibleWidget = QWidget()
        self.bibleLayout = QFormLayout()

        checkbox = QCheckBox()
        checkbox.setText("")
        checkbox.setChecked(config.presentationParser)
        checkbox.stateChanged.connect(self.presentationParserChanged)
        checkbox.setToolTip("Parse bible verse reference in the entered text")
        self.bibleLayout.addRow("Bible Reference", checkbox)

        versionCombo = QComboBox()
        self.bibleVersions = self.parent.textList
        versionCombo.addItems(self.bibleVersions)
        initialIndex = 0
        if config.mainText in self.bibleVersions:
            initialIndex = self.bibleVersions.index(config.mainText)
        versionCombo.setCurrentIndex(initialIndex)
        versionCombo.currentIndexChanged.connect(self.changeBibleVersion)
        self.bibleLayout.addRow("Bible Version", versionCombo)

        self.textEntry = QPlainTextEdit("John 3:16; Rm 5:8")
        self.bibleLayout.addRow(self.textEntry)

        button = QPushButton("Presentation")
        button.setToolTip("Go to Presentation")
        button.clicked.connect(self.goToPresentation)
        self.bibleLayout.addWidget(button)

        self.bibleLayout.addItem(
            QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding))

        self.bibleWidget.setLayout(self.bibleLayout)

        self.hymnWidget = QWidget()
        self.hymnLayout = QFormLayout()

        selected = 0
        book = "Hymn Lyrics - English"
        if book in self.books:
            selected = self.books.index(book)
        self.bookList = QComboBox()
        self.bookList.addItems(self.books)
        self.bookList.setCurrentIndex(selected)
        self.bookList.currentIndexChanged.connect(self.selectHymnBook)
        self.hymnLayout.addWidget(self.bookList)

        self.chapterlist = QListView()
        self.chapterlist.clicked.connect(self.selectHymn)
        # self.chapterlist.selectionModel().selectionChanged.connect(self.selectHymn)
        self.hymnLayout.addWidget(self.chapterlist)

        self.buttons = []
        for count in range(0, 10):
            hymnButton = QPushButton()
            hymnButton.setText(" ")
            hymnButton.setEnabled(False)
            hymnButton.clicked.connect(partial(self.selectParagraph, count))
            self.hymnLayout.addWidget(hymnButton)
            self.buttons.append(hymnButton)

        self.selectHymnBook(selected)

        self.hymnWidget.setLayout(self.hymnLayout)
        self.hymnWidget.hide()

        layout2.addWidget(self.bibleWidget)
        if len(self.books) > 0:
            layout2.addWidget(self.hymnWidget)

        layout.addLayout(layout1)
        layout.addLayout(layout2)
        self.setLayout(layout)

    def selectRadio(self, option):
        if option == "bible":
            self.bibleWidget.show()
            if len(self.books) > 0:
                self.hymnWidget.hide()
        elif option == "hymns":
            self.bibleWidget.hide()
            if len(self.books) > 0:
                self.hymnWidget.show()

    def selectHymnBook(self, option):
        from ToolsSqlite import Book
        from qtpy.QtCore import QStringListModel
        if len(self.books) > 0:
            self.hymnBook = self.books[option]
            self.hymns = sorted(Book(self.hymnBook).getTopicList())
            self.chapterModel = QStringListModel(self.hymns)
            self.chapterlist.setModel(self.chapterModel)

    def selectHymn(self, option):
        from ToolsSqlite import Book
        row = option.row()
        self.hymn = self.hymns[row]
        book = Book(self.hymnBook)
        sections = book.getParagraphSectionsByChapter(self.hymn)
        count = 0
        for button in self.buttons:
            if count < len(sections):
                section = sections[count]
                text = section.replace("<br>", "")[:30]
                button.setText(text)
                button.setEnabled(True)
            else:
                button.setText(" ")
                button.setEnabled(False)
            count += 1

    def selectParagraph(self, paragraph):
        command = "SCREENBOOK:::{0}:::{1}:::{2}".format(
            self.hymnBook, self.hymn, paragraph)
        self.parent.runTextCommand(command)

    def goToPresentation(self):
        command = "SCREEN:::{0}".format(self.textEntry.toPlainText())
        self.parent.runTextCommand(command)

    def changeColor(self):

        from qtpy.QtGui import QColor
        from qtpy.QtWidgets import QColorDialog

        color = QColorDialog.getColor(
            QColor(config.presentationColorOnDarkTheme if config.theme ==
                   "dark" else config.presentationColorOnLightTheme), self)
        if color.isValid():
            colorName = color.name()
            if config.theme == "dark":
                config.presentationColorOnDarkTheme = colorName
            else:
                config.presentationColorOnLightTheme = colorName
            buttonStyle = "QPushButton {0}background-color: {2}; color: {3};{1}".format(
                "{", "}", colorName,
                "white" if config.theme == "dark" else "black")
            self.changecolorbutton.setStyleSheet(buttonStyle)

    def presentationFontSizeChanged(self, value):
        config.presentationFontSize = value * 0.5
        self.fontsizeslider.setToolTip(str(config.presentationFontSize))

    def presentationMarginChanged(self, value):
        config.presentationMargin = value
        self.marginslider.setToolTip(str(config.presentationMargin))

    def presentationVerticalPositionChanged(self, value):
        config.presentationVerticalPosition = value
        self.verticalpositionslider.setToolTip(
            str(config.presentationVerticalPosition))

    def presentationHorizontalPositionChanged(self, value):
        config.presentationHorizontalPosition = value
        self.horizontalpositionslider.setValue(
            config.presentationHorizontalPosition)

    def presentationParserChanged(self):
        config.presentationParser = not config.presentationParser

    def changeBibleVersion(self, index):
        if __name__ == '__main__':
            config.mainText = self.bibleVersions[index]
        else:
            command = "TEXT:::{0}".format(self.bibleVersions[index])
            self.parent.runTextCommand(command)
Пример #18
0
class MOSVizViewer(DataViewer):

    LABEL = "MOSViz Viewer"
    window_closed = Signal()
    _toolbar_cls = MOSViewerToolbar
    tools = []
    subtools = []

    def __init__(self, session, parent=None):
        super(MOSVizViewer, self).__init__(session, parent=parent)

        self.slit_controller = SlitController(self)

        self.load_ui()

        # Define some data containers
        self.filepath = None
        self.savepath = None
        self.data_idx = None
        self.comments = False
        self.textChangedAt = None
        self.mask = None
        self.cutout_wcs = None
        self.level2_data = None
        self.spec2d_data = None

        self.catalog = None
        self.current_row = None
        self._specviz_instance = None
        self._loaded_data = {}
        self._primary_data = None
        self._layer_view = SimpleLayerWidget(parent=self)
        self._layer_view.layer_combo.currentIndexChanged.connect(
            self._selection_changed)
        self.resize(800, 600)

        self.image_viewer_hidden = False

    def load_ui(self):
        """
        Setup the MOSView viewer interface.
        """
        self.central_widget = QWidget(self)

        path = os.path.join(UI_DIR, 'mos_widget.ui')
        loadUi(path, self.central_widget)

        self.image_widget = DrawableImageWidget(
            slit_controller=self.slit_controller)
        self.spectrum2d_widget = Spectrum2DWidget()

        self._specviz_viewer = Workspace()
        self._specviz_viewer.add_plot_window()
        self.spectrum1d_widget = self._specviz_viewer.current_plot_window
        self.spectrum1d_widget.plot_widget.getPlotItem(
        ).layout.setContentsMargins(45, 0, 25, 0)

        # Set up helper for sharing axes. SharedAxisHelper defaults to no sharing
        # and we control the sharing later by setting .sharex and .sharey on the
        # helper
        self.spectrum2d_image_share = SharedAxisHelper(
            self.spectrum2d_widget._axes, self.image_widget._axes)

        # We only need to set the image widget to keep the same aspect ratio
        # since the two other viewers don't require square pixels, so the axes
        # should not change shape.
        self.image_widget._axes.set_adjustable('datalim')

        self.meta_form_layout = self.central_widget.meta_form_layout
        self.meta_form_layout.setFieldGrowthPolicy(
            self.meta_form_layout.ExpandingFieldsGrow)
        self.central_widget.left_vertical_splitter.insertWidget(
            0, self.image_widget)
        self.central_widget.right_vertical_splitter.addWidget(
            self.spectrum2d_widget)
        self.central_widget.right_vertical_splitter.addWidget(
            self.spectrum1d_widget.widget())

        # Set the splitter stretch factors
        self.central_widget.left_vertical_splitter.setStretchFactor(0, 1)
        self.central_widget.left_vertical_splitter.setStretchFactor(1, 8)

        self.central_widget.right_vertical_splitter.setStretchFactor(0, 1)
        self.central_widget.right_vertical_splitter.setStretchFactor(1, 2)

        self.central_widget.horizontal_splitter.setStretchFactor(0, 1)
        self.central_widget.horizontal_splitter.setStretchFactor(1, 2)

        # Keep the left and right splitters in sync otherwise the axes don't line up
        self.central_widget.left_vertical_splitter.splitterMoved.connect(
            self._left_splitter_moved)
        self.central_widget.right_vertical_splitter.splitterMoved.connect(
            self._right_splitter_moved)

        # Set the central widget
        self.setCentralWidget(self.central_widget)

        self.central_widget.show()
        # Define the options widget
        self._options_widget = OptionsWidget()

    def show(self, *args, **kwargs):
        super(MOSVizViewer, self).show(*args, **kwargs)
        # Trigger a sync between the splitters
        self._left_splitter_moved()

        if self.image_viewer_hidden:
            self.image_widget.hide()
        else:
            self.image_widget.show()

    @avoid_circular
    def _right_splitter_moved(self, *args, **kwargs):
        if self.image_widget.isHidden():
            return
        sizes = self.central_widget.right_vertical_splitter.sizes()
        if sizes == [0, 0]:
            sizes = [230, 230]
        self.central_widget.left_vertical_splitter.setSizes(sizes)

    @avoid_circular
    def _left_splitter_moved(self, *args, **kwargs):
        if self.image_widget.isHidden():
            return
        sizes = self.central_widget.left_vertical_splitter.sizes()
        if sizes == [0, 0]:
            sizes = [230, 230]
        self.central_widget.right_vertical_splitter.setSizes(sizes)

    def setup_connections(self):
        """
        Connects gui elements to event calls.
        """
        # Connect the selection event for the combo box to what's displayed
        self.toolbar.source_select.currentIndexChanged[int].connect(
            lambda ind: self.load_selection(self.catalog[ind]))

        self.toolbar.source_select.currentIndexChanged[int].connect(
            lambda ind: self._set_navigation(ind))

        # Connect the exposure selection event
        self.toolbar.exposure_select.currentIndexChanged[int].connect(
            lambda ind: self.load_exposure(ind))

        self.toolbar.exposure_select.currentIndexChanged[int].connect(
            lambda ind: self._set_exposure_navigation(ind))

        # Connect the specviz button
        if SpecvizDataViewer is not None:
            self.toolbar.open_specviz.triggered.connect(
                lambda: self._open_in_specviz())
        else:
            self.toolbar.open_specviz.setDisabled(True)

        # Connect slit previous and next buttons
        self.toolbar.cycle_next_action.triggered.connect(
            lambda: self._set_navigation(self.toolbar.source_select.
                                         currentIndex() + 1))
        self.toolbar.cycle_previous_action.triggered.connect(
            lambda: self._set_navigation(self.toolbar.source_select.
                                         currentIndex() - 1))

        # Connect exposure previous and next buttons
        self.toolbar.exposure_next_action.triggered.connect(
            lambda: self._set_exposure_navigation(self.toolbar.exposure_select.
                                                  currentIndex() + 1))
        self.toolbar.exposure_previous_action.triggered.connect(
            lambda: self._set_exposure_navigation(self.toolbar.exposure_select.
                                                  currentIndex() - 1))

        # Connect the toolbar axes setting actions
        self.toolbar.lock_x_action.triggered.connect(
            lambda state: self.set_locked_axes(x=state))

        self.toolbar.lock_y_action.triggered.connect(
            lambda state: self.set_locked_axes(y=state))

    def options_widget(self):
        return self._options_widget

    def initialize_toolbar(self):
        """
        Initialize the custom toolbar for the MOSViz viewer.
        """
        from glue.config import viewer_tool

        self.toolbar = self._toolbar_cls(self)

        for tool_id in self.tools:
            mode_cls = viewer_tool.members[tool_id]
            mode = mode_cls(self)
            self.toolbar.add_tool(mode)

        self.addToolBar(self.toolbar)

        self.setup_connections()

    def register_to_hub(self, hub):
        super(MOSVizViewer, self).register_to_hub(hub)

        def has_data_or_subset(x):
            if x.sender is self._primary_data:
                return True
            elif isinstance(x.sender,
                            Subset) and x.sender.data is self._primary_data:
                return True
            else:
                return False

        hub.subscribe(self,
                      msg.SubsetCreateMessage,
                      handler=self._add_subset,
                      filter=has_data_or_subset)

        hub.subscribe(self,
                      msg.SubsetUpdateMessage,
                      handler=self._update_subset,
                      filter=has_data_or_subset)

        hub.subscribe(self,
                      msg.SubsetDeleteMessage,
                      handler=self._remove_subset,
                      filter=has_data_or_subset)

        hub.subscribe(self,
                      msg.DataUpdateMessage,
                      handler=self._update_data,
                      filter=has_data_or_subset)

    def add_data(self, data):
        """
        Processes data message from the central communication hub.

        Parameters
        ----------
        data : :class:`glue.core.data.Data`
            Data object.
        """

        # Check whether the data is suitable for the MOSViz viewer - basically
        # we expect a table of 1D columns with at least three string and four
        # floating-point columns.

        if data.ndim != 1:
            QMessageBox.critical(self,
                                 "Error", "MOSViz viewer can only be used "
                                 "for data with 1-dimensional components",
                                 buttons=QMessageBox.Ok)
            return False

        components = [data.get_component(cid) for cid in data.main_components]

        categorical = [c for c in components if c.categorical]
        if len(categorical) < 3:
            QMessageBox.critical(
                self,
                "Error", "MOSViz viewer expected at least "
                "three string components/columns, representing "
                "the filenames of the 1D and 2D spectra and "
                "cutouts",
                buttons=QMessageBox.Ok)
            return False

        # We can relax the following requirement if we make the slit parameters
        # optional
        numerical = [c for c in components if c.numeric]
        if len(numerical) < 4:
            QMessageBox.critical(
                self,
                "Error", "MOSViz viewer expected at least "
                "four numerical components/columns, representing "
                "the slit position, length, and position angle",
                buttons=QMessageBox.Ok)
            return False

        # Make sure the loaders and column names are correct
        result = confirm_loaders_and_column_names(data)
        if not result:
            return False

        self._primary_data = data
        self._layer_view.data = data
        self._unpack_selection(data)
        return True

    def add_data_for_testing(self, data):
        """
        Processes data message from the central communication hub.

        Parameters
        ----------
        data : :class:`glue.core.data.Data`
            Data object.
        """

        # Check whether the data is suitable for the MOSViz viewer - basically
        # we expect a table of 1D columns with at least three string and four
        # floating-point columns.
        if data.ndim != 1:
            QMessageBox.critical(self,
                                 "Error", "MOSViz viewer can only be used "
                                 "for data with 1-dimensional components",
                                 buttons=QMessageBox.Ok)
            return False

        components = [data.get_component(cid) for cid in data.main_components]
        categorical = [c for c in components if c.categorical]
        if len(categorical) < 3:
            QMessageBox.critical(
                self,
                "Error", "MOSViz viewer expected at least "
                "three string components/columns, representing "
                "the filenames of the 1D and 2D spectra and "
                "cutouts",
                buttons=QMessageBox.Ok)
            return False

        # We can relax the following requirement if we make the slit parameters
        # optional
        numerical = [c for c in components if c.numeric]
        if len(numerical) < 4:
            QMessageBox.critical(
                self,
                "Error", "MOSViz viewer expected at least "
                "four numerical components/columns, representing "
                "the slit position, length, and position angle",
                buttons=QMessageBox.Ok)
            return False

        # Block of code to bypass the loader_selection gui
        #########################################################
        if 'loaders' not in data.meta:
            data.meta['loaders'] = {}

        # Deimos data
        data.meta['loaders']['spectrum1d'] = "DEIMOS 1D Spectrum"
        data.meta['loaders']['spectrum2d'] = "DEIMOS 2D Spectrum"
        data.meta['loaders']['cutout'] = "ACS Cutout Image"

        if 'special_columns' not in data.meta:
            data.meta['special_columns'] = {}

        data.meta['special_columns']['spectrum1d'] = 'spectrum1d'
        data.meta['special_columns']['spectrum2d'] = 'spectrum2d'
        data.meta['special_columns']['source_id'] = 'id'
        data.meta['special_columns']['cutout'] = 'cutout'
        data.meta['special_columns']['slit_ra'] = 'ra'
        data.meta['special_columns']['slit_dec'] = 'dec'
        data.meta['special_columns']['slit_width'] = 'slit_width'
        data.meta['special_columns']['slit_length'] = 'slit_length'

        data.meta['loaders_confirmed'] = True
        #########################################################

        self._primary_data = data
        self._layer_view.data = data
        self._unpack_selection(data)
        return True

    def add_subset(self, subset):
        """
        Processes subset messages from the central communication hub.

        Parameters
        ----------
        subset :
            Subset object.
        """
        self._layer_view.refresh()
        index = self._layer_view.layer_combo.findData(subset)
        self._layer_view.layer_combo.setCurrentIndex(index)
        return True

    def _update_data(self, message):
        """
        Update data message.

        Parameters
        ----------
        message : :class:`glue.core.message.Message`
            Data message object.
        """
        self._layer_view.refresh()

    def _add_subset(self, message):
        """
        Add subset message.

        Parameters
        ----------
        message : :class:`glue.core.message.Message`
            Subset message object.
        """
        self._layer_view.refresh()

    def _update_subset(self, message):
        """
        Update subset message.

        Parameters
        ----------
        message : :class:`glue.core.message.Message`
            Update message object.
        """
        self._layer_view.refresh()
        self._unpack_selection(message.subset)

    def _remove_subset(self, message):
        """
        Remove subset message.

        Parameters
        ----------
        message : :class:`glue.core.message.Message`
            Subset message object.
        """
        self._layer_view.refresh()
        self._unpack_selection(message.subset.data)

    def _selection_changed(self):
        self._unpack_selection(self._layer_view.layer_combo.currentData())

    def _unpack_selection(self, data):
        """
        Interprets the :class:`glue.core.data.Data` object by decomposing the
        data elements, extracting relevant data, and recomposing a
        package-agnostic dictionary object containing the relevant data.

        Parameters
        ----------
        data : :class:`glue.core.data.Data`
            Glue data object to decompose.

        """

        mask = None

        if isinstance(data, Subset):
            try:
                mask = data.to_mask()
            except IncompatibleAttribute:
                return

            if not np.any(mask):
                return

            data = data.data
        self.mask = mask

        # Clear the table
        self.catalog = Table()
        self.catalog.meta = data.meta

        self.comments = False
        col_names = data.components
        for att in col_names:
            cid = data.id[att]
            component = data.get_component(cid)

            if component.categorical:
                comp_labels = component.labels[mask]

                if comp_labels.ndim > 1:
                    comp_labels = comp_labels[0]

                if str(att) in ["comments", "flag"]:
                    self.comments = True
                elif str(att) in [
                        'level2', 'spectrum1d', 'spectrum2d', 'cutout'
                ]:
                    self.filepath = component._load_log.path
                    p = Path(self.filepath)
                    path = os.path.sep.join(p.parts[:-1])
                    self.catalog[str(att)] = [
                        os.path.join(path, x) for x in comp_labels
                    ]
                else:
                    self.catalog[str(att)] = comp_labels
            else:
                comp_data = component.data[mask]

                if comp_data.ndim > 1:
                    comp_data = comp_data[0]

                self.catalog[str(att)] = comp_data

        if len(self.catalog) > 0:
            if not self.comments:
                self.comments = self._load_comments(data.label)  #Returns bool
            else:
                self._data_collection_index(data.label)
                self._get_save_path()
            # Update gui elements
            self._update_navigation(select=0)

    def _update_navigation(self, select=0):
        """
        Updates the :class:`qtpy.QtWidgets.QComboBox` widget with the
        appropriate source `id`s from the MOS catalog.
        """

        if self.toolbar is None:
            return

        self.toolbar.source_select.blockSignals(True)

        self.toolbar.source_select.clear()
        if len(self.catalog) > 0 and self.catalog.meta["special_columns"][
                "source_id"] in self.catalog.colnames:
            self.toolbar.source_select.addItems(self.catalog[
                self.catalog.meta["special_columns"]["source_id"]][:])

        self.toolbar.source_select.setCurrentIndex(select)

        self.toolbar.source_select.blockSignals(False)

        self.toolbar.source_select.currentIndexChanged.emit(select)

    def _set_navigation(self, index):

        if len(self.catalog) < index:
            return

        if 0 <= index < self.toolbar.source_select.count():
            self.toolbar.source_select.setCurrentIndex(index)

        if index <= 0:
            self.toolbar.cycle_previous_action.setDisabled(True)
        else:
            self.toolbar.cycle_previous_action.setDisabled(False)

        if index >= self.toolbar.source_select.count() - 1:
            self.toolbar.cycle_next_action.setDisabled(True)
        else:
            self.toolbar.cycle_next_action.setDisabled(False)

    def _set_exposure_navigation(self, index):

        # For level 3-only data.
        if index == None:
            # for some unknown reason (related to layout
            # managers perhaps?), the combo box does not
            # disappear from screen even when forced to
            # hide. Next best solution is to disable it.
            self.toolbar.exposure_select.setEnabled(False)

            self.toolbar.exposure_next_action.setEnabled(False)
            self.toolbar.exposure_previous_action.setEnabled(False)
            return

        if index > self.toolbar.exposure_select.count():
            return

        if 0 <= index < self.toolbar.exposure_select.count():
            self.toolbar.exposure_select.setCurrentIndex(index)

        if index < 1:
            self.toolbar.exposure_previous_action.setEnabled(False)
        else:
            self.toolbar.exposure_previous_action.setEnabled(True)

        if index >= self.toolbar.exposure_select.count() - 1:
            self.toolbar.exposure_next_action.setEnabled(False)
        else:
            self.toolbar.exposure_next_action.setEnabled(True)

    def _open_in_specviz(self):
        if self._specviz_instance is None:
            # Store a reference to the currently opened data viewer. This means
            # new "open in specviz" events will be added to the current viewer
            # as opposed to opening a new viewer.
            self._specviz_instance = self.session.application.new_data_viewer(
                SpecvizDataViewer)

            # Clear the reference to ensure no qt dangling pointers
            def _clear_instance_reference():
                self._specviz_instance = None

            self._specviz_instance.window_closed.connect(
                _clear_instance_reference)

        # Create a new Spectrum1D object from the flux data attribute of
        # the incoming data
        spec = glue_data_to_spectrum1d(self._loaded_data['spectrum1d'], 'Flux')

        # Create a DataItem from the Spectrum1D object, which adds the data
        # to the internel specviz model
        data_item = self._specviz_instance.current_workspace.model.add_data(
            spec, 'Spectrum1D')
        self._specviz_instance.current_workspace.force_plot(data_item)

    def load_selection(self, row):
        """
        Processes a row in the MOS catalog by first loading the data set,
        updating the stored data components, and then rendering the data on
        the visible MOSViz viewer plots.

        Parameters
        ----------
        row : `astropy.table.Row`
            A row object representing a row in the MOS catalog. Each key
            should be a column name.
        """

        self.current_row = row

        # Get loaders
        loader_spectrum1d = SPECTRUM1D_LOADERS[self.catalog.meta["loaders"]
                                               ["spectrum1d"]]
        loader_spectrum2d = SPECTRUM2D_LOADERS[self.catalog.meta["loaders"]
                                               ["spectrum2d"]]
        loader_cutout = CUTOUT_LOADERS[self.catalog.meta["loaders"]["cutout"]]

        # Get column names
        colname_spectrum1d = self.catalog.meta["special_columns"]["spectrum1d"]
        colname_spectrum2d = self.catalog.meta["special_columns"]["spectrum2d"]
        colname_cutout = self.catalog.meta["special_columns"]["cutout"]

        level2_data = None
        if "level2" in self.catalog.meta["loaders"]:
            loader_level2 = LEVEL2_LOADERS[self.catalog.meta["loaders"]
                                           ["level2"]]
            colname_level2 = self.catalog.meta["special_columns"]["level2"]

            level2_basename = os.path.basename(row[colname_level2])
            if level2_basename != "None":
                level2_data = loader_level2(row[colname_level2])

        spec1d_basename = os.path.basename(row[colname_spectrum1d])
        if spec1d_basename == "None":
            spec1d_data = None
        else:
            spec1d_data = loader_spectrum1d(row[colname_spectrum1d])

        spec2d_basename = os.path.basename(row[colname_spectrum2d])
        if spec2d_basename == "None":
            spec2d_data = None
        else:
            spec2d_data = loader_spectrum2d(row[colname_spectrum2d])

        image_basename = os.path.basename(row[colname_cutout])
        if image_basename == "None":
            image_data = None
        else:
            image_data = loader_cutout(row[colname_cutout])

        self._update_data_components(spec1d_data, key='spectrum1d')
        self._update_data_components(spec2d_data, key='spectrum2d')
        self._update_data_components(image_data, key='cutout')

        self.level2_data = level2_data
        self.spec2d_data = spec2d_data

        self.render_data(row, spec1d_data, spec2d_data, image_data,
                         level2_data)

    def load_exposure(self, index):
        '''
        Loads the level 2 exposure into the 2D spectrum plot widget.

        It can also load back the level 3 spectrum.
        '''
        name = self.toolbar.exposure_select.currentText()
        if 'Level 3' in name:
            self.spectrum2d_widget.set_image(
                image=self.spec2d_data.get_component(
                    self.spec2d_data.id['Flux']).data,
                interpolation='none',
                aspect='auto',
                extent=self.extent,
                origin='lower')
        else:
            if name in [
                    component.label
                    for component in self.level2_data.components
            ]:
                self.spectrum2d_widget.set_image(
                    image=self.level2_data.get_component(
                        self.level2_data.id[name]).data,
                    interpolation='none',
                    aspect='auto',
                    extent=self.extent,
                    origin='lower')

    def _update_data_components(self, data, key):
        """
        Update the data components that act as containers for the displayed
        data in the MOSViz viewer. This obviates the need to keep creating new
        data components.
        Parameters
        ----------
        data : :class:`glue.core.data.Data`
            Data object to replace within the component.
        key : str
            References the particular data set type.
        """
        cur_data = self._loaded_data.get(key, None)

        if cur_data is not None and data is None:
            self._loaded_data[key] = None
            self.session.data_collection.remove(cur_data)
        elif cur_data is None and data is not None:
            self._loaded_data[key] = data
            self.session.data_collection.append(data)
        elif data is not None:
            cur_data.update_values_from_data(data)
        else:
            return

    def add_slit(self, row=None, width=None, length=None):
        if row is None:
            row = self.current_row

        wcs = self.cutout_wcs
        if wcs is None:
            raise Exception("Image viewer has no WCS information")

        ra = row[self.catalog.meta["special_columns"]["slit_ra"]]
        dec = row[self.catalog.meta["special_columns"]["slit_dec"]]

        if width is None:
            width = row[self.catalog.meta["special_columns"]["slit_width"]]
        if length is None:
            length = row[self.catalog.meta["special_columns"]["slit_length"]]

        self.slit_controller.add_rectangle_sky_slit(wcs, ra, dec, width,
                                                    length)

    def render_data(self,
                    row,
                    spec1d_data=None,
                    spec2d_data=None,
                    image_data=None,
                    level2_data=None):
        """
        Render the updated data sets in the individual plot widgets within the
        MOSViz viewer.
        """
        self._check_unsaved_comments()

        self.image_viewer_hidden = image_data is None

        if spec1d_data is not None:
            # TODO: This should not be needed. Must explore why the core model
            # is out of sync with the proxy model.
            self.spectrum1d_widget.plot_widget.clear_plots()

            # Clear the specviz model of any rendered plot items
            self._specviz_viewer.model.clear()

            # Create a new Spectrum1D object from the flux data attribute of
            # the incoming data
            spec = glue_data_to_spectrum1d(spec1d_data, 'Flux')

            # Create a DataItem from the Spectrum1D object, which adds the data
            # to the internel specviz model
            data_item = self._specviz_viewer.model.add_data(spec, 'Spectrum1D')

            # Get the PlotDataItem rendered via the plot's proxy model and
            # ensure that it is visible in the plot
            plot_data_item = self.spectrum1d_widget.proxy_model.item_from_id(
                data_item.identifier)
            plot_data_item.visible = True
            plot_data_item.color = "#000000"

            # Explicitly let the plot widget know that data items have changed
            self.spectrum1d_widget.plot_widget.on_item_changed(data_item)

        if not self.image_viewer_hidden:
            if not self.image_widget.isVisible():
                self.image_widget.setVisible(True)
            wcs = image_data.coords.wcs
            self.cutout_wcs = wcs

            array = image_data.get_component(image_data.id['Flux']).data

            # Add the slit patch to the plot
            self.slit_controller.clear_slits()
            if "slit_width" in self.catalog.meta["special_columns"] and \
                    "slit_length" in self.catalog.meta["special_columns"] and \
                    wcs is not None:
                self.add_slit(row)
                self.image_widget.draw_slit()
            else:
                self.image_widget.reset_limits()

            self.image_widget.set_image(array,
                                        wcs=wcs,
                                        interpolation='none',
                                        origin='lower')

            self.image_widget.axes.set_xlabel("Spatial X")
            self.image_widget.axes.set_ylabel("Spatial Y")
            if self.slit_controller.has_slits:
                self.image_widget.set_slit_limits()

            self.image_widget._redraw()
        else:
            self.cutout_wcs = None

        # Plot the 2D spectrum data last because by then we can make sure that
        # we set up the extent of the image appropriately if the cutout and the
        # 1D spectrum are present so that the axes can be locked.

        # We are repurposing the spectrum 2d widget to handle the display of both
        # the level 3 and level 2 spectra.
        if spec2d_data is not None or level2_data is not None:
            self._load_spectrum2d_widget(spec2d_data, level2_data)
        else:
            self.spectrum2d_widget.no_data()

        # Clear the meta information widget
        # NOTE: this process is inefficient
        for i in range(self.meta_form_layout.count()):
            wid = self.meta_form_layout.itemAt(i).widget()
            label = self.meta_form_layout.labelForField(wid)

            if label is not None:
                label.deleteLater()

            wid.deleteLater()

        # Repopulate the form layout
        # NOTE: this process is inefficient
        for col in row.colnames:
            if col.lower() not in ["comments", "flag"]:
                line_edit = QLineEdit(str(row[col]),
                                      self.central_widget.meta_form_widget)
                line_edit.setReadOnly(True)

                self.meta_form_layout.addRow(col, line_edit)

        # Set up comment and flag input/display boxes
        if self.comments:
            if self.savepath is not None:
                if self.savepath == -1:
                    line_edit = QLineEdit(
                        os.path.basename("Not Saving to File."),
                        self.central_widget.meta_form_widget)
                    line_edit.setReadOnly(True)
                    self.meta_form_layout.addRow("Save File", line_edit)
                else:
                    line_edit = QLineEdit(os.path.basename(self.savepath),
                                          self.central_widget.meta_form_widget)
                    line_edit.setReadOnly(True)
                    self.meta_form_layout.addRow("Save File", line_edit)

            self.input_flag = QLineEdit(self.get_flag(),
                                        self.central_widget.meta_form_widget)
            self.input_flag.textChanged.connect(self._text_changed)
            self.input_flag.setStyleSheet(
                "background-color: rgba(255, 255, 255);")
            self.meta_form_layout.addRow("Flag", self.input_flag)

            self.input_comments = QPlainTextEdit(
                self.get_comment(), self.central_widget.meta_form_widget)
            self.input_comments.textChanged.connect(self._text_changed)
            self.input_comments.setStyleSheet(
                "background-color: rgba(255, 255, 255);")
            self.meta_form_layout.addRow("Comments", self.input_comments)

            self.input_save = QPushButton('Save',
                                          self.central_widget.meta_form_widget)
            self.input_save.clicked.connect(self.update_comments)
            self.input_save.setDefault(True)

            self.input_refresh = QPushButton(
                'Reload', self.central_widget.meta_form_widget)
            self.input_refresh.clicked.connect(self.refresh_comments)

            self.meta_form_layout.addRow(self.input_save, self.input_refresh)

        if not self.isHidden() and self.image_viewer_hidden:
            self.image_widget.setVisible(False)

    def _load_spectrum2d_widget(self, spec2d_data, level2_data):

        if not spec2d_data:
            return

        xp2d = np.arange(spec2d_data.shape[1])
        yp2d = np.repeat(0, spec2d_data.shape[1])

        spectrum2d_disp, spectrum2d_offset = spec2d_data.coords.pixel2world(
            xp2d, yp2d)

        x_min = spectrum2d_disp.min()
        x_max = spectrum2d_disp.max()

        if self.slit_controller.has_slits and \
                        None not in self.slit_controller.y_bounds:
            y_min, y_max = self.slit_controller.y_bounds
        else:
            y_min = -0.5
            y_max = spec2d_data.shape[0] - 0.5

        self.extent = [x_min, x_max, y_min, y_max]

        # By default, displays the level 3 spectrum. The level 2
        # data is plotted elsewhere, driven by the exposure_select
        # combo box signals.
        self.spectrum2d_widget.set_image(image=spec2d_data.get_component(
            spec2d_data.id['Flux']).data,
                                         interpolation='none',
                                         aspect='auto',
                                         extent=self.extent,
                                         origin='lower')

        self.spectrum2d_widget.axes.set_xlabel("Wavelength")
        self.spectrum2d_widget.axes.set_ylabel("Spatial Y")
        self.spectrum2d_widget._redraw()

        # If the axis are linked between the 1d and 2d views, setting the data
        # often ignores the initial bounds and instead uses the bounds of the
        # 2d data until the 1d view is moved. Force the 2d viewer to honor
        # the 1d view bounds.
        self.spectrum1d_widget.plot_widget.getPlotItem().sigXRangeChanged.emit(
            None,
            self.spectrum1d_widget.plot_widget.viewRange()[0])

        # Populates the level 2 exposures combo box
        if level2_data:
            self.toolbar.exposure_select.clear()
            self.toolbar.exposure_select.addItems(['Level 3'])
            components = level2_data.main_components + level2_data.derived_components
            self.toolbar.exposure_select.addItems(
                [component.label for component in components])
            self._set_exposure_navigation(0)
        else:
            self._set_exposure_navigation(None)

    @defer_draw
    def set_locked_axes(self, x=None, y=None):

        # Here we only change the setting if x or y are not None
        # since if set_locked_axes is called with eg. x=True, then
        # we shouldn't change the y setting.

        if x is not None:
            if x:  # Lock the x axis if x is True

                def on_x_range_changed(xlim):
                    self.spectrum2d_widget.axes.set_xlim(*xlim)
                    self.spectrum2d_widget._redraw()

                self.spectrum1d_widget.plot_widget.getPlotItem(
                ).sigXRangeChanged.connect(lambda a, b: on_x_range_changed(b))

                # Call the slot to update the axis linking initially
                # FIXME: Currently, this does not work for some reason.
                on_x_range_changed(
                    self.spectrum1d_widget.plot_widget.viewRange()[0])
            else:  # Unlock the x axis if x is False
                self.spectrum1d_widget.plot_widget.getPlotItem(
                ).sigXRangeChanged.disconnect()

        if y is not None:
            self.spectrum2d_image_share.sharey = y

        self.spectrum2d_widget._redraw()
        self.image_widget._redraw()

    def layer_view(self):
        return self._layer_view

    def _text_changed(self):
        if self.textChangedAt is None:
            i = self.toolbar.source_select.currentIndex()
            self.textChangedAt = self._index_hash(i)

    def _check_unsaved_comments(self):
        if self.textChangedAt is None:
            return  #Nothing to be changed
        i = self.toolbar.source_select.currentIndex()
        i = self._index_hash(i)
        if self.textChangedAt == i:
            self.textChangedAt = None
            return  #This is a refresh
        info = "Comments or flags changed but were not saved. Would you like to save them?"
        reply = QMessageBox.question(self, '', info,
                                     QMessageBox.Yes | QMessageBox.No)
        if reply == QMessageBox.Yes:
            self.update_comments(True)
        self.textChangedAt = None

    def _data_collection_index(self, label):
        idx = -1
        for i, l in enumerate(self.session.data_collection):
            if l.label == label:
                idx = i
                break
        if idx == -1:
            return -1
        self.data_idx = idx
        return idx

    def _index_hash(self, i):
        """Local selection index -> Table index"""
        if self.mask is not None:
            size = self.mask.size
            temp = np.arange(size)
            return temp[self.mask][i]
        else:
            return i

    def _id_to_index_hash(self, ID, l):
        """Object Name -> Table index"""
        for i, name in enumerate(l):
            if name == ID:
                return i
        return None

    def get_slit_dimensions_from_file(self):
        if self.catalog is None:
            return None
        if "slit_width" in self.catalog.meta["special_columns"] and \
                "slit_length" in self.catalog.meta["special_columns"]:
            width = self.current_row[self.catalog.meta["special_columns"]
                                     ["slit_width"]]
            length = self.current_row[self.catalog.meta["special_columns"]
                                      ["slit_length"]]
            return [length, width]
        return None

    def get_slit_units_from_file(self):
        # TODO: Update once units infrastructure is in place
        return ["arcsec", "arcsec"]

    def get_comment(self):
        idx = self.data_idx
        i = self.toolbar.source_select.currentIndex()
        i = self._index_hash(i)
        comp = self.session.data_collection[idx].get_component("comments")
        return comp.labels[i]

    def get_flag(self):
        idx = self.data_idx
        i = self.toolbar.source_select.currentIndex()
        i = self._index_hash(i)
        comp = self.session.data_collection[idx].get_component("flag")
        return comp.labels[i]

    def send_NumericalDataChangedMessage(self):
        idx = self.data_idx
        data = self.session.data_collection[idx]
        data.hub.broadcast(msg.NumericalDataChangedMessage(data, "comments"))

    def refresh_comments(self):
        self.input_flag.setText(self.get_flag())
        self.input_comments.setPlainText(self.get_comment())
        self.input_flag.setStyleSheet("background-color: rgba(255, 255, 255);")
        self.textChangedAt = None

    def _get_save_path(self):
        """
        Try to get save path from other MOSVizViewer instances
        """
        for v in self.session.application.viewers[0]:
            if isinstance(v, MOSVizViewer):
                if v.savepath is not None:
                    if v.data_idx == self.data_idx:
                        self.savepath = v.savepath
                        break

    def _setup_save_path(self):
        """
        Prompt the user for a file to save comments and flags into.
        """
        fail = True
        success = False
        info = "Where would you like to save comments and flags?"
        option = pick_item(
            [0, 1], [os.path.basename(self.filepath), "New MOSViz Table file"],
            label=info,
            title="Comment Setup")
        if option == 0:
            self.savepath = self.filepath
        elif option == 1:
            dirname = os.path.dirname(self.filepath)
            path = compat.getsavefilename(caption="New MOSViz Table File",
                                          basedir=dirname,
                                          filters="*.txt")[0]
            if path == "":
                return fail
            self.savepath = path
        else:
            return fail

        for v in self.session.application.viewers[0]:
            if isinstance(v, MOSVizViewer):
                if v.data_idx == self.data_idx:
                    v.savepath = self.savepath
        self._layer_view.refresh()
        return success

    def update_comments(self, pastSelection=False):
        """
        Process comment and flag changes and save to file.

        Parameters
        ----------
        pastSelection : bool
            True when updating past selections. Used when
            user forgets to save.
        """
        if self.input_flag.text() == "":
            self.input_flag.setStyleSheet("background-color: rgba(255, 0, 0);")
            return

        i = None
        try:
            i = int(self.input_flag.text())
        except ValueError:
            self.input_flag.setStyleSheet("background-color: rgba(255, 0, 0);")
            info = QMessageBox.information(self, "Status:",
                                           "Flag must be an int!")
            return
        self.input_flag.setStyleSheet("background-color: rgba(255, 255, 255);")

        idx = self.data_idx
        if pastSelection:
            i = self.textChangedAt
            self.textChangedAt = None
        else:
            i = self.toolbar.source_select.currentIndex()
            i = self._index_hash(i)
        data = self.session.data_collection[idx]

        comp = data.get_component("comments")
        comp.labels.flags.writeable = True
        comp.labels[i] = self.input_comments.toPlainText()

        comp = data.get_component("flag")
        comp.labels.flags.writeable = True
        comp.labels[i] = self.input_flag.text()

        self.send_NumericalDataChangedMessage()
        self.write_comments()

        self.textChangedAt = None

    def _load_comments(self, label):
        """
        Populate the comments and flag columns.
        Attempt to load comments from file.

        Parameters
        ----------
        label : str
            The label of the data in
            session.data_collection.
        """

        #Make sure its the right data
        #(beacuse subset data is masked)
        idx = self._data_collection_index(label)
        if idx == -1:
            return False
        data = self.session.data_collection[idx]

        #Fill in default comments:
        length = data.shape[0]
        new_comments = np.array(["" for i in range(length)], dtype=object)
        new_flags = np.array(["0" for i in range(length)], dtype=object)

        #Fill in any saved comments:
        meta = data.meta
        obj_names = data.get_component(
            self.catalog.meta["special_columns"]["source_id"]).labels

        if "MOSViz_comments" in meta.keys():
            try:
                comments = meta["MOSViz_comments"]
                for key in comments.keys():
                    index = self._id_to_index_hash(key, obj_names)
                    if index is not None:
                        line = comments[key]
                        new_comments[index] = line
            except Exception as e:
                print("MOSViz Comment Load Failed: ", e)

        if "MOSViz_flags" in meta.keys():
            try:
                flags = meta["MOSViz_flags"]
                for key in flags.keys():
                    index = self._id_to_index_hash(key, obj_names)
                    if index is not None:
                        line = flags[key]
                        new_flags[index] = line
            except Exception as e:
                print("MOSViz Flag Load Failed: ", e)

        #Send to DC
        data.add_component(CategoricalComponent(new_flags, "flag"), "flag")
        data.add_component(CategoricalComponent(new_comments, "comments"),
                           "comments")
        return True

    def write_comments(self):
        """
        Setup save file. Write comments and flags to file
        """

        if self.savepath is None:
            fail = self._setup_save_path()
            if fail: return
        if self.savepath == -1:
            return  #Do not save to file option

        idx = self.data_idx
        data = self.session.data_collection[idx]
        save_comments = data.get_component("comments").labels
        save_flag = data.get_component("flag").labels
        obj_names = data.get_component(
            self.catalog.meta["special_columns"]["source_id"]).labels

        fn = self.savepath
        folder = os.path.dirname(fn)

        t = astropy_table.data_to_astropy_table(data)

        #Check if load and save dir paths match
        temp = os.path.dirname(self.filepath)
        if not os.path.samefile(folder, temp):
            t['spectrum1d'].flags.writeable = True
            t['spectrum2d'].flags.writeable = True
            t['cutout'].flags.writeable = True
            for i in range(len(t)):
                t['spectrum1d'][i] = os.path.abspath(t['spectrum1d'][i])
                t['spectrum2d'][i] = os.path.abspath(t['spectrum2d'][i])
                t['cutout'][i] = os.path.abspath(t['cutout'][i])
        try:
            t.remove_column("comments")
            t.remove_column("flag")

            keys = t.meta.keys()

            if "MOSViz_comments" in keys:
                t.meta.pop("MOSViz_comments")

            if "MOSViz_flags" in keys:
                t.meta.pop("MOSViz_flags")

            comments = OrderedDict()
            flags = OrderedDict()

            for i, line in enumerate(save_comments):
                if line != "":
                    line = line.replace("\n", " ")
                    key = str(obj_names[i])
                    comments[key] = line

            for i, line in enumerate(save_flag):
                if line != "0" and line != "":
                    line = comments.replace("\n", " ")
                    key = str(obj_names[i])
                    flags[key] = line

            if len(comments) > 0:
                t.meta["MOSViz_comments"] = comments
            if len(flags) > 0:
                t.meta["MOSViz_flags"] = flags

            t.write(fn, format="ascii.ecsv", overwrite=True)
        except Exception as e:
            print("Comment write failed:", e)

    def closeEvent(self, event):
        """
        Clean up the extraneous data components created when opening the
        MOSViz viewer by overriding the parent class's close event.
        """
        super(MOSVizViewer, self).closeEvent(event)

        for data in self._loaded_data.values():
            self.session.data_collection.remove(data)
Пример #19
0
    def setupUI(self):

        from functools import partial
        from qtpy.QtCore import Qt
        from qtpy.QtWidgets import QHBoxLayout, QFormLayout, QSlider, QPushButton, QPlainTextEdit, QCheckBox, QComboBox
        from qtpy.QtWidgets import QRadioButton, QWidget, QVBoxLayout, QListView, QSpacerItem, QSizePolicy

        layout = QHBoxLayout()

        layout1 = QFormLayout()

        self.fontsizeslider = QSlider(Qt.Horizontal)
        self.fontsizeslider.setMinimum(1)
        self.fontsizeslider.setMaximum(12)
        self.fontsizeslider.setTickInterval(2)
        self.fontsizeslider.setSingleStep(2)
        self.fontsizeslider.setValue(config.presentationFontSize / 0.5)
        self.fontsizeslider.setToolTip(str(config.presentationFontSize))
        self.fontsizeslider.valueChanged.connect(
            self.presentationFontSizeChanged)
        layout1.addRow("Font Size", self.fontsizeslider)

        self.changecolorbutton = QPushButton()
        buttonStyle = "QPushButton {0}background-color: {2}; color: {3};{1}".format(
            "{", "}", config.presentationColorOnDarkTheme if config.theme
            == "dark" else config.presentationColorOnLightTheme,
            "white" if config.theme == "dark" else "black")
        self.changecolorbutton.setStyleSheet(buttonStyle)
        self.changecolorbutton.setToolTip("Change Color")
        self.changecolorbutton.clicked.connect(self.changeColor)
        layout1.addRow("Font Color", self.changecolorbutton)

        self.marginslider = QSlider(Qt.Horizontal)
        self.marginslider.setMinimum(0)
        self.marginslider.setMaximum(200)
        self.marginslider.setTickInterval(50)
        self.marginslider.setSingleStep(50)
        self.marginslider.setValue(config.presentationMargin)
        self.marginslider.setToolTip(str(config.presentationMargin))
        self.marginslider.valueChanged.connect(self.presentationMarginChanged)
        layout1.addRow("Margin", self.marginslider)

        self.verticalpositionslider = QSlider(Qt.Horizontal)
        self.verticalpositionslider.setMinimum(10)
        self.verticalpositionslider.setMaximum(90)
        self.verticalpositionslider.setTickInterval(10)
        self.verticalpositionslider.setSingleStep(10)
        self.verticalpositionslider.setValue(
            config.presentationVerticalPosition)
        self.verticalpositionslider.setToolTip(
            str(config.presentationVerticalPosition))
        self.verticalpositionslider.valueChanged.connect(
            self.presentationVerticalPositionChanged)
        layout1.addRow("Vertical Position", self.verticalpositionslider)

        self.horizontalpositionslider = QSlider(Qt.Horizontal)
        self.horizontalpositionslider.setMinimum(10)
        self.horizontalpositionslider.setMaximum(90)
        self.horizontalpositionslider.setTickInterval(10)
        self.horizontalpositionslider.setSingleStep(10)
        self.horizontalpositionslider.setValue(
            config.presentationHorizontalPosition)
        self.horizontalpositionslider.setToolTip(
            str(config.presentationHorizontalPosition))
        self.horizontalpositionslider.valueChanged.connect(
            self.presentationHorizontalPositionChanged)
        layout1.addRow("Horizontal Position", self.horizontalpositionslider)

        self.showBibleSelection = QRadioButton()
        self.showBibleSelection.setChecked(True)
        self.showBibleSelection.clicked.connect(
            lambda: self.selectRadio("bible"))
        layout1.addRow("Bible", self.showBibleSelection)

        if len(self.books) > 0:
            self.showHymnsSelection = QRadioButton()
            self.showHymnsSelection.setChecked(False)
            self.showHymnsSelection.clicked.connect(
                lambda: self.selectRadio("hymns"))
            layout1.addRow("Hymns", self.showHymnsSelection)

        # Second column

        layout2 = QVBoxLayout()

        self.bibleWidget = QWidget()
        self.bibleLayout = QFormLayout()

        checkbox = QCheckBox()
        checkbox.setText("")
        checkbox.setChecked(config.presentationParser)
        checkbox.stateChanged.connect(self.presentationParserChanged)
        checkbox.setToolTip("Parse bible verse reference in the entered text")
        self.bibleLayout.addRow("Bible Reference", checkbox)

        versionCombo = QComboBox()
        self.bibleVersions = self.parent.textList
        versionCombo.addItems(self.bibleVersions)
        initialIndex = 0
        if config.mainText in self.bibleVersions:
            initialIndex = self.bibleVersions.index(config.mainText)
        versionCombo.setCurrentIndex(initialIndex)
        versionCombo.currentIndexChanged.connect(self.changeBibleVersion)
        self.bibleLayout.addRow("Bible Version", versionCombo)

        self.textEntry = QPlainTextEdit("John 3:16; Rm 5:8")
        self.bibleLayout.addRow(self.textEntry)

        button = QPushButton("Presentation")
        button.setToolTip("Go to Presentation")
        button.clicked.connect(self.goToPresentation)
        self.bibleLayout.addWidget(button)

        self.bibleLayout.addItem(
            QSpacerItem(20, 40, QSizePolicy.Minimum, QSizePolicy.Expanding))

        self.bibleWidget.setLayout(self.bibleLayout)

        self.hymnWidget = QWidget()
        self.hymnLayout = QFormLayout()

        selected = 0
        book = "Hymn Lyrics - English"
        if book in self.books:
            selected = self.books.index(book)
        self.bookList = QComboBox()
        self.bookList.addItems(self.books)
        self.bookList.setCurrentIndex(selected)
        self.bookList.currentIndexChanged.connect(self.selectHymnBook)
        self.hymnLayout.addWidget(self.bookList)

        self.chapterlist = QListView()
        self.chapterlist.clicked.connect(self.selectHymn)
        # self.chapterlist.selectionModel().selectionChanged.connect(self.selectHymn)
        self.hymnLayout.addWidget(self.chapterlist)

        self.buttons = []
        for count in range(0, 10):
            hymnButton = QPushButton()
            hymnButton.setText(" ")
            hymnButton.setEnabled(False)
            hymnButton.clicked.connect(partial(self.selectParagraph, count))
            self.hymnLayout.addWidget(hymnButton)
            self.buttons.append(hymnButton)

        self.selectHymnBook(selected)

        self.hymnWidget.setLayout(self.hymnLayout)
        self.hymnWidget.hide()

        layout2.addWidget(self.bibleWidget)
        if len(self.books) > 0:
            layout2.addWidget(self.hymnWidget)

        layout.addLayout(layout1)
        layout.addLayout(layout2)
        self.setLayout(layout)
Пример #20
0
 def focusOutEvent(self, event):
     # set up signals
     self.userFinishedEditing()
     return QPlainTextEdit.focusOutEvent(self, event)
Пример #21
0
 def focusInEvent(self, event):
     self.setOrigValue(self.text())
     return QPlainTextEdit.focusInEvent(self, event)
Пример #22
0
 def focusOutEvent(self, event):
     """Reimplemented to handle focus"""
     self.focus_changed.emit()
     QPlainTextEdit.focusOutEvent(self, event)
Пример #23
0
class ZusatzFensterKerndaten(QWidget):
    def __init__(self, nummer, text):
        super().__init__()
        self.initMe(nummer, text)

    def initMe(self, nummer, text):
        self.l1 = QLabel(self)
        self.l1.setText('Inhalt der eingelesenen Zelle')
        self.l1.move(20, 5)

        self.nummer = nummer
        self.setGeometry(400, 300, 500, 700)
        self.zelle = QPlainTextEdit(self)
        self.zelle.setGeometry(0, 40, 500, 250)
        self.zelle.setPlainText(text)
        self.zelle.setReadOnly(True)

        self.l2 = QLabel(self)
        self.l2.setText(
            """Bitte geben Sie hier den Wert ein nach dem in der Zelle gesucht werden soll.
Bsp. Wollen Sie einen Lastpunkt auslesen, welcher mit 5000 rpm angegeben ist, geben Sie 5000 ein.
Achtung: keine Einheiten mit angeben. Nur Zahlen!""")
        self.l2.move(10, 330)

        self.eing = QLineEdit(self)
        self.eing.move(10, 410)

        p = QPushButton('Prüfen', self)
        p.clicked.connect(self.pruefen)
        p.move(180, 409)

        self.l3 = QLabel(self)
        self.l3.setText('vorangehende Zeichenkette')
        self.l3.move(10, 460)

        self.suchstring = QLineEdit(self)
        self.suchstring.move(180, 459)
        self.suchstring.setDisabled(True)

        self.l5 = QLabel(self)
        self.l5.setStyleSheet("background-color: yellow")
        self.l5.setText(
            "Prüfen Sie die vorrangehende Zeichenkette.\nSollte diese nicht stimmen, können Sie selbst eine angeben und erneut prüfen.\nAchtung: Leerzeichen nicht vergessen "
        )
        self.l5.move(10, 490)
        self.l5.setVisible(False)

        self.l4 = QLabel(self)
        self.l4.setText('gefundener Eintrag')
        self.l4.move(10, 540)

        self.gefundener_string = QLineEdit(self)
        self.gefundener_string.move(180, 539)
        self.gefundener_string.setReadOnly(True)

        frage = QPushButton(self)
        frage.setIcon(QIcon("bilder_vorlagenersteller\\FrageIcon.png"))
        frage.move(450, 10)
        frage.clicked.connect(self.Hilfe)

        self.weiter = QPushButton('Weiter', self)
        self.weiter.move(420, 650)
        self.weiter.setDisabled(True)
        self.weiter.clicked.connect(self.weiter_gehts)

    def suchstring_finden(self):
        startindex = self.zelle.toPlainText().find(self.eing.text())
        if startindex == 0:
            suchstring = '##Anfang###'

        elif startindex == -1:
            suchstring = 'ungültige Eingabe'

        else:
            suchstring = ''
            for i in range(0, 11):
                suchstring = self.zelle.toPlainText()[startindex -
                                                      i] + suchstring
                if (startindex - i) == 0:
                    break

        return suchstring[:-1]

    def pruefen(self):
        suchstring = self.suchstring.text()

        if suchstring == '':
            suchstring = self.suchstring_finden()
        print(suchstring)

        self.suchstring.setDisabled(False)
        self.l5.setVisible(True)
        self.weiter.setDisabled(False)
        self.suchstring.setText(suchstring)

        startindex = self.zelle.toPlainText().find(suchstring) + len(
            suchstring)
        ende = startindex + len(self.eing.text())

        self.gefundener_string.setText(
            self.zelle.toPlainText()[startindex:ende])

    def weiter_gehts(self):
        w.findChild(QLabel, self.nummer).setVisible(True)
        w.findChild(QLineEdit, 'suchstr' + self.nummer).setVisible(True)
        w.findChild(QLineEdit,
                    'suchstr' + self.nummer).setText(self.suchstring.text())
        self.close()

    def Hilfe(self):
        self.h = HilfeFenster(
            "bilder_vorlagenersteller\\erweitertes_einlesen.png")
        self.h.show()
Пример #24
0
class ArbeitsdatenFenster(QWidget):
    def __init__(self):
        super().__init__()
        self.initMe()

    def initMe(self):

        kd = QLabel(self)
        kd.setText(
            'Einlesen der Arbeitsdaten\n\nMappe\t\t Zeile1      Spalte1      Länge      Breite'
        )
        kd.move(100, 80)

        dt = QLabel(self)
        dt.setText(
            'Gesamtmoment\n\n\n\n\n\n\n\n\nRadialkraft\n\n\n\n\n\n\n\n\nTangentialkraft\n\n\n\n\n\n\n\n\nBiegemoment'
        )
        dt.move(10, 133)
        vb = QLabel(self)

        vb.move(200, 110)

        frage = QPushButton(self)
        frage.setIcon(QIcon("bilder_vorlagenersteller\\FrageIcon.png"))
        frage.move(510, 30)
        frage.clicked.connect(self.Hilfe)

        self.weiter = QPushButton('Weiter', self)
        self.weiter.setDisabled(True)
        self.weiter_gehts = [False, False, False, False]
        self.weiter.move(480, 650)
        self.weiter.clicked.connect(self.weiter_funkt)

        name_dbox = [
            'Dropbox\nGesamtmoment', 'Dropbox\nRadialkraft',
            'Dropbox\nTangentialkraft', 'Dropbox\nBiegemoment'
        ]
        for zeile in range(0, 4):

            self.drop = Dropbox(self)
            self.drop.setGeometry(420, 125 + zeile * 120, 140, 100)
            self.drop.setText(name_dbox[zeile])
            self.drop.setObjectName('Adrop' + str(zeile))

            for spalte in range(0, 5):

                self.eing = QPlainTextEdit(self)

                self.eing.setAcceptDrops(False)
                self.eing.setObjectName(str(zeile) + str(spalte))
                self.eing.textChanged.connect(self.freigabe)
                if spalte == 0:
                    self.eing.setGeometry(100 + 80 * spalte, 125 + zeile * 120,
                                          80, 100)
                elif spalte < 3:
                    self.eing.setGeometry(200 + 45 * (spalte - 1),
                                          125 + zeile * 120, 40, 100)
                else:
                    self.eing.setGeometry(210 + 45 * (spalte - 1),
                                          125 + zeile * 120, 40, 100)
                    self.eing.textChanged.connect(self.freigabe)

        self.show()

    def freigabe(self):
        self.weiter.setEnabled(True)

        for zeile in range(0, 4):
            for spalte in range(0, 5):
                if self.findChild(QPlainTextEdit,
                                  str(zeile) +
                                  str(spalte)).toPlainText() == '':
                    self.weiter.setDisabled(True)

    def weiter_funkt(self):
        self.arbeits_daten = {}
        datenblock = []
        for zeile in range(0, 4):
            datenzeile = []
            for spalte in range(0, 5):
                eintraege = self.findChild(QPlainTextEdit,
                                           str(zeile) +
                                           str(spalte)).toPlainText()
                datenzeile.append(eintraege.split('\n'))
            datenblock.append(datenzeile)
        self.arbeits_daten['gesamt_moment'] = datenblock[0]
        self.arbeits_daten['f_rad'] = datenblock[1]
        self.arbeits_daten['f_tan'] = datenblock[2]
        self.arbeits_daten['biege_moment'] = datenblock[3]
        self.close()

    def Hilfe(self):
        self.h = HilfeFenster(
            "bilder_vorlagenersteller\\einlesen_arbeitsdaten.png")
        self.h.show()
Пример #25
0
 def resizeEvent(self, *e):
     cr = self.contentsRect()
     rec = QRect(cr.left(), cr.top(), self.margin.getWidth(), cr.height())
     self.margin.setGeometry(rec)
     QPlainTextEdit.resizeEvent(self, *e)
Пример #26
0
class GetValueDialog(QDialog):
    """
    A dialog that gets a single value
    """
    def __init__(self, parent=None, label_name=''):
        """
        :param parent:
        :param label_name
        """
        super(GetValueDialog, self).__init__(parent)

        layout = QVBoxLayout(self)

        # details information
        self.info_line = QPlainTextEdit(self)
        self.info_line.setEnabled(False)
        layout.addWidget(self.info_line)

        # input
        self.label = QLabel(self)
        self.value_edit = QLineEdit(self)
        layout.addWidget(self.label)
        layout.addWidget(self.value_edit)
        # END-IF-ELSE

        # nice widget for editing the date
        # self.datetime = QDateTimeEdit(self)
        # self.datetime.setCalendarPopup(True)
        # self.datetime.setDateTime(QDateTime.currentDateTime())
        # layout.addWidget(self.datetime)

        # OK and Cancel buttons
        buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel,
                                   QtCore.Qt.Horizontal, self)

        buttons.accepted.connect(self.accept)
        buttons.rejected.connect(self.reject)
        layout.addWidget(buttons)

        # set some values
        self.setWindowTitle('Get user input')
        self.label.setText(label_name)

        return

    # get current date and time from the dialog
    def get_value(self):
        """ get the value in string
        :return:
        """
        return str(self.value_edit.text())

    def set_message_type(self, message_type):
        """

        :param message_type:
        :return:
        """
        if message_type == 'error':
            self.value_edit.setStyleSheet("color: rgb(255, 0, 0);")
        else:
            self.value_edit.setStyleSheet('color: blue')

        return

    def set_title(self, title):
        """
        set window/dialog title
        :param title:
        :return:
        """
        self.setWindowTitle(title)

        return

    def show_message(self, message):
        """
        set or show message
        :param message:
        :return:
        """
        self.info_line.setPlainText(message)

        return
Пример #27
0
    def render_data(self,
                    row,
                    spec1d_data=None,
                    spec2d_data=None,
                    image_data=None):
        """
        Render the updated data sets in the individual plot widgets within the
        MOSViz viewer.
        """
        self._check_unsaved_comments()

        if spec1d_data is not None:

            spectrum1d_x = spec1d_data[spec1d_data.id['Wavelength']]
            spectrum1d_y = spec1d_data[spec1d_data.id['Flux']]
            spectrum1d_yerr = spec1d_data[spec1d_data.id['Uncertainty']]

            self.spectrum1d_widget.set_data(x=spectrum1d_x,
                                            y=spectrum1d_y,
                                            yerr=spectrum1d_yerr)

            # Try to retrieve the wcs information
            try:
                flux_unit = spec1d_data.header.get('BUNIT', 'Jy').lower()
                flux_unit = flux_unit.replace('counts', 'count')
                flux_unit = u.Unit(flux_unit)
            except ValueError:
                flux_unit = u.Unit("Jy")

            try:
                disp_unit = spec1d_data.header.get('CUNIT1',
                                                   'Angstrom').lower()
                disp_unit = u.Unit(disp_unit)
            except ValueError:
                disp_unit = u.Unit("Angstrom")

            self.spectrum1d_widget.axes.set_xlabel(
                "Wavelength [{}]".format(disp_unit))
            self.spectrum1d_widget.axes.set_ylabel(
                "Flux [{}]".format(flux_unit))

        if image_data is not None:
            wcs = image_data.coords.wcs

            self.image_widget.set_image(image_data.get_component(
                image_data.id['Flux']).data,
                                        wcs=wcs,
                                        interpolation='none',
                                        origin='lower')

            self.image_widget.axes.set_xlabel("Spatial X")
            self.image_widget.axes.set_ylabel("Spatial Y")

            # Add the slit patch to the plot

            ra = row[self.catalog.meta["special_columns"]
                     ["slit_ra"]] * u.degree
            dec = row[self.catalog.meta["special_columns"]
                      ["slit_dec"]] * u.degree
            slit_width = row[self.catalog.meta["special_columns"]
                             ["slit_width"]]
            slit_length = row[self.catalog.meta["special_columns"]
                              ["slit_length"]]

            skycoord = SkyCoord(ra, dec, frame='fk5')
            xp, yp = skycoord.to_pixel(wcs)

            scale = np.sqrt(proj_plane_pixel_area(wcs)) * 3600.

            dx = slit_width / scale
            dy = slit_length / scale

            self.image_widget.draw_rectangle(x=xp, y=yp, width=dx, height=dy)

            self.image_widget._redraw()
        else:
            self.image_widget.setVisible(False)

        # Plot the 2D spectrum data last because by then we can make sure that
        # we set up the extent of the image appropriately if the cutout and the
        # 1D spectrum are present so that the axes can be locked.

        if spec2d_data is not None:
            wcs = spec2d_data.coords.wcs

            xp2d = np.arange(spec2d_data.shape[1])
            yp2d = np.repeat(0, spec2d_data.shape[1])
            spectrum2d_disp, spectrum2d_offset = spec2d_data.coords.pixel2world(
                xp2d, yp2d)
            x_min = spectrum2d_disp.min()
            x_max = spectrum2d_disp.max()

            if image_data is None:
                y_min = -0.5
                y_max = spec2d_data.shape[0] - 0.5
            else:
                y_min = yp - dy / 2.
                y_max = yp + dy / 2.

            extent = [x_min, x_max, y_min, y_max]

            self.spectrum2d_widget.set_image(image=spec2d_data.get_component(
                spec2d_data.id['Flux']).data,
                                             interpolation='none',
                                             aspect='auto',
                                             extent=extent,
                                             origin='lower')

            self.spectrum2d_widget.axes.set_xlabel("Wavelength")
            self.spectrum2d_widget.axes.set_ylabel("Spatial Y")

            self.spectrum2d_widget._redraw()

        # Clear the meta information widget
        # NOTE: this process is inefficient
        for i in range(self.meta_form_layout.count()):
            wid = self.meta_form_layout.itemAt(i).widget()
            label = self.meta_form_layout.labelForField(wid)

            if label is not None:
                label.deleteLater()

            wid.deleteLater()

        # Repopulate the form layout
        # NOTE: this process is inefficient
        for col in row.colnames:
            if col.lower() not in ["comments", "flag"]:
                line_edit = QLineEdit(str(row[col]),
                                      self.central_widget.meta_form_widget)
                line_edit.setReadOnly(True)

                self.meta_form_layout.addRow(col, line_edit)

        # Set up comment and flag input/display boxes
        if self.comments:
            if self.savepath is not None:
                if self.savepath == -1:
                    line_edit = QLineEdit(
                        os.path.basename("Not Saving to File."),
                        self.central_widget.meta_form_widget)
                    line_edit.setReadOnly(True)
                    self.meta_form_layout.addRow("Save File", line_edit)
                else:
                    line_edit = QLineEdit(os.path.basename(self.savepath),
                                          self.central_widget.meta_form_widget)
                    line_edit.setReadOnly(True)
                    self.meta_form_layout.addRow("Save File", line_edit)

            self.input_flag = QLineEdit(self.get_flag(),
                                        self.central_widget.meta_form_widget)
            self.input_flag.textChanged.connect(self._text_changed)
            self.input_flag.setStyleSheet(
                "background-color: rgba(255, 255, 255);")
            self.meta_form_layout.addRow("Flag", self.input_flag)

            self.input_comments = QPlainTextEdit(
                self.get_comment(), self.central_widget.meta_form_widget)
            self.input_comments.textChanged.connect(self._text_changed)
            self.input_comments.setStyleSheet(
                "background-color: rgba(255, 255, 255);")
            self.meta_form_layout.addRow("Comments", self.input_comments)

            self.input_save = QPushButton('Save',
                                          self.central_widget.meta_form_widget)
            self.input_save.clicked.connect(self.update_comments)
            self.input_save.setDefault(True)

            self.input_refresh = QPushButton(
                'Reload', self.central_widget.meta_form_widget)
            self.input_refresh.clicked.connect(self.refresh_comments)

            self.meta_form_layout.addRow(self.input_save, self.input_refresh)
Пример #28
0
 def __initWidgets(self):
     self.consoleEdit = QPlainTextEdit()
     self.setWidget(self.consoleEdit)
Пример #29
0
 def focusInEvent(self, event):
     """Reimplemented to handle focus"""
     self.focus_changed.emit()
     self.focus_in.emit()
     self.highlight_current_cell()
     QPlainTextEdit.focusInEvent(self, event)
Пример #30
0
    def render_data(self,
                    row,
                    spec1d_data=None,
                    spec2d_data=None,
                    image_data=None,
                    level2_data=None):
        """
        Render the updated data sets in the individual plot widgets within the
        MOSViz viewer.
        """
        self._check_unsaved_comments()

        self.image_viewer_hidden = image_data is None

        if spec1d_data is not None:
            # TODO: This should not be needed. Must explore why the core model
            # is out of sync with the proxy model.
            self.spectrum1d_widget.plot_widget.clear_plots()

            # Clear the specviz model of any rendered plot items
            self._specviz_viewer.model.clear()

            # Create a new Spectrum1D object from the flux data attribute of
            # the incoming data
            spec = glue_data_to_spectrum1d(spec1d_data, 'Flux')

            # Create a DataItem from the Spectrum1D object, which adds the data
            # to the internel specviz model
            data_item = self._specviz_viewer.model.add_data(spec, 'Spectrum1D')

            # Get the PlotDataItem rendered via the plot's proxy model and
            # ensure that it is visible in the plot
            plot_data_item = self.spectrum1d_widget.proxy_model.item_from_id(
                data_item.identifier)
            plot_data_item.visible = True
            plot_data_item.color = "#000000"

            # Explicitly let the plot widget know that data items have changed
            self.spectrum1d_widget.plot_widget.on_item_changed(data_item)

        if not self.image_viewer_hidden:
            if not self.image_widget.isVisible():
                self.image_widget.setVisible(True)
            wcs = image_data.coords.wcs
            self.cutout_wcs = wcs

            array = image_data.get_component(image_data.id['Flux']).data

            # Add the slit patch to the plot
            self.slit_controller.clear_slits()
            if "slit_width" in self.catalog.meta["special_columns"] and \
                    "slit_length" in self.catalog.meta["special_columns"] and \
                    wcs is not None:
                self.add_slit(row)
                self.image_widget.draw_slit()
            else:
                self.image_widget.reset_limits()

            self.image_widget.set_image(array,
                                        wcs=wcs,
                                        interpolation='none',
                                        origin='lower')

            self.image_widget.axes.set_xlabel("Spatial X")
            self.image_widget.axes.set_ylabel("Spatial Y")
            if self.slit_controller.has_slits:
                self.image_widget.set_slit_limits()

            self.image_widget._redraw()
        else:
            self.cutout_wcs = None

        # Plot the 2D spectrum data last because by then we can make sure that
        # we set up the extent of the image appropriately if the cutout and the
        # 1D spectrum are present so that the axes can be locked.

        # We are repurposing the spectrum 2d widget to handle the display of both
        # the level 3 and level 2 spectra.
        if spec2d_data is not None or level2_data is not None:
            self._load_spectrum2d_widget(spec2d_data, level2_data)
        else:
            self.spectrum2d_widget.no_data()

        # Clear the meta information widget
        # NOTE: this process is inefficient
        for i in range(self.meta_form_layout.count()):
            wid = self.meta_form_layout.itemAt(i).widget()
            label = self.meta_form_layout.labelForField(wid)

            if label is not None:
                label.deleteLater()

            wid.deleteLater()

        # Repopulate the form layout
        # NOTE: this process is inefficient
        for col in row.colnames:
            if col.lower() not in ["comments", "flag"]:
                line_edit = QLineEdit(str(row[col]),
                                      self.central_widget.meta_form_widget)
                line_edit.setReadOnly(True)

                self.meta_form_layout.addRow(col, line_edit)

        # Set up comment and flag input/display boxes
        if self.comments:
            if self.savepath is not None:
                if self.savepath == -1:
                    line_edit = QLineEdit(
                        os.path.basename("Not Saving to File."),
                        self.central_widget.meta_form_widget)
                    line_edit.setReadOnly(True)
                    self.meta_form_layout.addRow("Save File", line_edit)
                else:
                    line_edit = QLineEdit(os.path.basename(self.savepath),
                                          self.central_widget.meta_form_widget)
                    line_edit.setReadOnly(True)
                    self.meta_form_layout.addRow("Save File", line_edit)

            self.input_flag = QLineEdit(self.get_flag(),
                                        self.central_widget.meta_form_widget)
            self.input_flag.textChanged.connect(self._text_changed)
            self.input_flag.setStyleSheet(
                "background-color: rgba(255, 255, 255);")
            self.meta_form_layout.addRow("Flag", self.input_flag)

            self.input_comments = QPlainTextEdit(
                self.get_comment(), self.central_widget.meta_form_widget)
            self.input_comments.textChanged.connect(self._text_changed)
            self.input_comments.setStyleSheet(
                "background-color: rgba(255, 255, 255);")
            self.meta_form_layout.addRow("Comments", self.input_comments)

            self.input_save = QPushButton('Save',
                                          self.central_widget.meta_form_widget)
            self.input_save.clicked.connect(self.update_comments)
            self.input_save.setDefault(True)

            self.input_refresh = QPushButton(
                'Reload', self.central_widget.meta_form_widget)
            self.input_refresh.clicked.connect(self.refresh_comments)

            self.meta_form_layout.addRow(self.input_save, self.input_refresh)

        if not self.isHidden() and self.image_viewer_hidden:
            self.image_widget.setVisible(False)
Пример #31
0
class Properties(QWidget):
    def __init__(self, settings: PartSettings):
        super().__init__()
        self._settings = settings
        self.export_btn = QPushButton("Export profile")
        self.export_btn.clicked.connect(self.export_profile)
        self.import_btn = QPushButton("Import profile")
        self.import_btn.clicked.connect(self.import_profiles)
        self.export_pipeline_btn = QPushButton("Export pipeline")
        self.export_pipeline_btn.clicked.connect(self.export_pipeline)
        self.import_pipeline_btn = QPushButton("Import pipeline")
        self.import_pipeline_btn.clicked.connect(self.import_pipeline)
        self.delete_btn = QPushButton("Delete profile")
        self.delete_btn.setDisabled(True)
        self.delete_btn.clicked.connect(self.delete_profile)
        self.multiple_files_chk = QCheckBox("Show multiple files panel")
        self.multiple_files_chk.setChecked(
            self._settings.get("multiple_files_widget", False))
        self.multiple_files_chk.stateChanged.connect(
            self.multiple_files_visibility)
        self.rename_btn = QPushButton("Rename profile")
        self.rename_btn.clicked.connect(self.rename_profile)
        self.rename_btn.setDisabled(True)
        self.voxel_size_label = QLabel()
        self.info_label = QPlainTextEdit()
        self.info_label.setReadOnly(True)
        self.profile_list = SearchableListWidget()
        self.profile_list.currentTextChanged.connect(self.profile_chosen)
        self.pipeline_list = SearchableListWidget()
        self.pipeline_list.currentTextChanged.connect(self.profile_chosen)
        self.spacing = [QDoubleSpinBox() for _ in range(3)]
        self.lock_spacing = LockCheckBox()
        self.lock_spacing.stateChanged.connect(self.spacing[1].setDisabled)
        self.lock_spacing.stateChanged.connect(self.synchronize_spacing)
        # noinspection PyUnresolvedReferences
        self.spacing[2].valueChanged.connect(self.synchronize_spacing)
        units_value = self._settings.get("units_value", Units.nm)
        for el in self.spacing:
            el.setAlignment(Qt.AlignRight)
            el.setButtonSymbols(QAbstractSpinBox.NoButtons)
            el.setRange(0, 1000000)
            # noinspection PyUnresolvedReferences
            el.valueChanged.connect(self.image_spacing_change)
        self.units = EnumComboBox(Units)
        self.units.set_value(units_value)
        # noinspection PyUnresolvedReferences
        self.units.currentIndexChanged.connect(self.update_spacing)

        spacing_layout = QHBoxLayout()
        spacing_layout.addWidget(self.lock_spacing)
        for txt, el in zip(["x", "y", "z"], self.spacing[::-1]):
            spacing_layout.addWidget(QLabel(txt + ":"))
            spacing_layout.addWidget(el)
        spacing_layout.addWidget(self.units)
        spacing_layout.addStretch(1)
        voxel_size_layout = QHBoxLayout()
        voxel_size_layout.addWidget(self.voxel_size_label)
        voxel_size_layout.addSpacing(30)
        profile_layout = QGridLayout()
        profile_layout.setSpacing(0)
        profile_layout.addWidget(QLabel("Profiles:"), 0, 0)
        profile_layout.addWidget(self.profile_list, 1, 0)
        profile_layout.addWidget(QLabel("Pipelines:"), 2, 0)
        profile_layout.addWidget(self.pipeline_list, 3, 0, 4, 1)
        profile_layout.addWidget(self.info_label, 1, 1, 3, 2)
        profile_layout.addWidget(self.export_btn, 4, 1)
        profile_layout.addWidget(self.import_btn, 4, 2)
        profile_layout.addWidget(self.export_pipeline_btn, 5, 1)
        profile_layout.addWidget(self.import_pipeline_btn, 5, 2)
        profile_layout.addWidget(self.delete_btn, 6, 1)
        profile_layout.addWidget(self.rename_btn, 6, 2)
        layout = QVBoxLayout()
        layout.addLayout(spacing_layout)
        layout.addLayout(voxel_size_layout)
        layout.addWidget(self.multiple_files_chk)

        layout.addLayout(profile_layout, 1)
        self.setLayout(layout)

    @Slot(int)
    def multiple_files_visibility(self, val: int):
        self._settings.set("multiple_files_widget", val)

    # @Slot(str)  # PySide bug
    def profile_chosen(self, text):
        if text == "":
            self.delete_btn.setEnabled(False)
            self.rename_btn.setEnabled(False)
            self.info_label.setPlainText("")
            return
        try:
            if self.sender() == self.profile_list.list_widget:
                profile = self._settings.segmentation_profiles[text]
                self.pipeline_list.selectionModel().clear()
                self.delete_btn.setText("Delete profile")
                self.rename_btn.setText("Rename profile")
            elif self.sender() == self.pipeline_list.list_widget:
                profile = self._settings.segmentation_pipelines[text]
                self.profile_list.selectionModel().clear()
                self.delete_btn.setText("Delete pipeline")
                self.rename_btn.setText("Rename pipeline")
            else:
                return
        except KeyError:
            return

        # TODO update with knowledge from profile dict
        self.delete_btn.setEnabled(True)
        self.rename_btn.setEnabled(True)
        self.info_label.setPlainText(
            profile.pretty_print(analysis_algorithm_dict))

    def synchronize_spacing(self):
        if self.lock_spacing.isChecked():
            self.spacing[1].setValue(self.spacing[2].value())

    def image_spacing_change(self):
        spacing = [
            el.value() / UNIT_SCALE[self.units.currentIndex()]
            for i, el in enumerate(self.spacing)
        ]
        if not self.spacing[0].isEnabled():
            spacing = spacing[1:]
        self._settings.image_spacing = spacing

        voxel_size = 1
        for el in self._settings.image_spacing:
            voxel_size *= el * UNIT_SCALE[self.units.currentIndex()]
        self.voxel_size_label.setText(
            f"Voxel_size: {voxel_size} {self.units.get_value().name}"
            f"<sup>{len(self._settings.image_spacing)}</sup>")

    def update_spacing(self, index=None):
        voxel_size = 1
        value = self.units.get_value()
        if index is not None:
            self._settings.set("units_value", value)
        for el, sp in zip(self.spacing[::-1],
                          self._settings.image_spacing[::-1]):
            el.blockSignals(True)
            current_size = sp * UNIT_SCALE[self.units.currentIndex()]
            voxel_size *= current_size
            el.setValue(current_size)
            el.blockSignals(False)
        self.spacing[0].setDisabled(len(self._settings.image_spacing) == 2)
        self.voxel_size_label.setText(
            f"Voxel_size: {voxel_size} {value.name}"
            f"<sup>{len(self._settings.image_spacing)}</sup>")

    def update_profile_list(self):
        current_names = set(self._settings.segmentation_profiles.keys())
        self.profile_list.clear()
        self.profile_list.addItems(sorted(current_names))
        self.pipeline_list.clear()
        self.pipeline_list.addItems(
            sorted(set(self._settings.segmentation_pipelines.keys())))
        self.delete_btn.setDisabled(True)
        self.rename_btn.setDisabled(True)
        self.info_label.setPlainText("")

    def showEvent(self, _event):
        self.update_profile_list()
        self.update_spacing()

    def event(self, event: QEvent):
        if event.type() == QEvent.WindowActivate and self.isVisible():
            self.update_profile_list()
            self.update_spacing()
            self.multiple_files_chk.setChecked(
                self._settings.get("multiple_files_widget", False))
        return super().event(event)

    def delete_profile(self):
        text, dkt = "", {}
        if self.profile_list.selectedItems():
            text = self.profile_list.selectedItems()[0].text()
            dkt = self._settings.segmentation_profiles
        elif self.pipeline_list.selectedItems():
            text = self.pipeline_list.selectedItems()[0].text()
            dkt = self._settings.segmentation_pipelines
        if text != "":
            self.delete_btn.setDisabled(True)
            del dkt[text]
            self.update_profile_list()

    def export_profile(self):
        exp = ExportDialog(self._settings.segmentation_profiles,
                           ProfileDictViewer)
        if not exp.exec_():
            return
        dial = QFileDialog(self, "Export profile segment")
        dial.setFileMode(QFileDialog.AnyFile)
        dial.setAcceptMode(QFileDialog.AcceptSave)
        dial.setDirectory(
            self._settings.get("io.save_directory", str(Path.home())))
        dial.setNameFilter("Segment profile (*.json)")
        dial.setDefaultSuffix("json")
        dial.selectFile("segment_profile.json")
        dial.setHistory(dial.history() + self._settings.get_path_history())
        if dial.exec_():
            file_path = dial.selectedFiles()[0]
            self._settings.set("io.save_directory", os.path.dirname(file_path))
            self._settings.add_path_history(os.path.dirname(file_path))
            data = {
                x: self._settings.segmentation_profiles[x]
                for x in exp.get_export_list()
            }
            with open(file_path, "w") as ff:
                json.dump(data,
                          ff,
                          cls=self._settings.json_encoder_class,
                          indent=2)

    def import_profiles(self):
        dial = QFileDialog(self, "Import profile segment")
        dial.setFileMode(QFileDialog.ExistingFile)
        dial.setAcceptMode(QFileDialog.AcceptOpen)
        dial.setDirectory(
            self._settings.get("io.save_directory", str(Path.home())))
        dial.setNameFilter("Segment profile (*.json)")
        dial.setHistory(dial.history() + self._settings.get_path_history())
        if dial.exec_():
            file_path = dial.selectedFiles()[0]
            save_dir = os.path.dirname(file_path)
            self._settings.set("io.save_directory", save_dir)
            self._settings.add_path_history(save_dir)
            profs, err = self._settings.load_part(file_path)
            if err:
                QMessageBox.warning(
                    self, "Import error",
                    "error during importing, part of data were filtered.")
            profiles_dict = self._settings.segmentation_profiles
            imp = ImportDialog(profs, profiles_dict, ProfileDictViewer)
            if not imp.exec_():
                return
            for original_name, final_name in imp.get_import_list():
                profiles_dict[final_name] = profs[original_name]
            self._settings.dump()
            self.update_profile_list()

    def export_pipeline(self):
        exp = ExportDialog(self._settings.segmentation_pipelines,
                           ProfileDictViewer)
        if not exp.exec_():
            return
        dial = QFileDialog(self, "Export pipeline segment")
        dial.setFileMode(QFileDialog.AnyFile)
        dial.setAcceptMode(QFileDialog.AcceptSave)
        dial.setDirectory(self._settings.get("io.save_directory", ""))
        dial.setNameFilter("Segment pipeline (*.json)")
        dial.setDefaultSuffix("json")
        dial.selectFile("segment_pipeline.json")
        dial.setHistory(dial.history() + self._settings.get_path_history())
        if dial.exec_():
            file_path = dial.selectedFiles()[0]
            data = {
                x: self._settings.segmentation_pipelines[x]
                for x in exp.get_export_list()
            }
            with open(file_path, "w") as ff:
                json.dump(data,
                          ff,
                          cls=self._settings.json_encoder_class,
                          indent=2)
            self._settings.set("io.save_directory", os.path.dirname(file_path))
            self._settings.add_path_history(os.path.dirname(file_path))

    def import_pipeline(self):
        dial = QFileDialog(self, "Import pipeline segment")
        dial.setFileMode(QFileDialog.ExistingFile)
        dial.setAcceptMode(QFileDialog.AcceptOpen)
        dial.setDirectory(self._settings.get("io.save_directory", ""))
        dial.setNameFilter("Segment pipeline (*.json)")
        dial.setHistory(dial.history() + self._settings.get_path_history())
        if dial.exec_():
            file_path = dial.selectedFiles()[0]
            self._settings.set("io.save_directory", os.path.dirname(file_path))
            self._settings.add_path_history(os.path.dirname(file_path))
            profs, err = self._settings.load_part(file_path)
            if err:
                QMessageBox.warning(
                    self, "Import error",
                    "error during importing, part of data were filtered.")
            profiles_dict = self._settings.segmentation_pipelines
            imp = ImportDialog(profs, profiles_dict, ProfileDictViewer)
            if not imp.exec_():
                return
            for original_name, final_name in imp.get_import_list():
                profiles_dict[final_name] = profs[original_name]
            self._settings.dump()
            self.update_profile_list()

    def rename_profile(self):
        profile_name, profiles_dict = "", {}
        if self.profile_list.selectedItems():
            profile_name = self.profile_list.selectedItems()[0].text()
            profiles_dict = self._settings.segmentation_profiles
        elif self.pipeline_list.selectedItems():
            profile_name = self.pipeline_list.selectedItems()[0].text()
            profiles_dict = self._settings.segmentation_pipelines
        if profile_name == "":
            return
        text, ok = QInputDialog.getText(self,
                                        "New profile name",
                                        f"New name for {profile_name}",
                                        text=profile_name)
        if ok:
            text = text.strip()
            if text in profiles_dict.keys():
                res = QMessageBox.warning(
                    self,
                    "Already exist",
                    f"Profile with name {text} already exist. Would you like to overwrite?",
                    QMessageBox.Yes | QMessageBox.No,
                    QMessageBox.No,
                )
                if res == QMessageBox.No:
                    self.rename_profile()
                    return
            profiles_dict[text] = profiles_dict.pop(profile_name)
            self._settings.dump()
            self.update_profile_list()
Пример #32
0
    def render_data(self, row, spec1d_data=None, spec2d_data=None,
                    image_data=None, level2_data=None):
        """
        Render the updated data sets in the individual plot widgets within the
        MOSViz viewer.
        """
        self._check_unsaved_comments()

        self.image_viewer_hidden = image_data is None

        if spec1d_data is not None:
            # TODO: This should not be needed. Must explore why the core model
            # is out of sync with the proxy model.
            self.spectrum1d_widget.plot_widget.clear_plots()

            # Clear the specviz model of any rendered plot items
            self._specviz_viewer.model.clear()

            # Create a new Spectrum1D object from the flux data attribute of
            # the incoming data
            spec = glue_data_to_spectrum1d(spec1d_data, 'Flux')

            # Create a DataItem from the Spectrum1D object, which adds the data
            # to the internel specviz model
            data_item = self._specviz_viewer.model.add_data(spec, 'Spectrum1D')

            # Get the PlotDataItem rendered via the plot's proxy model and
            # ensure that it is visible in the plot
            plot_data_item = self.spectrum1d_widget.proxy_model.item_from_id(data_item.identifier)
            plot_data_item.visible = True
            plot_data_item.color = "#000000"

            # Explicitly let the plot widget know that data items have changed
            self.spectrum1d_widget.plot_widget.on_item_changed(data_item)

        if not self.image_viewer_hidden:
            if not self.image_widget.isVisible():
                self.image_widget.setVisible(True)
            wcs = image_data.coords.wcs
            self.cutout_wcs = wcs

            array = image_data.get_component(image_data.id['Flux']).data

            # Add the slit patch to the plot
            self.slit_controller.clear_slits()
            if "slit_width" in self.catalog.meta["special_columns"] and \
                    "slit_length" in self.catalog.meta["special_columns"] and \
                    wcs is not None:
                self.add_slit(row)
                self.image_widget.draw_slit()
            else:
                self.image_widget.reset_limits()

            self.image_widget.set_image(array, wcs=wcs, interpolation='none', origin='lower')

            self.image_widget.axes.set_xlabel("Spatial X")
            self.image_widget.axes.set_ylabel("Spatial Y")
            if self.slit_controller.has_slits:
                self.image_widget.set_slit_limits()

            self.image_widget._redraw()
        else:
            self.cutout_wcs = None

        # Plot the 2D spectrum data last because by then we can make sure that
        # we set up the extent of the image appropriately if the cutout and the
        # 1D spectrum are present so that the axes can be locked.

        # We are repurposing the spectrum 2d widget to handle the display of both
        # the level 3 and level 2 spectra.
        if spec2d_data is not None or level2_data is not None:
            self._load_spectrum2d_widget(spec2d_data, level2_data)
        else:
            self.spectrum2d_widget.no_data()

        # Clear the meta information widget
        # NOTE: this process is inefficient
        for i in range(self.meta_form_layout.count()):
            wid = self.meta_form_layout.itemAt(i).widget()
            label = self.meta_form_layout.labelForField(wid)

            if label is not None:
                label.deleteLater()

            wid.deleteLater()

        # Repopulate the form layout
        # NOTE: this process is inefficient
        for col in row.colnames:
            if col.lower() not in ["comments", "flag"]:
                line_edit = QLineEdit(str(row[col]),
                                      self.central_widget.meta_form_widget)
                line_edit.setReadOnly(True)

                self.meta_form_layout.addRow(col, line_edit)

        # Set up comment and flag input/display boxes
        if self.comments:
            if self.savepath is not None:
                if self.savepath == -1:
                    line_edit = QLineEdit(os.path.basename("Not Saving to File."),
                                      self.central_widget.meta_form_widget)
                    line_edit.setReadOnly(True)
                    self.meta_form_layout.addRow("Save File", line_edit)
                else:
                    line_edit = QLineEdit(os.path.basename(self.savepath),
                                      self.central_widget.meta_form_widget)
                    line_edit.setReadOnly(True)
                    self.meta_form_layout.addRow("Save File", line_edit)

            self.input_flag = QLineEdit(self.get_flag(),
                self.central_widget.meta_form_widget)
            self.input_flag.textChanged.connect(self._text_changed)
            self.input_flag.setStyleSheet("background-color: rgba(255, 255, 255);")
            self.meta_form_layout.addRow("Flag", self.input_flag)

            self.input_comments = QPlainTextEdit(self.get_comment(),
                self.central_widget.meta_form_widget)
            self.input_comments.textChanged.connect(self._text_changed)
            self.input_comments.setStyleSheet("background-color: rgba(255, 255, 255);")
            self.meta_form_layout.addRow("Comments", self.input_comments)

            self.input_save = QPushButton('Save',
                self.central_widget.meta_form_widget)
            self.input_save.clicked.connect(self.update_comments)
            self.input_save.setDefault(True)

            self.input_refresh = QPushButton('Reload',
                self.central_widget.meta_form_widget)
            self.input_refresh.clicked.connect(self.refresh_comments)

            self.meta_form_layout.addRow(self.input_save, self.input_refresh)

        if not self.isHidden() and self.image_viewer_hidden:
            self.image_widget.setVisible(False)
Пример #33
0
    def __init__(self, parent=None):
        QPlainTextEdit.__init__(self, parent)
        BaseEditMixin.__init__(self)
        self.setAttribute(Qt.WA_DeleteOnClose)

        self.extra_selections_dict = {}
        self._restore_selection_pos = None

        # Trailing newlines/spaces trimming
        self.remove_trailing_spaces = False
        self.remove_trailing_newlines = False

        # Add a new line when saving
        self.add_newline = False

        # Code snippets
        self.code_snippets = True

        self.textChanged.connect(self.changed)
        self.cursorPositionChanged.connect(self.cursor_position_changed)

        self.indent_chars = " " * 4
        self.tab_stop_width_spaces = 4

        # Code completion / calltips
        if parent is not None:
            mainwin = parent
            while not isinstance(mainwin, QMainWindow):
                mainwin = mainwin.parent()
                if mainwin is None:
                    break
            if mainwin is not None:
                parent = mainwin

        self.completion_widget = CompletionWidget(self, parent)
        self.codecompletion_auto = False
        self.setup_completion()

        self.calltip_widget = CallTipWidget(self, hide_timer_on=False)
        self.calltip_position = None
        self.tooltip_widget = ToolTipWidget(self, as_tooltip=True)

        self.highlight_current_cell_enabled = False

        # The color values may be overridden by the syntax highlighter
        # Highlight current line color
        self.currentline_color = QColor(Qt.red).lighter(190)
        self.currentcell_color = QColor(Qt.red).lighter(194)

        # Brace matching
        self.bracepos = None
        self.matched_p_color = QColor(Qt.green)
        self.unmatched_p_color = QColor(Qt.red)

        self.decorations = TextDecorationsManager(self)

        # Save current cell. This is invalidated as soon as the text changes.
        # Useful to avoid recomputing while scrolling.
        self.current_cell = None

        def reset_current_cell():
            self.current_cell = None

        self.textChanged.connect(reset_current_cell)
Пример #34
0
class MOSVizViewer(DataViewer):

    LABEL = "MOSViz Viewer"
    window_closed = Signal()
    _toolbar_cls = MOSViewerToolbar

    def __init__(self, session, parent=None):
        super(MOSVizViewer, self).__init__(session, parent=parent)
        self.load_ui()

        # Define some data containers
        self.filepath = None
        self.savepath = None
        self.data_idx = None
        self.comments = False
        self.textChangedAt = None
        self.mask = None

        self.catalog = None
        self.current_row = None
        self._specviz_instance = None
        self._loaded_data = {}
        self._primary_data = None
        self._layer_view = SimpleLayerWidget(parent=self)
        self._layer_view.layer_combo.currentIndexChanged.connect(
            self._selection_changed)
        self.resize(800, 600)

    def load_ui(self):
        """
        Setup the MOSView viewer interface.
        """
        self.central_widget = QWidget(self)

        path = os.path.join(UI_DIR, 'mos_widget.ui')
        loadUi(path, self.central_widget)

        self.image_widget = DrawableImageWidget()
        self.spectrum2d_widget = MOSImageWidget()
        self.spectrum1d_widget = Line1DWidget()

        # Set up helper for sharing axes. SharedAxisHelper defaults to no sharing
        # and we control the sharing later by setting .sharex and .sharey on the
        # helper
        self.spectrum2d_spectrum1d_share = SharedAxisHelper(
            self.spectrum2d_widget._axes, self.spectrum1d_widget._axes)
        self.spectrum2d_image_share = SharedAxisHelper(
            self.spectrum2d_widget._axes, self.image_widget._axes)

        # We only need to set the image widget to keep the same aspect ratio
        # since the two other viewers don't require square pixels, so the axes
        # should not change shape.
        self.image_widget._axes.set_adjustable('datalim')

        self.meta_form_layout = self.central_widget.meta_form_layout
        self.meta_form_layout.setFieldGrowthPolicy(
            self.meta_form_layout.ExpandingFieldsGrow)
        self.central_widget.left_vertical_splitter.insertWidget(
            0, self.image_widget)
        self.central_widget.right_vertical_splitter.addWidget(
            self.spectrum2d_widget)
        self.central_widget.right_vertical_splitter.addWidget(
            self.spectrum1d_widget)

        # Set the splitter stretch factors
        self.central_widget.left_vertical_splitter.setStretchFactor(0, 1)
        self.central_widget.left_vertical_splitter.setStretchFactor(1, 8)

        self.central_widget.right_vertical_splitter.setStretchFactor(0, 1)
        self.central_widget.right_vertical_splitter.setStretchFactor(1, 2)

        self.central_widget.horizontal_splitter.setStretchFactor(0, 1)
        self.central_widget.horizontal_splitter.setStretchFactor(1, 2)

        # Keep the left and right splitters in sync otherwise the axes don't line up
        self.central_widget.left_vertical_splitter.splitterMoved.connect(
            self._left_splitter_moved)
        self.central_widget.right_vertical_splitter.splitterMoved.connect(
            self._right_splitter_moved)

        # Set the central widget
        self.setCentralWidget(self.central_widget)

        # Define the options widget
        self._options_widget = OptionsWidget()

    def show(self, *args, **kwargs):
        super(MOSVizViewer, self).show(*args, **kwargs)
        # Trigger a sync between the splitters
        self._left_splitter_moved()

    @avoid_circular
    def _right_splitter_moved(self, *args, **kwargs):
        sizes = self.central_widget.right_vertical_splitter.sizes()
        if sizes == [0, 0]:
            sizes = [230, 230]
        self.central_widget.left_vertical_splitter.setSizes(sizes)

    @avoid_circular
    def _left_splitter_moved(self, *args, **kwargs):
        sizes = self.central_widget.left_vertical_splitter.sizes()
        if sizes == [0, 0]:
            sizes = [230, 230]
        self.central_widget.right_vertical_splitter.setSizes(sizes)

    def setup_connections(self):
        """
        Connects gui elements to event calls.
        """
        # Connect the selection event for the combo box to what's displayed
        self.toolbar.source_select.currentIndexChanged[int].connect(
            lambda ind: self.load_selection(self.catalog[ind]))

        self.toolbar.source_select.currentIndexChanged[int].connect(
            lambda ind: self._set_navigation(ind))

        # Connect the specviz button
        if SpecVizViewer is not None:
            self.toolbar.open_specviz.triggered.connect(
                lambda: self._open_in_specviz())
        else:
            self.toolbar.open_specviz.setDisabled(True)

        # Connect previous and forward buttons
        self.toolbar.cycle_next_action.triggered.connect(
            lambda: self._set_navigation(self.toolbar.source_select.
                                         currentIndex() + 1))

        # Connect previous and previous buttons
        self.toolbar.cycle_previous_action.triggered.connect(
            lambda: self._set_navigation(self.toolbar.source_select.
                                         currentIndex() - 1))

        # Connect the toolbar axes setting actions
        self.toolbar.lock_x_action.triggered.connect(
            lambda state: self.set_locked_axes(x=state))

        self.toolbar.lock_y_action.triggered.connect(
            lambda state: self.set_locked_axes(y=state))

    def options_widget(self):
        return self._options_widget

    def initialize_toolbar(self):
        """
        Initialize the custom toolbar for the MOSViz viewer.
        """
        from glue.config import viewer_tool

        self.toolbar = self._toolbar_cls(self)

        for tool_id in self.tools:
            mode_cls = viewer_tool.members[tool_id]
            mode = mode_cls(self)
            self.toolbar.add_tool(mode)

        self.addToolBar(self.toolbar)

        self.setup_connections()

    def register_to_hub(self, hub):
        super(MOSVizViewer, self).register_to_hub(hub)

        def has_data_or_subset(x):
            if x.sender is self._primary_data:
                return True
            elif isinstance(x.sender,
                            Subset) and x.sender.data is self._primary_data:
                return True
            else:
                return False

        hub.subscribe(self,
                      msg.SubsetCreateMessage,
                      handler=self._add_subset,
                      filter=has_data_or_subset)

        hub.subscribe(self,
                      msg.SubsetUpdateMessage,
                      handler=self._update_subset,
                      filter=has_data_or_subset)

        hub.subscribe(self,
                      msg.SubsetDeleteMessage,
                      handler=self._remove_subset,
                      filter=has_data_or_subset)

        hub.subscribe(self,
                      msg.DataUpdateMessage,
                      handler=self._update_data,
                      filter=has_data_or_subset)

    def add_data(self, data):
        """
        Processes data message from the central communication hub.

        Parameters
        ----------
        data : :class:`glue.core.data.Data`
            Data object.
        """

        # Check whether the data is suitable for the MOSViz viewer - basically
        # we expect a table of 1D columns with at least three string and four
        # floating-point columns.

        if data.ndim != 1:
            QMessageBox.critical(self,
                                 "Error", "MOSViz viewer can only be used "
                                 "for data with 1-dimensional components",
                                 buttons=QMessageBox.Ok)
            return False

        components = [
            data.get_component(cid) for cid in data.visible_components
        ]

        categorical = [c for c in components if c.categorical]
        if len(categorical) < 3:
            QMessageBox.critical(
                self,
                "Error", "MOSViz viewer expected at least "
                "three string components/columns, representing "
                "the filenames of the 1D and 2D spectra and "
                "cutouts",
                buttons=QMessageBox.Ok)
            return False

        # We can relax the following requirement if we make the slit parameters
        # optional
        numerical = [c for c in components if c.numeric]
        if len(numerical) < 4:
            QMessageBox.critical(
                self,
                "Error", "MOSViz viewer expected at least "
                "four numerical components/columns, representing "
                "the slit position, length, and position angle",
                buttons=QMessageBox.Ok)
            return False

        # Make sure the loaders and column names are correct
        result = confirm_loaders_and_column_names(data)
        if not result:
            return False

        self._primary_data = data
        self._layer_view.data = data
        self._unpack_selection(data)
        return True

    def add_subset(self, subset):
        """
        Processes subset messages from the central communication hub.

        Parameters
        ----------
        subset :
            Subset object.
        """
        self._layer_view.refresh()
        index = self._layer_view.layer_combo.findData(subset)
        self._layer_view.layer_combo.setCurrentIndex(index)
        return True

    def _update_data(self, message):
        """
        Update data message.

        Parameters
        ----------
        message : :class:`glue.core.message.Message`
            Data message object.
        """
        self._layer_view.refresh()

    def _add_subset(self, message):
        """
        Add subset message.

        Parameters
        ----------
        message : :class:`glue.core.message.Message`
            Subset message object.
        """
        self._layer_view.refresh()

    def _update_subset(self, message):
        """
        Update subset message.

        Parameters
        ----------
        message : :class:`glue.core.message.Message`
            Update message object.
        """
        self._layer_view.refresh()
        self._unpack_selection(message.subset)

    def _remove_subset(self, message):
        """
        Remove subset message.

        Parameters
        ----------
        message : :class:`glue.core.message.Message`
            Subset message object.
        """
        self._layer_view.refresh()
        self._unpack_selection(message.subset.data)

    def _selection_changed(self):
        self._unpack_selection(self._layer_view.layer_combo.currentData())

    def _unpack_selection(self, data):
        """
        Interprets the :class:`glue.core.data.Data` object by decomposing the
        data elements, extracting relevant data, and recomposing a
        package-agnostic dictionary object containing the relevant data.

        Parameters
        ----------
        data : :class:`glue.core.data.Data`
            Glue data object to decompose.

        """

        mask = None

        if isinstance(data, Subset):
            try:
                mask = data.to_mask()
            except IncompatibleAttribute:
                return

            if not np.any(mask):
                return

            data = data.data
        self.mask = mask

        # Clear the table
        self.catalog = Table()
        self.catalog.meta = data.meta

        self.comments = False
        col_names = data.components
        for att in col_names:
            cid = data.id[att]
            component = data.get_component(cid)

            if component.categorical:
                comp_labels = component.labels[mask]

                if comp_labels.ndim > 1:
                    comp_labels = comp_labels[0]

                if str(att) in ["comments", "flag"]:
                    self.comments = True
                elif str(att) in ['spectrum1d', 'spectrum2d', 'cutout']:
                    self.filepath = component._load_log.path
                    path = '/'.join(component._load_log.path.split('/')[:-1])
                    self.catalog[str(att)] = [
                        os.path.join(path, x) for x in comp_labels
                    ]
                else:
                    self.catalog[str(att)] = comp_labels
            else:
                comp_data = component.data[mask]

                if comp_data.ndim > 1:
                    comp_data = comp_data[0]

                self.catalog[str(att)] = comp_data

        if len(self.catalog) > 0:
            if not self.comments:
                self.comments = self._load_comments(data.label)  #Returns bool
            else:
                self._data_collection_index(data.label)
                self._get_save_path()
            # Update gui elements
            self._update_navigation(select=0)

    def _update_navigation(self, select=0):
        """
        Updates the :class:`qtpy.QtWidgets.QComboBox` widget with the
        appropriate source `id`s from the MOS catalog.
        """

        if self.toolbar is None:
            return

        self.toolbar.source_select.blockSignals(True)

        self.toolbar.source_select.clear()

        if len(self.catalog) > 0 and self.catalog.meta["special_columns"][
                "source_id"] in self.catalog.colnames:
            self.toolbar.source_select.addItems(self.catalog[
                self.catalog.meta["special_columns"]["source_id"]][:])

        self.toolbar.source_select.setCurrentIndex(select)

        self.toolbar.source_select.blockSignals(False)

        self.toolbar.source_select.currentIndexChanged.emit(select)

    def _set_navigation(self, index):

        if len(self.catalog) < index:
            return

        if 0 <= index < self.toolbar.source_select.count():
            self.toolbar.source_select.setCurrentIndex(index)

        if index <= 0:
            self.toolbar.cycle_previous_action.setDisabled(True)
        else:
            self.toolbar.cycle_previous_action.setDisabled(False)

        if index >= self.toolbar.source_select.count() - 1:
            self.toolbar.cycle_next_action.setDisabled(True)
        else:
            self.toolbar.cycle_next_action.setDisabled(False)

    def _open_in_specviz(self):
        _specviz_instance = self.session.application.new_data_viewer(
            SpecVizViewer)

        spec1d_data = self._loaded_data['spectrum1d']

        spec_data = Spectrum1DRef(
            data=spec1d_data.get_component(spec1d_data.id['Flux']).data,
            dispersion=spec1d_data.get_component(
                spec1d_data.id['Wavelength']).data,
            uncertainty=StdDevUncertainty(
                spec1d_data.get_component(spec1d_data.id['Uncertainty']).data),
            unit="",
            name=self.current_row['id'],
            wcs=WCS(spec1d_data.header))

        _specviz_instance.open_data(spec_data)

    def load_selection(self, row):
        """
        Processes a row in the MOS catalog by first loading the data set,
        updating the stored data components, and then rendering the data on
        the visible MOSViz viewer plots.

        Parameters
        ----------
        row : :class:`astropy.table.Row`
            A row object representing a row in the MOS catalog. Each key
            should be a column name.
        """

        self.current_row = row

        # Get loaders
        loader_spectrum1d = SPECTRUM1D_LOADERS[self.catalog.meta["loaders"]
                                               ["spectrum1d"]]
        loader_spectrum2d = SPECTRUM2D_LOADERS[self.catalog.meta["loaders"]
                                               ["spectrum2d"]]
        loader_cutout = CUTOUT_LOADERS[self.catalog.meta["loaders"]["cutout"]]

        # Get column names
        colname_spectrum1d = self.catalog.meta["special_columns"]["spectrum1d"]
        colname_spectrum2d = self.catalog.meta["special_columns"]["spectrum2d"]
        colname_cutout = self.catalog.meta["special_columns"]["cutout"]

        spec1d_data = loader_spectrum1d(row[colname_spectrum1d])
        spec2d_data = loader_spectrum2d(row[colname_spectrum2d])

        self._update_data_components(spec1d_data, key='spectrum1d')
        self._update_data_components(spec2d_data, key='spectrum2d')

        basename = os.path.basename(row[colname_cutout])
        if basename == "None":
            self.render_data(row, spec1d_data, spec2d_data, None)
        else:
            image_data = loader_cutout(row[colname_cutout])
            self._update_data_components(image_data, key='cutout')
            self.render_data(row, spec1d_data, spec2d_data, image_data)

    def _update_data_components(self, data, key):
        """
        Update the data components that act as containers for the displayed
        data in the MOSViz viewer. This obviates the need to keep creating new
        data components.

        Parameters
        ----------
        data : :class:`glue.core.data.Data`
            Data object to replace within the component.
        key : str
            References the particular data set type.
        """
        cur_data = self._loaded_data.get(key, None)

        if cur_data is None:
            self._loaded_data[key] = data
            self.session.data_collection.append(data)
        else:
            cur_data.update_values_from_data(data)

    def render_data(self,
                    row,
                    spec1d_data=None,
                    spec2d_data=None,
                    image_data=None):
        """
        Render the updated data sets in the individual plot widgets within the
        MOSViz viewer.
        """
        self._check_unsaved_comments()

        if spec1d_data is not None:

            spectrum1d_x = spec1d_data[spec1d_data.id['Wavelength']]
            spectrum1d_y = spec1d_data[spec1d_data.id['Flux']]
            spectrum1d_yerr = spec1d_data[spec1d_data.id['Uncertainty']]

            self.spectrum1d_widget.set_data(x=spectrum1d_x,
                                            y=spectrum1d_y,
                                            yerr=spectrum1d_yerr)

            # Try to retrieve the wcs information
            try:
                flux_unit = spec1d_data.header.get('BUNIT', 'Jy').lower()
                flux_unit = flux_unit.replace('counts', 'count')
                flux_unit = u.Unit(flux_unit)
            except ValueError:
                flux_unit = u.Unit("Jy")

            try:
                disp_unit = spec1d_data.header.get('CUNIT1',
                                                   'Angstrom').lower()
                disp_unit = u.Unit(disp_unit)
            except ValueError:
                disp_unit = u.Unit("Angstrom")

            self.spectrum1d_widget.axes.set_xlabel(
                "Wavelength [{}]".format(disp_unit))
            self.spectrum1d_widget.axes.set_ylabel(
                "Flux [{}]".format(flux_unit))

        if image_data is not None:
            wcs = image_data.coords.wcs

            self.image_widget.set_image(image_data.get_component(
                image_data.id['Flux']).data,
                                        wcs=wcs,
                                        interpolation='none',
                                        origin='lower')

            self.image_widget.axes.set_xlabel("Spatial X")
            self.image_widget.axes.set_ylabel("Spatial Y")

            # Add the slit patch to the plot

            ra = row[self.catalog.meta["special_columns"]
                     ["slit_ra"]] * u.degree
            dec = row[self.catalog.meta["special_columns"]
                      ["slit_dec"]] * u.degree
            slit_width = row[self.catalog.meta["special_columns"]
                             ["slit_width"]]
            slit_length = row[self.catalog.meta["special_columns"]
                              ["slit_length"]]

            skycoord = SkyCoord(ra, dec, frame='fk5')
            xp, yp = skycoord.to_pixel(wcs)

            scale = np.sqrt(proj_plane_pixel_area(wcs)) * 3600.

            dx = slit_width / scale
            dy = slit_length / scale

            self.image_widget.draw_rectangle(x=xp, y=yp, width=dx, height=dy)

            self.image_widget._redraw()
        else:
            self.image_widget.setVisible(False)

        # Plot the 2D spectrum data last because by then we can make sure that
        # we set up the extent of the image appropriately if the cutout and the
        # 1D spectrum are present so that the axes can be locked.

        if spec2d_data is not None:
            wcs = spec2d_data.coords.wcs

            xp2d = np.arange(spec2d_data.shape[1])
            yp2d = np.repeat(0, spec2d_data.shape[1])
            spectrum2d_disp, spectrum2d_offset = spec2d_data.coords.pixel2world(
                xp2d, yp2d)
            x_min = spectrum2d_disp.min()
            x_max = spectrum2d_disp.max()

            if image_data is None:
                y_min = -0.5
                y_max = spec2d_data.shape[0] - 0.5
            else:
                y_min = yp - dy / 2.
                y_max = yp + dy / 2.

            extent = [x_min, x_max, y_min, y_max]

            self.spectrum2d_widget.set_image(image=spec2d_data.get_component(
                spec2d_data.id['Flux']).data,
                                             interpolation='none',
                                             aspect='auto',
                                             extent=extent,
                                             origin='lower')

            self.spectrum2d_widget.axes.set_xlabel("Wavelength")
            self.spectrum2d_widget.axes.set_ylabel("Spatial Y")

            self.spectrum2d_widget._redraw()

        # Clear the meta information widget
        # NOTE: this process is inefficient
        for i in range(self.meta_form_layout.count()):
            wid = self.meta_form_layout.itemAt(i).widget()
            label = self.meta_form_layout.labelForField(wid)

            if label is not None:
                label.deleteLater()

            wid.deleteLater()

        # Repopulate the form layout
        # NOTE: this process is inefficient
        for col in row.colnames:
            if col.lower() not in ["comments", "flag"]:
                line_edit = QLineEdit(str(row[col]),
                                      self.central_widget.meta_form_widget)
                line_edit.setReadOnly(True)

                self.meta_form_layout.addRow(col, line_edit)

        # Set up comment and flag input/display boxes
        if self.comments:
            if self.savepath is not None:
                if self.savepath == -1:
                    line_edit = QLineEdit(
                        os.path.basename("Not Saving to File."),
                        self.central_widget.meta_form_widget)
                    line_edit.setReadOnly(True)
                    self.meta_form_layout.addRow("Save File", line_edit)
                else:
                    line_edit = QLineEdit(os.path.basename(self.savepath),
                                          self.central_widget.meta_form_widget)
                    line_edit.setReadOnly(True)
                    self.meta_form_layout.addRow("Save File", line_edit)

            self.input_flag = QLineEdit(self.get_flag(),
                                        self.central_widget.meta_form_widget)
            self.input_flag.textChanged.connect(self._text_changed)
            self.input_flag.setStyleSheet(
                "background-color: rgba(255, 255, 255);")
            self.meta_form_layout.addRow("Flag", self.input_flag)

            self.input_comments = QPlainTextEdit(
                self.get_comment(), self.central_widget.meta_form_widget)
            self.input_comments.textChanged.connect(self._text_changed)
            self.input_comments.setStyleSheet(
                "background-color: rgba(255, 255, 255);")
            self.meta_form_layout.addRow("Comments", self.input_comments)

            self.input_save = QPushButton('Save',
                                          self.central_widget.meta_form_widget)
            self.input_save.clicked.connect(self.update_comments)
            self.input_save.setDefault(True)

            self.input_refresh = QPushButton(
                'Reload', self.central_widget.meta_form_widget)
            self.input_refresh.clicked.connect(self.refresh_comments)

            self.meta_form_layout.addRow(self.input_save, self.input_refresh)

    @defer_draw
    def set_locked_axes(self, x=None, y=None):

        # Here we only change the setting if x or y are not None
        # since if set_locked_axes is called with eg. x=True, then
        # we shouldn't change the y setting.

        if x is not None:
            self.spectrum2d_spectrum1d_share.sharex = x

        if y is not None:
            self.spectrum2d_image_share.sharey = y

        self.spectrum1d_widget._redraw()
        self.spectrum2d_widget._redraw()
        self.image_widget._redraw()

    def layer_view(self):
        return self._layer_view

    def _text_changed(self):
        if self.textChangedAt is None:
            i = self.toolbar.source_select.currentIndex()
            self.textChangedAt = self._index_hash(i)

    def _check_unsaved_comments(self):
        if self.textChangedAt is None:
            return  #Nothing to be changed
        i = self.toolbar.source_select.currentIndex()
        i = self._index_hash(i)
        if self.textChangedAt == i:
            self.textChangedAt = None
            return  #This is a refresh
        info = "Comments or flags changed but were not saved. Would you like to save them?"
        reply = QMessageBox.question(self, '', info,
                                     QMessageBox.Yes | QMessageBox.No)
        if reply == QMessageBox.Yes:
            self.update_comments(True)
        self.textChangedAt = None

    def _data_collection_index(self, label):
        idx = -1
        for i, l in enumerate(self.session.data_collection):
            if l.label == label:
                idx = i
                break
        if idx == -1:
            return -1
        self.data_idx = idx
        return idx

    def _index_hash(self, i):
        """Local selection index -> Table index"""
        if self.mask is not None:
            size = self.mask.size
            temp = np.arange(size)
            return temp[self.mask][i]
        else:
            return i

    def _id_to_index_hash(self, ID, l):
        """Object Name -> Table index"""
        for i, name in enumerate(l):
            if name == ID:
                return i
        return None

    def get_comment(self):
        idx = self.data_idx
        i = self.toolbar.source_select.currentIndex()
        i = self._index_hash(i)
        comp = self.session.data_collection[idx].get_component("comments")
        return comp.labels[i]

    def get_flag(self):
        idx = self.data_idx
        i = self.toolbar.source_select.currentIndex()
        i = self._index_hash(i)
        comp = self.session.data_collection[idx].get_component("flag")
        return comp.labels[i]

    def send_NumericalDataChangedMessage(self):
        idx = self.data_idx
        data = self.session.data_collection[idx]
        data.hub.broadcast(msg.NumericalDataChangedMessage(data, "comments"))

    def refresh_comments(self):
        self.input_flag.setText(self.get_flag())
        self.input_comments.setPlainText(self.get_comment())
        self.input_flag.setStyleSheet("background-color: rgba(255, 255, 255);")
        self.textChangedAt = None

    def _get_save_path(self):
        """
        Try to get save path from other MOSVizViewer instances
        """
        for v in self.session.application.viewers[0]:
            if isinstance(v, MOSVizViewer):
                if v.savepath is not None:
                    if v.data_idx == self.data_idx:
                        self.savepath = v.savepath
                        break

    def _setup_save_path(self):
        """
        Prompt the user for a file to save comments and flags into.
        """
        fail = True
        success = False
        info = "Where would you like to save comments and flags?"
        option = pick_item(
            [0, 1], [os.path.basename(self.filepath), "New MOSViz Table file"],
            label=info,
            title="Comment Setup")
        if option == 0:
            self.savepath = self.filepath
        elif option == 1:
            dirname = os.path.dirname(self.filepath)
            path = compat.getsavefilename(caption="New MOSViz Table File",
                                          basedir=dirname,
                                          filters="*.txt")[0]
            if path == "":
                return fail
            self.savepath = path
        else:
            return fail

        for v in self.session.application.viewers[0]:
            if isinstance(v, MOSVizViewer):
                if v.data_idx == self.data_idx:
                    v.savepath = self.savepath
        self._layer_view.refresh()
        return success

    def update_comments(self, pastSelection=False):
        """
        Process comment and flag changes and save to file.

        Parameters
        ----------
        pastSelection : bool
            True when updating past selections. Used when
            user forgets to save.
        """
        if self.input_flag.text() == "":
            self.input_flag.setStyleSheet("background-color: rgba(255, 0, 0);")
            return

        i = None
        try:
            i = int(self.input_flag.text())
        except ValueError:
            self.input_flag.setStyleSheet("background-color: rgba(255, 0, 0);")
            info = QMessageBox.information(self, "Status:",
                                           "Flag must be an int!")
            return
        self.input_flag.setStyleSheet("background-color: rgba(255, 255, 255);")

        idx = self.data_idx
        if pastSelection:
            i = self.textChangedAt
            self.textChangedAt = None
        else:
            i = self.toolbar.source_select.currentIndex()
            i = self._index_hash(i)
        data = self.session.data_collection[idx]

        comp = data.get_component("comments")
        comp.labels.flags.writeable = True
        comp.labels[i] = self.input_comments.toPlainText()

        comp = data.get_component("flag")
        comp.labels.flags.writeable = True
        comp.labels[i] = self.input_flag.text()

        self.send_NumericalDataChangedMessage()
        self.write_comments()

        self.textChangedAt = None

    def _load_comments(self, label):
        """
        Populate the comments and flag columns.
        Attempt to load comments from file.

        Parameters
        ----------
        label : str
            The label of the data in
            session.data_collection.
        """

        #Make sure its the right data
        #(beacuse subset data is masked)
        idx = self._data_collection_index(label)
        if idx == -1:
            return False
        data = self.session.data_collection[idx]

        #Fill in default comments:
        length = data.shape[0]
        new_comments = np.array(["" for i in range(length)], dtype=object)
        new_flags = np.array(["0" for i in range(length)], dtype=object)

        #Fill in any saved comments:
        meta = data.meta
        obj_names = data.get_component(
            self.catalog.meta["special_columns"]["source_id"]).labels

        if "MOSViz_comments" in meta.keys():
            try:
                comments = meta["MOSViz_comments"]
                for key in comments.keys():
                    index = self._id_to_index_hash(key, obj_names)
                    if index is not None:
                        line = comments[key]
                        new_comments[index] = line
            except Exception as e:
                print("MOSViz Comment Load Failed: ", e)

        if "MOSViz_flags" in meta.keys():
            try:
                flags = meta["MOSViz_flags"]
                for key in flags.keys():
                    index = self._id_to_index_hash(key, obj_names)
                    if index is not None:
                        line = flags[key]
                        new_flags[index] = line
            except Exception as e:
                print("MOSViz Flag Load Failed: ", e)

        #Send to DC
        data.add_component(CategoricalComponent(new_flags, "flag"), "flag")
        data.add_component(CategoricalComponent(new_comments, "comments"),
                           "comments")
        return True

    def write_comments(self):
        """
        Setup save file. Write comments and flags to file
        """

        if self.savepath is None:
            fail = self._setup_save_path()
            if fail: return
        if self.savepath == -1:
            return  #Do not save to file option

        idx = self.data_idx
        data = self.session.data_collection[idx]
        save_comments = data.get_component("comments").labels
        save_flag = data.get_component("flag").labels
        obj_names = data.get_component(
            self.catalog.meta["special_columns"]["source_id"]).labels

        fn = self.savepath
        folder = os.path.dirname(fn)

        t = astropy_table.data_to_astropy_table(data)

        #Check if load and save dir paths match
        temp = os.path.dirname(self.filepath)
        if not os.path.samefile(folder, temp):
            t['spectrum1d'].flags.writeable = True
            t['spectrum2d'].flags.writeable = True
            t['cutout'].flags.writeable = True
            for i in range(len(t)):
                t['spectrum1d'][i] = os.path.abspath(t['spectrum1d'][i])
                t['spectrum2d'][i] = os.path.abspath(t['spectrum2d'][i])
                t['cutout'][i] = os.path.abspath(t['cutout'][i])
        try:
            t.remove_column("comments")
            t.remove_column("flag")

            keys = t.meta.keys()

            if "MOSViz_comments" in keys:
                t.meta.pop("MOSViz_comments")

            if "MOSViz_flags" in keys:
                t.meta.pop("MOSViz_flags")

            comments = OrderedDict()
            flags = OrderedDict()

            for i, line in enumerate(save_comments):
                if line != "":
                    line = line.replace("\n", " ")
                    key = str(obj_names[i])
                    comments[key] = line

            for i, line in enumerate(save_flag):
                if line != "0" and line != "":
                    line = com.replace("\n", " ")
                    key = str(obj_names[i])
                    flags[key] = line

            if len(comments) > 0:
                t.meta["MOSViz_comments"] = comments
            if len(flags) > 0:
                t.meta["MOSViz_flags"] = flags

            t.write(fn, format="ascii.ecsv", overwrite=True)
        except Exception as e:
            print("Comment write failed:", e)

    def closeEvent(self, event):
        """
        Clean up the extraneous data components created when opening the
        MOSViz viewer by overriding the parent class's close event.
        """
        super(MOSVizViewer, self).closeEvent(event)

        for data in self._loaded_data.values():
            self.session.data_collection.remove(data)
Пример #35
0
class FileDialog(QDialog):
    def __init__(self,
                 file_name,
                 job_name,
                 job_number,
                 realization,
                 iteration,
                 parent=None):
        super().__init__(parent)

        self.setWindowTitle(
            f"{job_name} # {job_number} "
            f"Realization: {realization} Iteration: {iteration}")

        try:
            self._file = open(file_name, "r")
        except OSError as error:
            self._mb = QMessageBox(
                QMessageBox.Critical,
                "Error opening file",
                error.strerror,
                QMessageBox.Ok,
                self,
            )
            self._mb.finished.connect(self.accept)
            self._mb.show()
            return

        self._view = QPlainTextEdit()
        self._view.setReadOnly(True)
        self._view.setWordWrapMode(QTextOption.NoWrap)
        # for moving the actual slider
        self._view.verticalScrollBar().sliderMoved.connect(self._update_cursor)
        # for mouse wheel and keyboard arrows
        self._view.verticalScrollBar().valueChanged.connect(
            self._update_cursor)

        self._view.setFont(QFontDatabase.systemFont(QFontDatabase.FixedFont))

        self._follow_mode = False

        self._init_layout()
        self._init_thread()

        self.show()

    @Slot()
    def _stop_thread(self):
        self._thread.quit()
        self._thread.wait()

    def _init_layout(self):
        self.setMinimumWidth(600)
        self.setMinimumHeight(400)

        dialog_buttons = QDialogButtonBox(QDialogButtonBox.Ok)
        dialog_buttons.accepted.connect(self.accept)

        self._copy_all_button = dialog_buttons.addButton(
            "Copy All", QDialogButtonBox.ActionRole)
        self._copy_all_button.clicked.connect(self._copy_all)

        self._follow_button = dialog_buttons.addButton(
            "Follow", QDialogButtonBox.ActionRole)
        self._follow_button.setCheckable(True)
        self._follow_button.toggled.connect(self._enable_follow_mode)
        self._enable_follow_mode(self._follow_mode)

        layout = QVBoxLayout(self)
        layout.addWidget(self._view)
        layout.addWidget(dialog_buttons)

    def _init_thread(self):
        self._thread = QThread()

        self._worker = FileUpdateWorker(self._file)
        self._worker.moveToThread(self._thread)
        self._worker.read.connect(self._append_text)

        self._thread.started.connect(self._worker.setup)
        self._thread.finished.connect(self._worker.stop)
        self._thread.finished.connect(self._worker.deleteLater)
        self.finished.connect(self._stop_thread)

        self._thread.start()

    def _copy_all(self) -> None:
        text = self._view.toPlainText()
        QApplication.clipboard().setText(text, QClipboard.Clipboard)
        pass

    def _update_cursor(self, value: int) -> None:
        if not self._view.textCursor().hasSelection():
            block = self._view.document().findBlockByLineNumber(value)
            cursor = QTextCursor(block)
            self._view.setTextCursor(cursor)

    def _enable_follow_mode(self, enable: bool) -> None:
        if enable:
            self._view.moveCursor(QTextCursor.End)
            self._view.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
            self._view.verticalScrollBar().setDisabled(True)
            self._view.setTextInteractionFlags(Qt.NoTextInteraction)
            self._follow_mode = True
        else:
            self._view.verticalScrollBar().setDisabled(False)
            self._view.setVerticalScrollBarPolicy(Qt.ScrollBarAsNeeded)
            self._view.setTextInteractionFlags(Qt.TextSelectableByMouse
                                               | Qt.TextSelectableByKeyboard)
            self._follow_mode = False

    def _append_text(self, text: str) -> None:
        # Remove trailing newline as appendPlainText adds this
        if text[-1:] == "\n":
            text = text[:-1]
        if self._follow_mode:
            self._view.moveCursor(QTextCursor.End)
        self._view.appendPlainText(text)