class CalibrationConfigWidget(QObject):
    """Emitted when GUI data has changed"""
    gui_data_changed = Signal()

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

        self.cfg = HexrdConfig()

        loader = UiLoader()
        self.ui = loader.load_file('calibration_config_widget.ui', parent)

        self.detector_widgets_disabled = False

        # Turn off autocomplete for the QComboBox
        self.ui.cal_det_current.setCompleter(None)

        self.setup_connections()

        self.timer = None

    def setup_connections(self):
        self.ui.cal_det_current.installEventFilter(self)
        self.ui.cal_energy.valueChanged.connect(self.on_energy_changed)
        self.ui.cal_energy_wavelength.valueChanged.connect(
            self.on_energy_wavelength_changed)

        self.ui.cal_det_current.currentIndexChanged.connect(
            self.on_detector_changed)
        self.ui.cal_det_current.lineEdit().editingFinished.connect(
            self.on_detector_name_edited)
        self.ui.cal_det_remove.clicked.connect(self.on_detector_remove_clicked)
        self.ui.cal_det_add.clicked.connect(self.on_detector_add_clicked)

        all_widgets = self.get_all_widgets()
        skip_widgets = ['cal_det_current', 'cal_det_add', 'cal_det_remove']
        for widget in all_widgets:
            if widget.objectName() in skip_widgets:
                continue

            if isinstance(widget, QComboBox):
                widget.currentIndexChanged.connect(self.update_config_from_gui)
            elif isinstance(widget, QLineEdit):
                widget.textEdited.connect(self.update_config_from_gui)
            elif isinstance(widget, QPushButton):
                widget.pressed.connect(self.update_config_from_gui)
            else:
                widget.valueChanged.connect(self.update_config_from_gui)
                widget.valueChanged.connect(self.gui_value_changed)

        self.ui.cal_det_function.currentIndexChanged.connect(
            self.update_gui_from_config)
        self.ui.cal_det_buffer.clicked.connect(self._on_configure_buffer)

    def on_energy_changed(self):
        val = self.ui.cal_energy.value()

        # Make sure energy has same style
        kev_widget = getattr(self.ui, 'cal_energy')
        wave_widget = getattr(self.ui, 'cal_energy_wavelength')
        wave_widget.setStyleSheet(kev_widget.styleSheet())

        block_signals = self.ui.cal_energy_wavelength.blockSignals(True)
        try:
            new_wavelength = constants.KEV_TO_WAVELENGTH / val
            self.ui.cal_energy_wavelength.setValue(new_wavelength)
        finally:
            self.ui.cal_energy_wavelength.blockSignals(block_signals)

    def on_energy_wavelength_changed(self):
        val = self.ui.cal_energy_wavelength.value()

        block_signals = self.ui.cal_energy.blockSignals(True)
        try:
            new_energy = constants.WAVELENGTH_TO_KEV / val
            self.ui.cal_energy.setValue(new_energy)
        finally:
            self.ui.cal_energy.blockSignals(block_signals)

    def on_detector_changed(self):
        self.update_detector_from_config()

    def enable_detector_widgets(self, enable=True):
        detector_widgets = self.cfg.get_detector_widgets()
        detector_widgets.remove('cal_det_add')
        for widget in detector_widgets:
            gui_widget = getattr(self.ui, widget)
            gui_widget.setEnabled(enable)

        self.detector_widgets_disabled = not enable

    def on_detector_name_edited(self):
        new_name = self.ui.cal_det_current.currentText()
        detector_names = self.cfg.detector_names

        # Ignore it if there is already a detector with this name
        if new_name in detector_names:
            return

        # Get the old name from the underlying item
        idx = self.ui.cal_det_current.currentIndex()
        old_name = self.ui.cal_det_current.itemText(idx)

        self.ui.cal_det_current.setItemText(idx, new_name)
        self.cfg.rename_detector(old_name, new_name)

    def on_detector_remove_clicked(self):
        if self.ui.cal_det_current.count() <= 1:
            msg = 'Cannot remove last detector'
            QMessageBox.critical(self.ui, 'HEXRD', msg)
            return

        current_detector = self.get_current_detector()
        idx = self.ui.cal_det_current.currentIndex()

        self.cfg.remove_detector(current_detector)
        if idx > 0:
            self.ui.cal_det_current.setCurrentIndex(idx - 1)
        else:
            self.update_detector_from_config()

    def on_detector_add_clicked(self):
        combo = self.ui.cal_det_current
        current_detector = self.get_current_detector()
        detector_names = self.cfg.detector_names
        new_detector_name_base = 'detector_'
        for i in range(1000000):
            new_detector_name = new_detector_name_base + str(i + 1)
            if new_detector_name not in detector_names:
                self.cfg.add_detector(new_detector_name, current_detector)
                self.update_detector_from_config()
                new_ind = combo.findText(new_detector_name)
                combo.setCurrentIndex(new_ind)
                return

    def update_config_from_gui(self):
        """This function only updates the sender value"""
        sender = self.sender()
        name = sender.objectName()
        if name == 'cal_energy_wavelength':
            # Need to update the `cal_energy` entry instead
            sender = self.ui.cal_energy
            name = sender.objectName()

        value = self._get_gui_value(sender)
        current_detector = self.get_current_detector()
        self.cfg.set_val_from_widget_name(name, value, current_detector)

    def gui_value_changed(self):
        """We only want to emit a changed signal once editing is done"""
        if self.timer is None:
            self.timer = QTimer()
            self.timer.setSingleShot(True)
            self.timer.timeout.connect(self.gui_data_changed)

        # Start or restart our timer...
        self.timer.start(666)

    def update_gui_from_config(self):
        previously_blocked = self.block_all_signals()

        try:
            gui_yaml_paths = self.cfg.get_gui_yaml_paths()
            for var, path in gui_yaml_paths:
                if 'detector_name' in path:
                    # We give these special treatment
                    continue

                gui_var = getattr(self.ui, var)
                config_val = self.cfg.get_instrument_config_val(path)
                path[path.index('value')] = 'status'
                status_val = self.cfg.get_instrument_config_val(path)

                self._set_gui_value(gui_var, config_val, status_val)
        finally:
            self.unblock_all_signals(previously_blocked)

        # Update the wavelength also
        self.on_energy_changed()

        self.update_detector_from_config()

    def update_detector_from_config(self):
        previously_blocked = self.block_all_signals()

        try:
            combo_widget = self.ui.cal_det_current
            detector_names = self.cfg.detector_names
            if not detector_names:
                # Disable detector widgets if there is no valid detector
                if not self.detector_widgets_disabled:
                    self.enable_detector_widgets(enable=False)

                return
            elif self.detector_widgets_disabled:
                # Enable detector widgets if they were previously disabled
                self.enable_detector_widgets(enable=True)

            cur_detector = detector_names[0]
            if combo_widget.currentText() in detector_names:
                cur_detector = combo_widget.currentText()

            gui_yaml_paths = self.cfg.get_gui_yaml_paths(
                ['detectors', 'detector_name'])

            tilt_path = ['transform', 'tilt', 'value']
            dist_params_path = ['distortion', 'parameters']
            for var, path in gui_yaml_paths:
                if len(path) > 1 and path[:2] == dist_params_path:
                    # The distortion type should already be set before here
                    # Check for the number of params. If this is greater
                    # than the number of params, skip over it.
                    if not path[-1] < self.num_distortion_params:
                        continue

                gui_var = getattr(self.ui, var)
                full_path = ['detectors', cur_detector] + path
                config_val = self.cfg.get_instrument_config_val(full_path)
                full_path[full_path.index('value')] = 'status'
                status_val = self.cfg.get_instrument_config_val(full_path)

                if path[:-1] == tilt_path:
                    # Special treatment for tilt widgets
                    if HexrdConfig().rotation_matrix_euler() is None:
                        gui_var.setSuffix('')
                    else:
                        gui_var.setSuffix('°')
                        config_val = np.degrees(config_val).item()

                self._set_gui_value(gui_var, config_val, status_val)

            combo_items = []
            for i in range(combo_widget.count()):
                combo_items.append(combo_widget.itemText(i))

            if combo_items != detector_names:
                combo_widget.clear()
                combo_widget.addItems(detector_names)
                new_ind = combo_widget.findText(cur_detector)
                combo_widget.setCurrentIndex(new_ind)

        finally:
            self.unblock_all_signals(previously_blocked)

        # Update distortion parameter enable states
        self.update_distortion_params_enable_states()

    def get_current_detector(self):
        if self.detector_widgets_disabled:
            return None

        return self.ui.cal_det_current.currentText()

    def get_all_widgets(self):
        gui_yaml_paths = self.cfg.get_gui_yaml_paths()
        widgets = [x[0] for x in gui_yaml_paths]
        widgets += self.cfg.get_detector_widgets()
        widgets += ['cal_energy_wavelength']
        widgets = list(set(widgets))
        widgets = [getattr(self.ui, x) for x in widgets]
        return widgets

    def block_all_signals(self):
        previously_blocked = []
        all_widgets = self.get_all_widgets()

        for widget in all_widgets:
            previously_blocked.append(widget.blockSignals(True))

        return previously_blocked

    def unblock_all_signals(self, previously_blocked):
        all_widgets = self.get_all_widgets()

        for block, widget in zip(previously_blocked, all_widgets):
            widget.blockSignals(block)

    def _set_gui_value(self, gui_object, value, flag=None):
        """This is for calling various set methods for GUI variables

        For instance, QComboBox will call "setCurrentText", while
        QSpinBox will call "setValue".
        """
        if isinstance(gui_object, QComboBox):
            gui_object.setCurrentText(value)
        else:
            if flag == 1 and not gui_object.styleSheet():
                s = 'QSpinBox, QDoubleSpinBox { background-color: lightgray; }'
                gui_object.setStyleSheet(s)
            elif gui_object.styleSheet() and flag != 1:
                gui_object.setStyleSheet("")
            # If it is anything else, just assume setValue()
            gui_object.setValue(value)

    def _get_gui_value(self, gui_object):
        """This is for calling various get methods for GUI variables

        For instance, QComboBox will call "currentText", while
        QSpinBox will call "value".
        """
        if isinstance(gui_object, QComboBox):
            return gui_object.currentText()
        else:
            # If it is anything else, just assume value()
            return gui_object.value()

    @property
    def distortion(self):
        return self.ui.cal_det_function.currentText()

    @property
    def num_distortion_params(self):
        return HexrdConfig.num_distortion_parameters(self.distortion)

    def update_distortion_params_enable_states(self):
        num_params = self.num_distortion_params
        label_enabled = (num_params != 0)

        self.ui.distortion_parameters_label.setEnabled(label_enabled)
        self.ui.distortion_parameters_label.setVisible(label_enabled)

        base_str = 'cal_det_param_'
        for i in range(1000):
            widget_name = base_str + str(i)
            if not hasattr(self.ui, widget_name):
                break

            widget = getattr(self.ui, widget_name)
            enable = (num_params > i)
            widget.setEnabled(enable)
            widget.setVisible(enable)

    def eventFilter(self, target, event):
        # Unfortunately, when a user modifies the name in the editable
        # QComboBox 'cal_det_current', and then they press enter, it does
        # not emit QLineEdit.editingFinished(), but instead emits
        # QComboBox.currentIndexChanged(). This behavior is a little odd.
        # We need QLineEdit.editingFinished() so that the name gets updated.
        # If we call QLineEdit.editingFinished() here explicitly, it gets
        # emitted twice: once in this function, and once when the focus
        # gets cleared. Rather than calling it twice, let's just clear the
        # focus here so it gets called only once.
        if type(target) == QComboBox:
            if target.objectName() == 'cal_det_current':
                widget = self.ui.cal_det_current
                enter_keys = [Qt.Key_Return, Qt.Key_Enter]
                if type(event) == QKeyEvent and event.key() in enter_keys:
                    widget.lineEdit().clearFocus()
                    return True

                if type(event) == QFocusEvent and event.lostFocus():
                    # This happens either if enter is pressed, or if the
                    # user tabs out.
                    items = [widget.itemText(i) for i in range(widget.count())]
                    text = widget.currentText()
                    idx = widget.currentIndex()
                    if text in items and widget.itemText(idx) != text:
                        # Prevent the QComboBox from automatically changing
                        # the index to be that of the other item in the list.
                        # This is confusing behavior, and it's not what we
                        # want here.
                        widget.setCurrentIndex(idx)
                        # Let the widget lose focus
                        return False

        return False

    def _on_configure_buffer(self):
        # Disable button to prevent multiple dialogs
        self.ui.cal_det_buffer.setEnabled(False)
        detector = self.get_current_detector()
        dialog = PanelBufferDialog(detector, self.ui)
        dialog.show()
        # Re-enable button
        dialog.ui.finished.connect(
            lambda _: self.ui.cal_det_buffer.setEnabled(True))
Example #2
0
class CalTreeItemModel(QAbstractItemModel):
    def __init__(self, parent=None):
        super(CalTreeItemModel, self).__init__(parent)
        self.root_item = TreeItem(['key', 'value', 'fixed'])
        self.cfg = HexrdConfig()
        self.rebuild_tree()

    def columnCount(self, parent):
        return self.root_item.column_count()

    def data(self, index, role):
        if not index.isValid():
            return None

        if role != Qt.DisplayRole and role != Qt.EditRole:
            return None

        item = self.get_item(index)

        value = item.data(index.column())
        if index.column() == STATUS_COL:
            if role == Qt.EditRole:
                return value
            if role == Qt.DisplayRole:
                return None
        return value

    def setData(self, index, value, role):
        item = self.get_item(index)
        path = self.get_path_from_root(item, index.column())

        # If they are identical, don't do anything
        if value == item.data(index.column()):
            return True

        if index.column() == VALUE_COL:
            old_value = self.cfg.get_instrument_config_val(path)

            # As a validation step, ensure that the new value can be
            # converted to the old value's type
            try:
                value = type(old_value)(value)
            except ValueError:
                msg = ('Could not convert ' + str(value) + ' to type ' +
                       str(type(old_value).__name__))
                QMessageBox.warning(None, 'HEXRD', msg)
                return False

        item.set_data(index.column(), value)

        if item.child_count() == 0:
            self.cfg.set_instrument_config_val(path, value)

        return True

    def flags(self, index):
        if not index.isValid():
            return Qt.NoItemFlags

        flags = super(CalTreeItemModel, self).flags(index)

        item = self.get_item(index)
        if ((index.column() == VALUE_COL and item.child_count() == 0)
                or index.column() == STATUS_COL):
            # The second and third columns with no children are editable
            flags = flags | Qt.ItemIsEditable

        return flags

    def headerData(self, section, orientation, role):
        if orientation == Qt.Horizontal and role == Qt.DisplayRole:
            return self.root_item.data(section)

        return None

    def index(self, row, column, parent):
        if not self.hasIndex(row, column, parent):
            return QModelIndex()

        parent_item = self.get_item(parent)
        child_item = parent_item.child(row)
        if not child_item:
            return QModelIndex()

        return self.createIndex(row, column, child_item)

    def parent(self, index):
        if not index.isValid():
            return QModelIndex()

        child_item = self.get_item(index)
        parent_item = child_item.parent_item
        if not parent_item or parent_item is self.root_item:
            return QModelIndex()

        return self.createIndex(parent_item.row(), KEY_COL, parent_item)

    def rowCount(self, parent=QModelIndex()):
        parent_item = self.get_item(parent)
        return parent_item.child_count()

    def get_item(self, index):
        # If the index is valid and the internal pointer is valid,
        # return the item. Otherwise, return the root item.
        if index.isValid():
            item = index.internalPointer()
            if item:
                return item

        return self.root_item

    def clear(self):
        # Remove all of the root item children. That clears it.
        root = self.root_item
        self.beginRemoveRows(QModelIndex(), KEY_COL, root.child_count() - 1)
        root.clear_children()
        self.endRemoveRows()

    def rebuild_tree(self):
        # Rebuild the tree from scratch
        self.clear()
        for key in self.cfg.internal_instrument_config.keys():
            tree_item = self.add_tree_item(key, None, REFINED, self.root_item)
            self.recursive_add_tree_items(
                self.cfg.internal_instrument_config[key], tree_item)
            self.update_parent_status(tree_item)

    def add_tree_item(self, key, value, status, parent):
        data = [key, value, status]
        tree_item = TreeItem(data, parent)
        return tree_item

    def set_value(self, key, cur_config, cur_tree_item):
        if isinstance(cur_config, list):
            children = cur_tree_item.child_items
            for child in children:
                value = cur_config[child.data(KEY_COL)]
                child.set_data(VALUE_COL, value)
        else:
            cur_tree_item.set_data(VALUE_COL, cur_config)
        return

    def recursive_add_tree_items(self, cur_config, cur_tree_item):
        if isinstance(cur_config, dict):
            keys = cur_config.keys()
        elif isinstance(cur_config, list):
            keys = range(len(cur_config))
        else:
            # This must be a value. Set it.
            cur_tree_item.set_data(STATUS_COL, cur_config)
            return

        for key in keys:
            if key == 'value':
                self.set_value(key, cur_config[key], cur_tree_item)
                continue
            elif key == 'status':
                tree_item = cur_tree_item
            else:
                tree_item = self.add_tree_item(key, None, REFINED,
                                               cur_tree_item)
            self.recursive_add_tree_items(cur_config[key], tree_item)

    def update_parent_status(self, parent):
        children = parent.child_items
        for child in children:
            if child.child_count() > 0:
                self.update_parent_status(child)
            if child.data(STATUS_COL):
                parent.set_data(STATUS_COL, FIXED)

    def get_path_from_root(self, tree_item, column):
        path = ['value'] if column == VALUE_COL else ['status']
        cur_tree_item = tree_item
        while True:
            text = cur_tree_item.data(KEY_COL)
            if _is_int(text):
                path.append(int(text))
            else:
                path.insert(0, text)
            cur_tree_item = cur_tree_item.parent_item
            if cur_tree_item is self.root_item:
                break

        return path
Example #3
0
class CalTreeItemModel(BaseTreeItemModel):

    def __init__(self, parent=None):
        super().__init__(parent)
        self.root_item = TreeItem(['key', 'value', 'refinable'])
        self.cfg = HexrdConfig()
        self.rebuild_tree()

    def data(self, index, role):
        if not index.isValid():
            return None

        if role != Qt.DisplayRole and role != Qt.EditRole:
            return None

        item = self.get_item(index)

        value = item.data(index.column())
        if index.column() == STATUS_COL:
            if role == Qt.EditRole:
                return value
            if role == Qt.DisplayRole:
                return None

        if isinstance(value, np.generic):
            # Get a native python type for display. Otherwise,
            # it won't display anything...
            value = value.item()

        return value

    def setData(self, index, value, role):
        item = self.get_item(index)
        path = self.path_to_value(item, index.column())

        # If they are identical, don't do anything
        # (we exclude np.ndarray's from this)
        is_numpy = isinstance(value, np.ndarray) or \
            isinstance(item.data(index.column()), np.ndarray)
        if not is_numpy and value == item.data(index.column()):
            return True

        key = item.data(KEY_COL)
        # Note: We don't want todo this check for panel buffers as they
        # can be a list or numpy.ndarray
        if index.column() == VALUE_COL and key != constants.BUFFER_KEY:
            old_value = self.cfg.get_instrument_config_val(path)

            # As a validation step, ensure that the new value can be
            # converted to the old value's type
            try:
                value = type(old_value)(value)
            except ValueError:
                msg = ('Could not convert ' + str(value) + ' to type ' +
                       str(type(old_value).__name__))
                QMessageBox.warning(None, 'HEXRD', msg)
                return False

        item.set_data(index.column(), value)

        if item.child_count() == 0:
            parent = item.parent_item.data(KEY_COL)
            if (parent == 'tilt' and
                    HexrdConfig().rotation_matrix_euler() is not None and
                    len(path) > 1 and path[-2] == 'value'):
                # Convert tilt values to radians before saving
                value = np.radians(value).item()
            self.cfg.set_instrument_config_val(path, value)
            dist_func_path = ['distortion', 'function_name', 'value']
            if len(path) > 4 and path[2:5] == dist_func_path:
                # Rebuild the tree if the distortion function changed
                QObject.parent(self).rebuild_tree()

        return True

    def flags(self, index):
        if not index.isValid():
            return Qt.NoItemFlags

        flags = super(CalTreeItemModel, self).flags(index)

        item = self.get_item(index)
        if ((index.column() == VALUE_COL and item.child_count() == 0) or
                index.column() == STATUS_COL):
            # The second and third columns with no children are editable
            flags = flags | Qt.ItemIsEditable

        return flags

    def add_tree_item(self, key, value, status, parent):
        # In the case of the panel buffer we don't want to added children
        # The editor will take care of this.
        if parent.data(KEY_COL) == constants.BUFFER_KEY:
            return

        data = [key, value, status]
        tree_item = TreeItem(data, parent)
        return tree_item

    def rebuild_tree(self):
        # Rebuild the tree from scratch
        self.clear()
        for key in self.cfg.internal_instrument_config.keys():
            tree_item = self.add_tree_item(key, None, REFINABLE,
                                           self.root_item)
            self.recursive_add_tree_items(
                self.cfg.internal_instrument_config[key], tree_item)
            self.update_parent_status(tree_item)

    def recursive_add_tree_items(self, cur_config, cur_tree_item):
        if isinstance(cur_config, dict):
            keys = cur_config.keys()
        elif isinstance(cur_config, list):
            keys = range(len(cur_config))
        else:
            # This must be a value. Set it.
            cur_tree_item.set_data(STATUS_COL, cur_config)
            return

        for key in keys:
            if key == 'value':
                data = cur_config[key]
                if (cur_tree_item.data(KEY_COL) == 'tilt' and
                        HexrdConfig().rotation_matrix_euler() is not None):
                    data = [np.degrees(rad).item() for rad in cur_config[key]]
                self.set_value(key, data, cur_tree_item)
                continue
            elif key == 'status':
                tree_item = cur_tree_item
            else:
                tree_item = self.add_tree_item(key, None, REFINABLE,
                                               cur_tree_item)
            if tree_item is not None:
                self.recursive_add_tree_items(cur_config[key], tree_item)

    def update_parent_status(self, parent):
        children = parent.child_items
        for child in children:
            if child.child_count() > 0:
                self.update_parent_status(child)
            if child.data(STATUS_COL):
                parent.set_data(STATUS_COL, FIXED)

    def path_to_value(self, tree_item, column):
        path = ['value'] if column == VALUE_COL else ['status']
        cur_tree_item = tree_item
        while True:
            text = cur_tree_item.data(KEY_COL)
            if is_int(text):
                path.append(int(text))
            else:
                path.insert(0, text)
            cur_tree_item = cur_tree_item.parent_item
            if cur_tree_item is self.root_item:
                break

        return path

    def set_value(self, key, cur_config, cur_tree_item):
        if isinstance(cur_config, list):
            children = cur_tree_item.child_items
            for child in children:
                value = cur_config[child.data(KEY_COL)]
                child.set_data(VALUE_COL, value)
        else:
            cur_tree_item.set_data(VALUE_COL, cur_config)
        return
Example #4
0
class CalibrationConfigWidget(QObject):
    """Emitted when GUI data has changed"""
    gui_data_changed = Signal()

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

        self.cfg = HexrdConfig()

        loader = UiLoader()
        self.ui = loader.load_file('calibration_config_widget.ui', parent)

        self.detector_widgets_disabled = False

        self.setup_connections()

        self.timer = None

    def setup_connections(self):
        self.ui.cal_energy.valueChanged.connect(self.on_energy_changed)
        self.ui.cal_energy_wavelength.valueChanged.connect(
            self.on_energy_wavelength_changed)

        self.ui.cal_det_current.currentIndexChanged.connect(
            self.on_detector_changed)
        self.ui.cal_det_current.editTextChanged.connect(
            self.on_detector_name_edited)
        self.ui.cal_det_remove.clicked.connect(self.on_detector_remove_clicked)
        self.ui.cal_det_add.clicked.connect(self.on_detector_add_clicked)

        all_widgets = self.get_all_widgets()
        skip_widgets = ['cal_det_current', 'cal_det_add', 'cal_det_remove']
        for widget in all_widgets:
            if widget.objectName() in skip_widgets:
                continue

            if isinstance(widget, QComboBox):
                widget.currentIndexChanged.connect(self.update_config_from_gui)
            elif isinstance(widget, QLineEdit):
                widget.textEdited.connect(self.update_config_from_gui)
            elif isinstance(widget, QPushButton):
                widget.pressed.connect(self.update_config_from_gui)
            else:
                widget.valueChanged.connect(self.update_config_from_gui)
                widget.valueChanged.connect(self.gui_value_changed)

    def on_energy_changed(self):
        val = self.ui.cal_energy.value()

        # Make sure energy has same style
        kev_widget = getattr(self.ui, 'cal_energy')
        wave_widget = getattr(self.ui, 'cal_energy_wavelength')
        wave_widget.setStyleSheet(kev_widget.styleSheet())

        block_signals = self.ui.cal_energy_wavelength.blockSignals(True)
        try:
            new_wavelength = constants.KEV_TO_WAVELENGTH / val
            self.ui.cal_energy_wavelength.setValue(new_wavelength)
        finally:
            self.ui.cal_energy_wavelength.blockSignals(block_signals)

    def on_energy_wavelength_changed(self):
        val = self.ui.cal_energy_wavelength.value()

        block_signals = self.ui.cal_energy.blockSignals(True)
        try:
            new_energy = constants.WAVELENGTH_TO_KEV / val
            self.ui.cal_energy.setValue(new_energy)
        finally:
            self.ui.cal_energy.blockSignals(block_signals)

    def on_detector_changed(self):
        self.update_detector_from_config()

    def enable_detector_widgets(self, enable=True):
        detector_widgets = self.cfg.get_detector_widgets()
        detector_widgets.remove('cal_det_add')
        for widget in detector_widgets:
            gui_widget = getattr(self.ui, widget)
            gui_widget.setEnabled(enable)

        self.detector_widgets_disabled = not enable

    def on_detector_name_edited(self):
        new_name = self.ui.cal_det_current.currentText()
        detector_names = self.cfg.get_detector_names()

        # Ignore it if there is already a detector with this name
        if new_name in detector_names:
            return

        # Get the old name from the underlying item
        idx = self.ui.cal_det_current.currentIndex()
        old_name = self.ui.cal_det_current.itemText(idx)

        self.ui.cal_det_current.setItemText(idx, new_name)
        self.cfg.rename_detector(old_name, new_name)

    def on_detector_remove_clicked(self):
        current_detector = self.get_current_detector()
        idx = self.ui.cal_det_current.currentIndex()

        self.cfg.remove_detector(current_detector)
        if idx > 0:
            self.ui.cal_det_current.setCurrentIndex(idx - 1)
        else:
            self.update_detector_from_config()

    def on_detector_add_clicked(self):
        current_detector = self.get_current_detector()
        detector_names = self.cfg.get_detector_names()
        new_detector_name_base = 'detector_'
        for i in range(1000000):
            new_detector_name = new_detector_name_base + str(i + 1)
            if new_detector_name not in detector_names:
                self.cfg.add_detector(new_detector_name, current_detector)
                self.update_detector_from_config()
                self.ui.cal_det_current.setCurrentText(new_detector_name)
                return

    def update_config_from_gui(self):
        """This function only updates the sender value"""
        sender = self.sender()
        name = sender.objectName()
        if name == 'cal_energy_wavelength':
            # Need to update the `cal_energy` entry instead
            sender = self.ui.cal_energy
            name = sender.objectName()

        value = self._get_gui_value(sender)
        current_detector = self.get_current_detector()
        self.cfg.set_val_from_widget_name(name, value, current_detector)

    def gui_value_changed(self):
        """We only want to emit a changed signal once editing is done"""
        if self.timer is None:
            self.timer = QTimer()
            self.timer.setSingleShot(True)
            self.timer.timeout.connect(self.gui_data_changed)

        # Start or restart our timer...
        self.timer.start(666)

    def update_gui_from_config(self):
        previously_blocked = self.block_all_signals()

        try:
            gui_yaml_paths = self.cfg.get_gui_yaml_paths()
            for var, path in gui_yaml_paths:
                if 'detector_name' in path:
                    # We give these special treatment
                    continue

                gui_var = getattr(self.ui, var)
                config_val = self.cfg.get_instrument_config_val(path)
                path[path.index('value')] = 'status'
                status_val = self.cfg.get_instrument_config_val(path)

                self._set_gui_value(gui_var, config_val, status_val)
        finally:
            self.unblock_all_signals(previously_blocked)

        # Update the wavelength also
        self.on_energy_changed()

        self.update_detector_from_config()

    def update_detector_from_config(self):
        previously_blocked = self.block_all_signals()

        try:
            detector_names = self.cfg.get_detector_names()
            if not detector_names:
                # Disable detector widgets if there is no valid detector
                if not self.detector_widgets_disabled:
                    self.enable_detector_widgets(enable=False)

                return
            elif self.detector_widgets_disabled:
                # Enable detector widgets if they were previously disabled
                self.enable_detector_widgets(enable=True)

            cur_detector = detector_names[0]
            if self.ui.cal_det_current.currentText() in detector_names:
                cur_detector = self.ui.cal_det_current.currentText()

            gui_yaml_paths = self.cfg.get_gui_yaml_paths(
                ['detectors', 'detector_name'])

            tilt_path = ['transform', 'tilt', 'value']
            for var, path in gui_yaml_paths:
                gui_var = getattr(self.ui, var)
                full_path = ['detectors', cur_detector] + path
                config_val = self.cfg.get_instrument_config_val(full_path)
                full_path[full_path.index('value')] = 'status'
                status_val = self.cfg.get_instrument_config_val(full_path)

                if path[:-1] == tilt_path:
                    # Special treatment for tilt widgets
                    if HexrdConfig().rotation_matrix_euler() is None:
                        gui_var.setSuffix('')
                    else:
                        gui_var.setSuffix('°')
                        config_val = np.degrees(config_val).item()

                self._set_gui_value(gui_var, config_val, status_val)

            combo_items = []
            for i in range(self.ui.cal_det_current.count()):
                combo_items.append(self.ui.cal_det_current.itemText(i))

            if combo_items != detector_names:
                self.ui.cal_det_current.clear()
                self.ui.cal_det_current.addItems(detector_names)
                self.ui.cal_det_current.setCurrentText(cur_detector)

        finally:
            self.unblock_all_signals(previously_blocked)

    def get_current_detector(self):
        if self.detector_widgets_disabled:
            return None

        return self.ui.cal_det_current.currentText()

    def get_all_widgets(self):
        gui_yaml_paths = self.cfg.get_gui_yaml_paths()
        widgets = [x[0] for x in gui_yaml_paths]
        widgets += self.cfg.get_detector_widgets()
        widgets += ['cal_energy_wavelength']
        widgets = list(set(widgets))
        widgets = [getattr(self.ui, x) for x in widgets]
        return widgets

    def block_all_signals(self):
        previously_blocked = []
        all_widgets = self.get_all_widgets()

        for widget in all_widgets:
            previously_blocked.append(widget.blockSignals(True))

        return previously_blocked

    def unblock_all_signals(self, previously_blocked):
        all_widgets = self.get_all_widgets()

        for block, widget in zip(previously_blocked, all_widgets):
            widget.blockSignals(block)

    def _set_gui_value(self, gui_object, value, flag=None):
        """This is for calling various set methods for GUI variables

        For instance, QComboBox will call "setCurrentText", while
        QSpinBox will call "setValue".
        """
        if isinstance(gui_object, QComboBox):
            gui_object.setCurrentText(value)
        else:
            if flag == 1 and not gui_object.styleSheet():
                s = 'QSpinBox, QDoubleSpinBox { background-color: lightgray; }'
                gui_object.setStyleSheet(s)
            elif gui_object.styleSheet() and flag != 1:
                gui_object.setStyleSheet("")
            # If it is anything else, just assume setValue()
            gui_object.setValue(value)

    def _get_gui_value(self, gui_object):
        """This is for calling various get methods for GUI variables

        For instance, QComboBox will call "currentText", while
        QSpinBox will call "value".
        """
        if isinstance(gui_object, QComboBox):
            return gui_object.currentText()
        else:
            # If it is anything else, just assume value()
            return gui_object.value()