Esempio n. 1
0
    class Multibinder:
        foo = bnd.multi_bind(["xbox", "ybox", "zbox"])

        def __init__(self):
            self.xbox = QtWidgets.QSpinBox()
            self.ybox = QtWidgets.QLabel()
            self.zbox = QtWidgets.QPlainTextEdit()
Esempio n. 2
0
class OptimizationRecoilParameterWidget(OptimizationParameterWidget):
    """
    Class that handles the recoil optimization parameters' ui.
    """

    # Recoil specific properties
    upper_limits = bnd.multi_bind(
        ("upperXDoubleSpinBox", "upperYDoubleSpinBox"))
    lower_limits = bnd.multi_bind(
        ("lowerXDoubleSpinBox", "lowerYDoubleSpinBox"))

    # sol_size values are unique (5, 7, 9 or 11) so they can be used in
    # two-way binding
    sol_size = bnd.bind("recoilTypeComboBox",
                        fget=sol_size_from_combobox,
                        fset=sol_size_to_combobox)
    recoil_type = bnd.bind("recoilTypeComboBox",
                           fget=recoil_from_combobox,
                           twoway=False)

    @property
    def optimization_type(self) -> OptimizationType:
        return OptimizationType.RECOIL

    def __init__(self, **kwargs):
        """Initialize the widget.

        Args:
            kwargs: property values to be shown in the widget.
        """
        ui_file = gutils.get_ui_dir() / "ui_optimization_recoil_params.ui"
        super().__init__(ui_file, **kwargs)
        self.radio_buttons()
        locale = QLocale.c()
        self.upperXDoubleSpinBox.setLocale(locale)
        self.lowerXDoubleSpinBox.setLocale(locale)
        self.upperYDoubleSpinBox.setLocale(locale)
        self.lowerYDoubleSpinBox.setLocale(locale)
Esempio n. 3
0
class GlobalSettingsDialog(QtWidgets.QDialog):
    """
    A GlobalSettingsDialog.
    """
    color_scheme = bnd.bind("combo_tofe_colors")
    tofe_invert_x = bnd.bind("check_tofe_invert_x")
    tofe_invert_y = bnd.bind("check_tofe_invert_y")
    tofe_transposed = bnd.bind("check_tofe_transpose")
    tofe_bin_x = bnd.multi_bind(
        ("spin_tofe_bin_x_min", "spin_tofe_bin_x_max")
    )
    tofe_bin_y = bnd.multi_bind(
        ("spin_tofe_bin_y_min", "spin_tofe_bin_y_max")
    )
    comp_x = bnd.bind("spin_tofe_compression_x")
    comp_y = bnd.bind("spin_tofe_compression_y")

    depth_iters = bnd.bind("spin_depth_iterations")

    presim_ions = bnd.bind("presim_spinbox")
    sim_ions = bnd.bind("sim_spinbox")
    ion_division = bnd.bind("ion_division_radios")
    min_concentration = bnd.bind("min_conc_spinbox")

    coinc_count = bnd.bind("line_coinc_count")

    cross_section = bnd.bind("cross_section_radios")

    save_window_geometries = bnd.bind("window_geom_chkbox")

    settings_updated = QtCore.pyqtSignal(GlobalSettings)

    def __init__(self, settings: GlobalSettings):
        """Constructor for the program
        """
        super().__init__()
        uic.loadUi(gutils.get_ui_dir() / "ui_global_settings.ui", self)

        self.settings = settings
        self.__added_timings = {}  # Placeholder for timings
        self.setAttribute(QtCore.Qt.WA_DeleteOnClose)

        gutils.set_min_max_handlers(
            self.spin_tofe_bin_x_min, self.spin_tofe_bin_x_max
        )
        gutils.set_min_max_handlers(
            self.spin_tofe_bin_y_min, self.spin_tofe_bin_y_max
        )
        gutils.set_btn_group_data(self.ion_division_radios, IonDivision)
        gutils.set_btn_group_data(self.cross_section_radios, CrossSection)
        gutils.fill_combobox(self.combo_tofe_colors, ToFEColorScheme)

        # Connect UI buttons
        self.OKButton.clicked.connect(self.__accept_changes)
        self.cancelButton.clicked.connect(self.close)
        buttons = self.findChild(QtWidgets.QButtonGroup, "elementButtons")
        buttons.buttonClicked.connect(self.__change_element_color)

        self.min_conc_spinbox.setLocale(QtCore.QLocale.c())

        if platform.system() == "Darwin":
            self.gridLayout.setVerticalSpacing(15)
            self.gridLayout.setHorizontalSpacing(15)

        self.__set_values()

    def closeEvent(self, event):
        """Disconnects settings updated signal before closing.
        """
        try:
            self.settings_updated.disconnect()
        except AttributeError:
            pass
        super().closeEvent(event)

    def __set_values(self):
        """Set settings values to dialog.
        """
        for button in self.groupBox_3.findChildren(QtWidgets.QPushButton):
            self.set_btn_color(
                button, self.settings.get_element_color(button.text()))

        label_adc = QtWidgets.QLabel("ADC")
        label_low = QtWidgets.QLabel("Low")
        label_high = QtWidgets.QLabel("High")
        self.grid_timing.addWidget(label_adc, 0, 0)
        self.grid_timing.addWidget(label_low, 1, 0)
        self.grid_timing.addWidget(label_high, 2, 0)
        for i in range(3):
            timing = self.settings.get_import_timing(i)
            label = QtWidgets.QLabel(f"{i}")
            spin_low = self.__create_spinbox(timing[0])
            spin_high = self.__create_spinbox(timing[1])
            self.__added_timings[i] = CoincTiming(i, spin_low, spin_high)
            self.grid_timing.addWidget(label, 0, i + 1)
            self.grid_timing.addWidget(spin_low, 1, i + 1)
            self.grid_timing.addWidget(spin_high, 2, i + 1)
        self.coinc_count = self.settings.get_import_coinc_count()
        # self.__set_cross_sections()
        self.cross_section = self.settings.get_cross_sections()

        # ToF-E graph settings
        self.tofe_invert_x = self.settings.get_tofe_invert_x()
        self.tofe_invert_y = self.settings.get_tofe_invert_y()
        self.tofe_transposed = self.settings.get_tofe_transposed()

        # TODO binding for bin mode
        tofe_bin_mode = self.settings.get_tofe_bin_range_mode()
        self.radio_tofe_bin_auto.setChecked(tofe_bin_mode == 0)
        self.radio_tofe_bin_manual.setChecked(tofe_bin_mode == 1)
        self.tofe_bin_x = self.settings.get_tofe_bin_range_x()
        self.tofe_bin_y = self.settings.get_tofe_bin_range_y()

        self.comp_x = self.settings.get_tofe_compression_x()
        self.comp_y = self.settings.get_tofe_compression_y()

        self.depth_iters = self.settings.get_num_iterations()

        self.presim_ions = self.settings.get_min_presim_ions()
        self.sim_ions = self.settings.get_min_simulation_ions()
        self.ion_division = self.settings.get_ion_division()
        self.min_conc_spinbox.setMinimum(GlobalSettings.MIN_CONC_LIMIT)
        self.min_concentration = self.settings.get_minimum_concentration()

        self.save_window_geometries = gutils.get_potku_setting(
            BaseTab.SAVE_WINDOW_GEOM_KEY, True
        )
        self.color_scheme = self.settings.get_tofe_color()

    @staticmethod
    def __create_spinbox(default):
        """
        Create a spinbox.
        """
        spinbox = QtWidgets.QSpinBox()
        spinbox.stepBy(1)
        spinbox.setMinimum(-1000)
        spinbox.setMaximum(1000)
        spinbox.setValue(int(default))
        return spinbox

    def __accept_changes(self):
        """Accept changed settings and save.
        """
        for button in self.groupBox_3.findChildren(QtWidgets.QPushButton):
            self.settings.set_element_color(button.text(), button.color)
        for key, coinc_timing in self.__added_timings.items():
            self.settings.set_import_timing(
                key, coinc_timing.low.value(), coinc_timing.high.value())
        self.settings.set_import_coinc_count(self.coinc_count)

        self.settings.set_cross_sections(self.cross_section)

        # ToF-E graph settings
        self.settings.set_tofe_invert_x(self.tofe_invert_x)
        self.settings.set_tofe_invert_y(self.tofe_invert_y)
        self.settings.set_tofe_transposed(self.tofe_transposed)
        self.settings.set_tofe_color(self.color_scheme)
        if self.radio_tofe_bin_auto.isChecked():
            self.settings.set_tofe_bin_range_mode(0)
        elif self.radio_tofe_bin_manual.isChecked():
            self.settings.set_tofe_bin_range_mode(1)

        self.settings.set_tofe_bin_range_x(*self.tofe_bin_x)
        self.settings.set_tofe_bin_range_y(*self.tofe_bin_y)
        self.settings.set_tofe_compression_x(self.comp_x)
        self.settings.set_tofe_compression_y(self.comp_y)
        self.settings.set_num_iterations(self.depth_iters)
        self.settings.set_min_presim_ions(self.presim_ions)
        self.settings.set_min_simulation_ions(self.sim_ions)
        self.settings.set_ion_division(self.ion_division)
        self.settings.set_minimum_concentration(self.min_concentration)

        gutils.set_potku_setting(
            BaseTab.SAVE_WINDOW_GEOM_KEY, self.save_window_geometries)

        # Save config and close
        self.settings.save_config()
        self.settings_updated.emit(self.settings)
        self.close()

    def __change_request_directory(self):
        """Change default request directory.
        """
        folder = QtWidgets.QFileDialog.getExistingDirectory(
            self, "Select default request directory",
            directory=self.requestPathLineEdit.text())
        if folder:
            self.requestPathLineEdit.setText(folder)

    def __change_element_color(self, button):
        """Change color of element button.
        
        Args:
            button: QPushButton
        """
        dialog = QtWidgets.QColorDialog(self)
        self.color = dialog.getColor(
            QtGui.QColor(button.color), self,
            f"Select Color for Element: {button.text()}")
        if self.color.isValid():
            self.set_btn_color(button, self.color.name())

    @staticmethod
    def set_btn_color(button, color_name):
        """Change button text color.
        
        Args:
            button: QPushButton
            color_name: String representing color.
        """
        color = QtGui.QColor(color_name)
        style = df.get_btn_stylesheet(color)

        button.color = color.name()
        if not button.isEnabled():
            return  # Do not set color for disabled buttons.
        button.setStyleSheet(style)
Esempio n. 4
0
class MeasurementSettingsWidget(QtWidgets.QWidget,
                                bnd.PropertyTrackingWidget,
                                bnd.PropertySavingWidget,
                                metaclass=QtABCMeta):
    """Class for creating a measurement settings tab.
    """
    # Signal that indicates whether the beam selection was ok or not (i.e. can
    # the selected element be used in measurements or simulations)
    beam_selection_ok = pyqtSignal(bool)

    measurement_setting_file_name = bnd.bind("nameLineEdit")
    measurement_setting_file_description = bnd.bind("descriptionPlainTextEdit")

    beam_ion = bnd.multi_bind(
        ["beamIonButton", "isotopeComboBox"], fget=element_from_gui,
        fset=element_to_gui, track_change=True
    )
    beam_energy = bnd.bind("energyDoubleSpinBox", track_change=True)
    beam_energy_distribution = bnd.bind(
        "energyDistDoubleSpinBox", track_change=True)
    beam_charge = bnd.bind("beamChargeSpinBox")
    beam_spot_size = bnd.multi_bind(
        ["spotSizeXdoubleSpinBox", "spotSizeYdoubleSpinBox"], track_change=True)
    beam_divergence = bnd.bind("divergenceDoubleSpinBox", track_change=True)
    beam_profile = bnd.bind("profileComboBox", track_change=True)

    run_fluence = bnd.bind("fluenceDoubleSpinBox")
    run_current = bnd.bind("currentDoubleSpinBox")
    run_time = bnd.bind("timeDoubleSpinBox")
    run_charge = bnd.bind("runChargeDoubleSpinBox")

    target_theta = bnd.bind("targetThetaDoubleSpinBox", track_change=True)
    detector_theta = bnd.bind("detectorThetaDoubleSpinBox", track_change=True)

    def __init__(self, obj: Union[Measurement, Simulation], preset_folder=None):
        """Initializes the widget.

        Args:
            obj: object that uses these settings, either a Measurement or a
                Simulation.
        """
        super().__init__()
        uic.loadUi(gutils.get_ui_dir() / "ui_measurement_settings_tab.ui", self)
        self.fluenceDoubleSpinBox = ScientificSpinBox()
        image = gf.get_root_dir() / "images" / "measurement_setup_angles.png"
        pixmap = QtGui.QPixmap(str(image))
        self.picture.setScaledContents(True)
        self.picture.setPixmap(pixmap)

        self.obj = obj
        self.__original_property_values = {}

        locale = QLocale.c()

        self.energyDoubleSpinBox.setLocale(locale)
        self.energyDistDoubleSpinBox.setLocale(locale)
        self.spotSizeXdoubleSpinBox.setLocale(locale)
        self.spotSizeYdoubleSpinBox.setLocale(locale)
        self.divergenceDoubleSpinBox.setLocale(locale)
        self.currentDoubleSpinBox.setLocale(locale)
        self.timeDoubleSpinBox.setLocale(locale)
        self.runChargeDoubleSpinBox.setLocale(locale)

        self.targetThetaDoubleSpinBox.setLocale(locale)
        self.detectorThetaDoubleSpinBox.setLocale(locale)
        self.detectorFiiDoubleSpinBox.setLocale(locale)
        self.targetFiiDoubleSpinBox.setLocale(locale)

        # Fii angles are currently not used so disable their spin boxes
        self.detectorFiiDoubleSpinBox.setEnabled(False)
        self.targetFiiDoubleSpinBox.setEnabled(False)
        gutils.fill_combobox(self.profileComboBox, Profile)

        # Copy of measurement's/simulation's run or default run
        # TODO should default run also be copied?
        if not self.obj.run:
            self.tmp_run = self.obj.request.default_run
        else:
            self.tmp_run = copy.deepcopy(self.obj.run)

        self.isotopeInfoLabel.setVisible(False)

        self.beamIonButton.clicked.connect(self.change_element)

        self.fields_are_valid = False
        iv.set_input_field_red(self.nameLineEdit)
        self.nameLineEdit.textChanged.connect(
            lambda: iv.check_text(self.nameLineEdit, qwidget=self))
        self.nameLineEdit.textEdited.connect(self.__validate)
        self.nameLineEdit.setEnabled(False)

        self.run_form_layout: QtWidgets.QFormLayout
        self.run_form_layout.insertRow(3, "Fluence", self.fluenceDoubleSpinBox)
        self.fluenceDoubleSpinBox.scientificLineEdit.setContextMenuPolicy(
            Qt.ActionsContextMenu)
        self.actionMultiply = QtWidgets.QAction(
            self.fluenceDoubleSpinBox.scientificLineEdit)
        self.actionMultiply.triggered.connect(self.__multiply_fluence)
        self.fluenceDoubleSpinBox.scientificLineEdit.addAction(
            self.actionMultiply)

        self.actionUndo = QtWidgets.QAction(
            self.fluenceDoubleSpinBox.scientificLineEdit)
        self.actionUndo.setText("Undo multiply")
        self.actionUndo.triggered.connect(self.__undo_fluence)

        self.actionUndo.setEnabled(bool(self.tmp_run.previous_fluence))
        self.fluenceDoubleSpinBox.scientificLineEdit.addAction(self.actionUndo)

        self.clipboard = QGuiApplication.clipboard()
        self._ratio = None
        self.clipboard.changed.connect(self.__update_multiply_action)
        self.__update_multiply_action()

        self.energyDoubleSpinBox.setToolTip("Energy set in MeV with.")

        if preset_folder is not None:
            self.preset_widget = PresetWidget.add_preset_widget(
                preset_folder / "measurement", "mea",
                lambda w: self.layout().insertWidget(0, w),
                save_callback=self.save_properties_to_file,
                load_callback=self.load_properties_from_file
            )
        else:
            self.preset_widget = None

        self.show_settings()

    def get_property_file_path(self) -> Path:
        raise NotImplementedError

    def save_on_close(self) -> bool:
        return False

    def save_properties_to_file(self, file_path: Path):
        def err_func(err: Exception):
            if self.preset_widget is not None:
                self.preset_widget.set_status_msg(
                    f"Failed to save preset: {err}")

        values = self.get_properties()
        values["beam_ion"] = str(values["beam_ion"])
        self._save_json_file(
            file_path, values, True, error_func=err_func)
        if self.preset_widget is not None:
            self.preset_widget.load_files(selected=file_path)

    def load_properties_from_file(self, file_path: Path):
        # TODO create a base class for settings widgets to get rid of this
        #   copy-paste code
        def err_func(err: Exception):
            if self.preset_widget is not None:
                self.preset_widget.set_status_msg(
                    f"Failed to load preset: {err}")
        values = self._load_json_file(file_path, error_func=err_func)
        if not values:
            return
        try:
            values["beam_ion"] = Element.from_string(values["beam_ion"])
            self.set_properties(**values)
        except KeyError as e:
            err_func(f"file contained no {e} key.")
        except ValueError as e:
            err_func(e)

    def get_original_property_values(self):
        """Returns the values of the properties when they were first set.
        """
        return self.__original_property_values

    def show_settings(self):
        """Show measurement settings.
        """
        self.measurement_setting_file_name = \
            self.obj.measurement_setting_file_name
        self.measurement_setting_file_description = \
            self.obj.measurement_setting_file_description
        self.dateLabel.setText(time.strftime("%c %z %Z", time.localtime(
            self.obj.modification_time)))

        run_params = {
            f"run_{key}": value
            for key, value in self.tmp_run.get_settings().items()
        }
        bean_params = {
            f"beam_{key}": value
            for key, value in self.tmp_run.beam.get_settings().items()
        }
        self.set_properties(**run_params, **bean_params)

        self.detector_theta = self.obj.detector.detector_theta
        self.target_theta = self.obj.target.target_theta

    def check_angles(self):
        """
        Check that detector angle is bigger than target angle.
        This is a must for measurement. Simulation can handle target angles
        greater than the detector angle.

        Return:
            Whether it is ok to use current angle settings.
        """
        if self.target_theta > self.detector_theta:
            reply = QtWidgets.QMessageBox.question(
                self, "Warning",
                "Measurement cannot use a target angle that is "
                "bigger than the detector angle (for simulation "
                "this is possible).\n\n"
                "Do you want to use these settings anyway?",
                QtWidgets.QMessageBox.Ok |
                QtWidgets.QMessageBox.Cancel,
                QtWidgets.QMessageBox.Cancel)
            if reply == QtWidgets.QMessageBox.Cancel:
                return False
        return True

    def get_run_and_beam_parameters(self):
        """Returns run and beam related properties as separate dictionaries with
        prefixes removed from each key.
        """
        run_params, beam_params = {}, {}
        for k, v in self.get_properties().items():
            if k.startswith("run_"):
                run_params[k[4:]] = v
            elif k.startswith("beam_"):
                beam_params[k[5:]] = v
        return run_params, beam_params

    def update_settings(self):
        """Update measurement settings.
        """
        isotope_index = self.isotopeComboBox.currentIndex()
        if isotope_index != -1:
            self.obj.measurement_setting_file_name = \
                self.measurement_setting_file_name
            self.obj.measurement_setting_file_description = \
                self.measurement_setting_file_description

            run_params, beam_params = self.get_run_and_beam_parameters()
            self.obj.run.set_settings(**run_params)
            self.obj.run.beam.set_settings(**beam_params)
            self.obj.run.previous_fluence = self.tmp_run.previous_fluence
            self.obj.detector.detector_theta = self.detector_theta
            self.obj.target.target_theta = self.target_theta
        else:
            QtWidgets.QMessageBox.critical(
                self, "Warning",
                "No isotope selected.\n\n"
                "Please select an isotope for the beam element.",
                QtWidgets.QMessageBox.Ok, QtWidgets.QMessageBox.Ok)

    def other_values_changed(self):
        """
        Check whether measurement values that don't require running a
        simulation again have been changed.

        Return:
             True or False.
        """
        # TODO make it possible to group TrackingProperties (for example into
        #  'critical' and 'noncritical' properties)
        if self.obj.measurement_setting_file_name != \
                self.measurement_setting_file_name:
            return True
        if self.obj.measurement_setting_file_description != \
                self.measurement_setting_file_description:
            return True
        if self.obj.run.beam.charge != self.beam_charge:
            return True
        if self.obj.run.current != self.run_current:
            return True
        if self.obj.run.time != self.run_time:
            return True
        if self.obj.run.charge != self.run_charge:
            return True
        if self.obj.run.fluence != self.run_fluence:
            return True
        return False

    def save_to_tmp_run(self):
        """
        Save run and beam parameters to tmp_run object.
        """
        isotope_index = self.isotopeComboBox.currentIndex()
        # TODO: Show a message box, don't just quietly do nothing
        if isotope_index != -1:
            run_params, beam_params = self.get_run_and_beam_parameters()
            self.tmp_run.set_settings(**run_params)
            self.tmp_run.beam.set_settings(**beam_params)
        else:
            QtWidgets.QMessageBox.critical(
                self, "Warning",
                "No isotope selected.\n\n"
                "Please select an isotope for the beam element.",
                QtWidgets.QMessageBox.Ok, QtWidgets.QMessageBox.Ok)

    def __validate(self):
        """Validate the measurement settings file name.
        """
        text = self.measurement_setting_file_name
        regex = "^[A-Za-z0-9-ÖöÄäÅå]*"
        valid_text = iv.validate_text_input(text, regex)

        self.measurement_setting_file_name = valid_text

    def __multiply_fluence(self):
        """Multiply fluence with clipboard's value.
        """
        try:
            new_fluence = round(self._ratio * self.run_fluence, 2)
            self.tmp_run.previous_fluence.append(self.run_fluence)
            self.run_fluence = new_fluence
            self.actionUndo.setEnabled(True)
        except ValueError:
            pass

    def __undo_fluence(self):
        """Undo latest change to fluence.
        """
        try:
            old_value = self.tmp_run.previous_fluence.pop()
            self.run_fluence = old_value
        except IndexError:
            pass

        # Enable undo if there are still previous values
        self.actionUndo.setEnabled(bool(self.tmp_run.previous_fluence))

    def __update_multiply_action(self):
        """Update the value with which the multiplication is done.
        """
        try:
            self._ratio = float(self.clipboard.text())
        except ValueError:
            if self._ratio is None:
                self._ratio = 1.0
        self.actionMultiply.setText(f"Multiply with value in clipboard\n"
                                    f"({self._ratio})")

    def change_element(self):
        """Opens element selection dialog and loads selected element's isotopes
        to the combobox.
        """
        dialog = ElementSelectionDialog()
        if dialog.element:
            self.beamIonButton.setText(dialog.element)
            # TODO use IsotopeSelectionWidget
            gutils.load_isotopes(dialog.element, self.isotopeComboBox)

            # Check if no isotopes
            if self.isotopeComboBox.count() == 0:
                self.isotopeInfoLabel.setVisible(True)
                self.fields_are_valid = False
                iv.set_input_field_red(self.isotopeComboBox)
                self.beam_selection_ok.emit(False)
            else:
                self.isotopeInfoLabel.setVisible(False)
                iv.check_text(self.nameLineEdit, qwidget=self)
                self.isotopeComboBox.setStyleSheet(
                    "background-color: %s" % "None")
                self.beam_selection_ok.emit(True)
        else:
            self.beam_selection_ok.emit(False)
Esempio n. 5
0
class DetectorSettingsWidget(QtWidgets.QWidget,
                             bnd.PropertyTrackingWidget,
                             metaclass=gutils.QtABCMeta):
    """Class for creating a detector settings tab.
    """
    # Key that is used to store the folder of the most recently added eff file
    EFF_FILE_FOLDER_KEY = "efficiency_folder"

    efficiency_files = bnd.bind("efficiencyListWidget")

    name = bnd.bind("nameLineEdit")
    modification_time = bnd.bind("dateLabel",
                                 fget=bnd.unix_time_from_label,
                                 fset=bnd.unix_time_to_label)
    description = bnd.bind("descriptionLineEdit")
    detector_type = bnd.bind("typeComboBox", track_change=True)
    angle_slope = bnd.bind("angleSlopeLineEdit", track_change=True)
    angle_offset = bnd.bind("angleOffsetLineEdit", track_change=True)
    tof_slope = bnd.bind("scientific_tof_slope", track_change=True)
    tof_offset = bnd.bind("scientific_tof_offset", track_change=True)
    timeres = bnd.bind("timeResSpinBox", track_change=True)
    virtual_size = bnd.multi_bind(
        ("virtualSizeXSpinBox", "virtualSizeYSpinBox"), track_change=True)

    def __init__(self,
                 obj: Detector,
                 request: Request,
                 icon_manager,
                 run=None):
        """Initializes a DetectorSettingsWidget object.

        Args:
              obj: a Detector object.
              request: Which request it belongs to.
              icon_manager: IconManager object.
              run: Run object. None if detector is default detector.
        """
        super().__init__()
        uic.loadUi(gutils.get_ui_dir() / "ui_request_detector_settings.ui",
                   self)

        self.obj = obj
        self.request = request
        self.icon_manager = icon_manager
        self.run = run
        self.__original_properties = {}

        # Temporary foils list which holds all the information given in the
        # foil dialog
        # If user presses ok or apply, these values will be saved into
        # request's default detector
        self.tmp_foil_info = []

        # List of foil indexes that are timing foils
        self.tof_foils = []

        # Add foil widgets and foil objects
        self.detector_structure_widgets = []
        self.foils_layout = self._add_default_foils(self.obj)
        self.detectorScrollAreaContents.layout().addLayout(self.foils_layout)
        self.newFoilButton.clicked.connect(
            lambda: self._add_new_foil(self.foils_layout))

        self.addEfficiencyButton.clicked.connect(self.__add_efficiency)
        self.removeEfficiencyButton.clicked.connect(self.__remove_efficiency)
        self.plotEfficiencyButton.clicked.connect(self.__plot_efficiency)

        self.efficiencyListWidget.itemSelectionChanged.connect(
            self._enable_remove_btn)
        self._enable_remove_btn()

        # Calibration settings
        # TODO: Require saving affected cuts if beam setting has been changed
        self.executeCalibrationButton.clicked.connect(
            self.__open_calibration_dialog)
        self.executeCalibrationButton.setEnabled(
            not self.request.samples.measurements.is_empty())

        gutils.fill_combobox(self.typeComboBox, DetectorType)

        self.fields_are_valid = False
        iv.set_input_field_red(self.nameLineEdit)
        self.nameLineEdit.textChanged.connect(
            lambda: iv.check_text(self.nameLineEdit, qwidget=self))
        self.nameLineEdit.textEdited.connect(
            lambda: iv.sanitize_file_name(self.nameLineEdit))
        self.nameLineEdit.setEnabled(False)

        locale = QLocale.c()
        self.timeResSpinBox.setLocale(locale)
        self.virtualSizeXSpinBox.setLocale(locale)
        self.virtualSizeYSpinBox.setLocale(locale)
        self.angleSlopeLineEdit.setLocale(locale)
        self.angleOffsetLineEdit.setLocale(locale)

        # Create scientific spinboxes for tof slope and tof offset
        self.formLayout_2.removeRow(self.slopeLineEdit)
        self.formLayout_2.removeRow(self.offsetLineEdit)

        self.scientific_tof_slope = ScientificSpinBox(minimum=-math.inf,
                                                      maximum=math.inf)
        self.scientific_tof_offset = ScientificSpinBox(minimum=-math.inf,
                                                       maximum=math.inf)

        self.formLayout_2.insertRow(0, "ToF slope [s/channel]:",
                                    self.scientific_tof_slope)
        self.formLayout_2.insertRow(1, "ToF offset[s]:",
                                    self.scientific_tof_offset)

        if platform.system() == "Darwin":
            self.scientific_tof_offset.scientificLineEdit.setFixedWidth(170)
            self.scientific_tof_slope.scientificLineEdit.setFixedWidth(170)

        # Save as and load
        self.saveButton.clicked.connect(self.__save_file)
        self.loadButton.clicked.connect(self.__load_file)

        self.show_settings()

    def get_original_property_values(self):
        """Returns the original values of the widget's properties.
        """
        return self.__original_properties

    def __load_file(self):
        """Load settings from file.
        """
        file = fdialogs.open_file_dialog(self, self.request.default_folder,
                                         "Select detector file",
                                         "Detector File (*.detector)")
        if file is None:
            return

        temp_detector = Detector.from_file(file, self.request, False)
        self.obj.set_settings(**temp_detector.get_settings())

        self.tmp_foil_info = []
        self.tof_foils = []
        self.detector_structure_widgets = []
        # Remove old widgets
        for i in range(self.detectorScrollAreaContents.layout().count()):
            layout_item = self.detectorScrollAreaContents.layout().itemAt(i)
            if layout_item == self.foils_layout:
                self.detectorScrollAreaContents.layout().removeItem(
                    layout_item)
                for j in reversed(range(layout_item.count())):
                    layout_item.itemAt(j).widget().deleteLater()

        # Add foil widgets and foil objects
        self.foils_layout = self._add_default_foils(temp_detector)
        self.detectorScrollAreaContents.layout().addLayout(self.foils_layout)

        self.show_settings()

    def __save_file(self):
        """Opens file dialog and sets and saves the settings to a file.
        """
        file = fdialogs.save_file_dialog(self, self.request.default_folder,
                                         "Save detector file",
                                         "Detector File (*.detector)")
        if file is None:
            return

        file = file.with_suffix(".detector")
        if not self.some_values_changed():
            self.obj.to_file(file)
        else:
            # Make temp detector, modify it according to widget values,
            # and write it to file.
            temp_detector = copy.deepcopy(self.obj)
            original_obj = self.obj
            self.obj = temp_detector
            self.update_settings()
            self.obj.to_file(file)
            self.obj = original_obj

    def show_settings(self):
        """Show Detector settings.
        """
        # Detector settings
        self.set_properties(**self.obj.get_settings())
        self.efficiency_files = self.obj.get_efficiency_files()

        # Detector foils
        self.tmp_foil_info = copy.deepcopy(self.obj.foils)
        self.calculate_distance()

        # Tof foils
        self.tof_foils = copy.deepcopy(self.obj.tof_foils)

    def update_settings(self):
        """Update detector settings.
        """
        self.obj.set_settings(**self.get_properties())
        # Detector foils
        self.calculate_distance()
        self.obj.foils = self.tmp_foil_info
        # Tof foils
        self.obj.tof_foils = self.tof_foils

    def some_values_changed(self):
        """Check if any detector settings values have been changed.

        Return:
            True or False.
        """
        if self.values_changed():
            return True
        if self.other_values_changed():
            return True
        return False

    def values_changed(self):
        """Check if detector settings values that require rerunning of the
        simulation have been changed.

        Return:
            True or False.
        """
        if self.are_values_changed():
            return True

        # TODO refactor foils
        # Detector foils
        self.calculate_distance()
        if self.foils_changed():
            return True
        # Tof foils
        if self.obj.tof_foils != self.tof_foils:
            return True

        return False

    def other_values_changed(self):
        """Check if detector settings values that don't require rerunning
        simulations have been changed.

        Return:
             True or False.
        """
        if self.obj.name != self.name:
            return True
        if self.obj.description != self.description:
            return True

        return False

    def foils_changed(self):
        """Check if detector foils have been changed.

        Return:
            True or False.
        """
        if len(self.obj.foils) != len(self.tmp_foil_info):
            return True
        for i in range(len(self.obj.foils)):
            foil = self.obj.foils[i]
            tmp_foil = self.tmp_foil_info[i]
            if type(foil) != type(tmp_foil):
                return True
            if foil.name != tmp_foil.name:
                return True
            if foil.distance != tmp_foil.distance:
                return True
            if foil.transmission != tmp_foil.transmission:
                return True
            # Check layers
            if self.layers_changed(foil, tmp_foil):
                return True
            if type(foil) is CircularFoil:
                if foil.diameter != tmp_foil.diameter:
                    return True
            else:
                if foil.size != tmp_foil.size:
                    return True
        return False

    def layers_changed(self, foil1, foil2):
        """Check if foil1 has different layers than foil2.

        Args:
            foil1: Foil object.
            foil2: Foil object.

        Return:
            True or False.
        """
        if len(foil1.layers) != len(foil2.layers):
            return True
        for i in range(len(foil1.layers)):
            layer1 = foil1.layers[i]
            layer2 = foil2.layers[i]
            if layer1.name != layer2.name:
                return True
            if layer1.thickness != layer2.thickness:
                return True
            if layer1.density != layer2.density:
                return True
            if layer1.start_depth != layer2.start_depth:
                return True
            # Check layer elements
            if self.layer_elements_changed(layer1, layer2):
                return True
        return False

    @staticmethod
    def layer_elements_changed(layer1, layer2):
        """Check if layer1 elements are different than layer2 elements.

        Args:
            layer1: Layer object.
            layer2: Layer object.

        Return:
            True or False.
        """
        if len(layer1.elements) != len(layer2.elements):
            return True
        for i in range(len(layer1.elements)):
            elem1 = layer1.elements[i]
            elem2 = layer2.elements[i]
            if elem1 != elem2:
                return True
        return False

    def _add_new_foil(self, layout, new_foil=None):
        """Add a new foil into detector.
        Args:
            layout: Layout into which the foil widget is added.
            new_foil: New Foil object to be added.
        """
        if self.tmp_foil_info:
            prev_distance = self.tmp_foil_info[-1].distance
        else:
            prev_distance = 0.0

        if new_foil is None:
            new_foil = CircularFoil(distance=prev_distance)

        foil_widget = FoilWidget(new_foil)
        foil_widget.distance_from_previous = new_foil.distance - prev_distance

        foil_widget.distanceDoubleSpinBox.valueChanged.connect(
            self.calculate_distance)
        foil_widget.foil_deletion.connect(self.delete_foil)
        foil_widget.foilButton.clicked.connect(self._open_foil_dialog)
        foil_widget.timingFoilCheckBox.stateChanged.connect(
            self._check_and_add)

        layout.addWidget(foil_widget)
        self.tmp_foil_info.append(new_foil)
        self.detector_structure_widgets.append(foil_widget)

        if len(self.tof_foils) >= 2:
            foil_widget.timingFoilCheckBox.setEnabled(False)
        return foil_widget

    def _add_default_foils(self, detector: Detector):
        """Add default foils as widgets.

        Return:
            Layout that holds the default foil widgets.
        """
        layout = QtWidgets.QHBoxLayout()

        foils = detector.foils
        for i in range(len(foils)):
            foil_widget = self._add_new_foil(layout, foils[i])
            for index in detector.tof_foils:
                if index == i:
                    foil_widget.timingFoilCheckBox.setChecked(True)
        return layout

    def _check_and_add(self):
        """Check if foil widget needs to be added into tof_foils list or
        removed from it.
        """
        check_box = self.sender()
        for i in range(len(self.detector_structure_widgets)):
            if self.detector_structure_widgets[i].timingFoilCheckBox is \
                    self.sender():
                if check_box.isChecked():
                    if self.tof_foils:
                        if self.tof_foils[0] > i:
                            self.tof_foils.insert(0, i)
                        else:
                            self.tof_foils.append(i)
                        if len(self.tof_foils) >= 2:
                            self._disable_checkboxes()
                    else:
                        self.tof_foils.append(i)
                else:
                    self.tof_foils.remove(i)
                    if 0 < len(self.tof_foils) < 2:
                        self._enable_checkboxes()
                break

    def _disable_checkboxes(self):
        """Disable selection of foil widgets as tof foil if they are not in the
        tof_foils list.
        """
        for i in range(len(self.detector_structure_widgets)):
            if i not in self.tof_foils:
                widget = self.detector_structure_widgets[i]
                widget.timingFoilCheckBox.setEnabled(False)

    def _enable_checkboxes(self):
        """Allow all foil widgets to be selected as tof foil.
        """
        for i in range(len(self.detector_structure_widgets)):
            widget = self.detector_structure_widgets[i]
            widget.timingFoilCheckBox.setEnabled(True)

    def _enable_remove_btn(self):
        """Sets the remove button either enabled or not depending whether an
        item is selected in the efficiency file list.
        """
        self.removeEfficiencyButton.setEnabled(
            bool(self.efficiencyListWidget.currentItem()))

    def _open_foil_dialog(self):
        """Open the FoilDialog which is used to modify the Foil object.
        """
        foil_name = self.sender().text()
        foil_object_index = -1
        for i in range(len(self.tmp_foil_info)):
            if foil_name == self.tmp_foil_info[i].name:
                foil_object_index = i
                break
        FoilDialog(self.tmp_foil_info, foil_object_index, self.icon_manager)
        self.sender().setText(self.tmp_foil_info[foil_object_index].name)

    def __add_efficiency(self):
        """Opens a dialog that allows the user to add efficiency files to
        the Efficiency_files folder of the detector.
        """
        eff_folder = gutils.get_potku_setting(
            DetectorSettingsWidget.EFF_FILE_FOLDER_KEY,
            self.request.default_folder)

        new_eff_files = fdialogs.open_files_dialog(self, eff_folder,
                                                   "Select efficiency files",
                                                   "Efficiency File (*.eff)")
        if not new_eff_files:
            return

        used_eff_files = {
            Detector.get_used_efficiency_file_name(f)
            for f in self.efficiency_files
        }

        for eff_file in new_eff_files:
            used_eff_file = Detector.get_used_efficiency_file_name(eff_file)

            if used_eff_file not in used_eff_files:
                try:
                    self.obj.add_efficiency_file(eff_file)
                    used_eff_files.add(used_eff_file)
                except OSError as e:
                    QtWidgets.QMessageBox.critical(
                        self, "Error",
                        f"Failed to add the efficiency file: {e}\n",
                        QtWidgets.QMessageBox.Ok, QtWidgets.QMessageBox.Ok)
            else:
                QtWidgets.QMessageBox.critical(
                    self, "Error",
                    f"There already is an efficiency file for element "
                    f"{used_eff_file.stem}.\n", QtWidgets.QMessageBox.Ok,
                    QtWidgets.QMessageBox.Ok)

        self.efficiency_files = self.obj.get_efficiency_files()
        # Store the folder where we previously fetched an eff-file
        gutils.set_potku_setting(DetectorSettingsWidget.EFF_FILE_FOLDER_KEY,
                                 str(eff_file.parent))

    def __remove_efficiency(self):
        """Removes efficiency files from detector's efficiency directory and
        updates settings view.
        """
        self.efficiencyListWidget: QtWidgets.QListWidget
        selected_items = self.efficiencyListWidget.selectedItems()
        if selected_items:
            reply = QtWidgets.QMessageBox.question(
                self, "Confirmation",
                "Are you sure you want to delete selected efficiencies?",
                QtWidgets.QMessageBox.Yes | QtWidgets.QMessageBox.No
                | QtWidgets.QMessageBox.Cancel, QtWidgets.QMessageBox.Cancel)
            if reply == QtWidgets.QMessageBox.No or reply == \
                    QtWidgets.QMessageBox.Cancel:
                return

            for item in selected_items:
                selected_eff_file = item.data(QtCore.Qt.UserRole)
                self.obj.remove_efficiency_file(selected_eff_file)

            self.efficiency_files = self.obj.get_efficiency_files()
            self._enable_remove_btn()

    def __plot_efficiency(self):
        """
        Open efficiency plot widget
        """
        self.eff_folder = gutils.get_potku_setting(
            DetectorSettingsWidget.EFF_FILE_FOLDER_KEY,
            self.request.default_folder)
        self.efficiency_files = self.obj.get_efficiency_files()
        self.efficiency_files_list = []
        i = 0
        for file in self.efficiency_files:
            file_name = gf.get_root_dir() / self.eff_folder / str(
                self.efficiency_files[i])
            self.efficiency_files_list.append(file_name)
            i += 1
        EfficiencyDialog(self.efficiency_files_list, self)

    def __open_calibration_dialog(self):
        """
        Open the CalibrationDialog.
        """
        measurements = [
            self.request.samples.measurements.get_key_value(key)
            for key in self.request.samples.measurements.measurements
        ]
        CalibrationDialog(measurements, self.obj, self.run, self)

    def calculate_distance(self):
        """
        Calculate the distances of the foils from the target.
        """
        distance = 0
        for foil, widget in zip(self.tmp_foil_info,
                                self.detector_structure_widgets):
            distance += widget.distance_from_previous
            widget.cumulative_distance = distance
            foil.distance = distance

    def delete_foil(self, foil_widget):
        """Delete a foil from widgets and Foil objects.

        Args:
            foil_widget: Widget to be deleted. Its index is used to delete
            Foil objects as well.
        """
        index_of_item_to_be_deleted = self.detector_structure_widgets.index(
            foil_widget)
        del (self.detector_structure_widgets[index_of_item_to_be_deleted])
        foil_to_be_deleted = self.tmp_foil_info[index_of_item_to_be_deleted]
        # Check if foil to be deleted is in tof_foils and remove it fro the
        # tof_foils list if it is.
        if index_of_item_to_be_deleted in self.tof_foils:
            self.tof_foils.remove(index_of_item_to_be_deleted)
            if 0 < len(self.tof_foils) < 2:
                self._enable_checkboxes()
        self.tmp_foil_info.remove(foil_to_be_deleted)
        for i in range(len(self.tof_foils)):
            if self.tof_foils[i] > index_of_item_to_be_deleted:
                self.tof_foils[i] = self.tof_foils[i] - 1

        self.foils_layout.removeWidget(foil_widget)
        foil_widget.deleteLater()
        self.calculate_distance()