示例#1
0
    def remove_item(self, name, check_dialog=False):
        """Pushes a RemoveProjectItemCommand to the toolbox undo stack.

        Args:
            name (str): Item's name
            check_dialog (bool): If True, shows 'Are you sure?' message box
        """
        delete_data = int(
            self._settings.value("appSettings/deleteData",
                                 defaultValue="0")) != 0
        if check_dialog:
            msg = "Remove item <b>{}</b> from project? ".format(name)
            if not delete_data:
                msg += "Item data directory will still be available in the project directory after this operation."
            else:
                msg += "<br><br><b>Warning: Item data will be permanently lost after this operation.</b>"
            msg += "<br><br>Tip: Remove items by pressing 'Delete' key to bypass this dialog."
            # noinspection PyCallByClass, PyTypeChecker
            message_box = QMessageBox(
                QMessageBox.Question,
                "Remove Item",
                msg,
                buttons=QMessageBox.Ok | QMessageBox.Cancel,
                parent=self._toolbox,
            )
            message_box.button(QMessageBox.Ok).setText("Remove Item")
            answer = message_box.exec_()
            if answer != QMessageBox.Ok:
                return
        self._toolbox.undo_stack.push(
            RemoveProjectItemCommand(self, name, delete_data=delete_data))
示例#2
0
 def remove_all_items(self):
     """Pushes a RemoveAllProjectItemsCommand to the toolbox undo stack.
     """
     items_per_category = self._project_item_model.items_per_category()
     if not any(v for v in items_per_category.values()):
         self._logger.msg.emit("No project items to remove")
         return
     delete_data = int(
         self._settings.value("appSettings/deleteData",
                              defaultValue="0")) != 0
     msg = "Remove all items from project?"
     if not delete_data:
         msg += "Item data directory will still be available in the project directory after this operation."
     else:
         msg += "<br><br><b>Warning: Item data will be permanently lost after this operation.</b>"
     message_box = QMessageBox(
         QMessageBox.Question,
         "Remove All Items",
         msg,
         buttons=QMessageBox.Ok | QMessageBox.Cancel,
         parent=self._toolbox,
     )
     message_box.button(QMessageBox.Ok).setText("Remove Items")
     answer = message_box.exec_()
     if answer != QMessageBox.Ok:
         return
     links = self._toolbox.ui.graphicsView.links()
     self._toolbox.undo_stack.push(
         RemoveAllProjectItemsCommand(self,
                                      items_per_category,
                                      links,
                                      delete_data=delete_data))
    def get_project_directory(self):
        """Asks the user to select a new project directory. If the selected directory
        is already a Spine Toolbox project directory, asks if overwrite is ok. Used
        when opening a project from an old style project file (.proj).

        Returns:
            str: Path to project directory or an empty string if operation is canceled.
        """
        # Ask user for a new directory where to save the project
        answer = QFileDialog.getExistingDirectory(self._toolbox, "Select a project directory", os.path.abspath("C:\\"))
        if not answer:  # Canceled (american-english), cancelled (british-english)
            return ""
        if not os.path.isdir(answer):  # Check that it's a directory
            msg = "Selection is not a directory, please try again"
            # noinspection PyCallByClass, PyArgumentList
            QMessageBox.warning(self._toolbox, "Invalid selection", msg)
            return ""
        # Check if the selected directory is already a project directory and ask if overwrite is ok
        if os.path.isdir(os.path.join(answer, ".spinetoolbox")):
            msg = (
                "Directory \n\n{0}\n\nalready contains a Spine Toolbox project."
                "\n\nWould you like to overwrite it?".format(answer)
            )
            message_box = QMessageBox(
                QMessageBox.Question,
                "Overwrite?",
                msg,
                buttons=QMessageBox.Ok | QMessageBox.Cancel,
                parent=self._toolbox,
            )
            message_box.button(QMessageBox.Ok).setText("Overwrite")
            msgbox_answer = message_box.exec_()
            if msgbox_answer != QMessageBox.Ok:
                return ""
        return answer  # New project directory
示例#4
0
    def remove_project_items(self, *indexes, ask_confirmation=False):
        """Pushes a RemoveProjectItemsCommand to the toolbox undo stack.

        Args:
            *indexes (QModelIndex): Indexes of the items in project item model
            ask_confirmation (bool): If True, shows 'Are you sure?' message box
        """
        indexes = list(indexes)
        delete_data = int(
            self._settings.value("appSettings/deleteData",
                                 defaultValue="0")) != 0
        if ask_confirmation:
            names = ", ".join(ind.data() for ind in indexes)
            msg = f"Remove item(s) <b>{names}</b> from project? "
            if not delete_data:
                msg += "Item data directory will still be available in the project directory after this operation."
            else:
                msg += "<br><br><b>Warning: Item data will be permanently lost after this operation.</b>"
            msg += "<br><br>Tip: Remove items by pressing 'Delete' key to bypass this dialog."
            # noinspection PyCallByClass, PyTypeChecker
            message_box = QMessageBox(
                QMessageBox.Question,
                "Remove Item",
                msg,
                buttons=QMessageBox.Ok | QMessageBox.Cancel,
                parent=self._toolbox,
            )
            message_box.button(QMessageBox.Ok).setText("Remove Item")
            answer = message_box.exec_()
            if answer != QMessageBox.Ok:
                return
        self._toolbox.undo_stack.push(
            RemoveProjectItemsCommand(self, *indexes, delete_data=delete_data))
 def save_datapackage(self, checked=False):
     """Write datapackage to file 'datapackage.json' in data directory."""
     if os.path.isfile(
             os.path.join(self._data_connection.data_dir,
                          "datapackage.json")):
         msg = ('<b>Replacing file "datapackage.json" in "{}"</b>. '
                'Are you sure?').format(
                    os.path.basename(self._data_connection.data_dir))
         message_box = QMessageBox(QMessageBox.Question,
                                   "Replace 'datapackage.json",
                                   msg,
                                   QMessageBox.Ok | QMessageBox.Cancel,
                                   parent=self)
         message_box.button(QMessageBox.Ok).setText("Replace File")
         answer = message_box.exec_()
         if answer == QMessageBox.Cancel:
             return False
     if self.datapackage.save(
             os.path.join(self._data_connection.data_dir,
                          'datapackage.json')):
         msg = '"datapackage.json" saved in {}'.format(
             self._data_connection.data_dir)
         self.msg.emit(msg)
         return True
     msg = 'Failed to save "datapackage.json" in {}'.format(
         self._data_connection.data_dir)
     self.msg_error.emit(msg)
     return False
示例#6
0
    def ask(
        parent,
        title_txt: str = _('Frage'),
        message: str = '',
        ok_btn_txt: str = _('OK'),
        abort_btn_txt: str = _('Abbrechen')
    ) -> bool:
        """ Pop-Up Warning Box ask to continue or break, returns False on abort """
        msg_box = QMessageBox(QMessageBox.Question,
                              title_txt,
                              message,
                              parent=parent)

        msg_box.setStandardButtons(QMessageBox.Ok | QMessageBox.Abort)

        msg_box.button(QMessageBox.Ok).setText(ok_btn_txt)
        msg_box.button(QMessageBox.Abort).setText(abort_btn_txt)

        # Block until answered
        answer = msg_box.exec()

        if answer == QMessageBox.Abort:
            # User selected abort
            return False

        # User selected -Ok-, continue
        return True
示例#7
0
    def action_apply(self):
        """
        Apply parameters to relevant .nif files
        """
        if self.nif_files_list_widget.count() == 0:
            QMessageBox.warning(self, "No .nif files loaded",
                                "Don't forget to load .nif files !")
            return

        if self.nif_files_list_widget.count() >= get_config().getint(
                "DEFAULT", "softLimit"):
            box = QMessageBox()
            box.setIcon(QMessageBox.Question)
            box.setWindowTitle('Are you sure ?')
            box.setText(
                "The tool may struggle with more than 100 .nif files at once. We advise you to process small batches.\n\nAre you sure you wish to continue ?"
            )
            box.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
            buttonY = box.button(QMessageBox.Yes)
            buttonY.setText('Yes')
            buttonN = box.button(QMessageBox.No)
            buttonN.setText('No')
            box.exec_()

            if box.clickedButton() == buttonN:
                return

        log.info("Applying parameters to " +
                 str(self.nif_files_list_widget.count()) + " files ...")
        self.toggle(False)
        self.progress_bar.setValue(0)
        self.processed_files = itertools.count()

        CONFIG.set("NIF", "Glossiness", str(self.spin_box_glossiness.value())),
        CONFIG.set("NIF", "SpecularStrength",
                   str(self.spin_box_specular_strength.value())),
        save_config()

        QMessageBox.warning(
            self, "Attention !",
            "The process is quite slow.\n\nThe gui will be mostly unresponsive to your input. Don't close the application, unless the completion pourcentage has not been updated in a long time (several minutes).\nIt took me 13 minutes to process 100 files for example."
        )

        #for indices in chunkify(range(self.nif_files_list_widget.count()), QThreadPool.globalInstance().maxThreadCount()-1):
        QThreadPool.globalInstance().setExpiryTimeout(-1)
        for index in range(self.nif_files_list_widget.count()):
            item = self.nif_files_list_widget.item(index)
            worker = NifProcessWorker(
                index=index,
                path=item.text(),
                keywords=self.keywords,
                glossiness=self.spin_box_glossiness.value(),
                specular_strength=self.spin_box_specular_strength.value())
            worker.signals.start.connect(self.start_apply_action)
            worker.signals.result.connect(self.result_apply_action)
            worker.signals.finished.connect(self.finish_apply_action)
            QThreadPool.globalInstance().start(worker)
示例#8
0
def rename_dir(old_dir, new_dir, toolbox, box_title):
    """Renames directory. Called by ``ProjectItemModel.set_item_name()``

    Args:
        old_dir (str): Absolute path to directory that will be renamed
        new_dir (str): Absolute path to new directory
        toolbox (ToolboxUI): A toolbox to log messages and ask questions.
        box_title (str): The title of the message boxes, (e.g. "Undoing 'rename DC1 to DC2'")

    Returns:
        bool: True if operation was successful, False otherwise
    """
    if os.path.exists(new_dir):
        msg = "Directory <b>{0}</b> already exists.<br/><br/>Would you like to overwrite its contents?".format(new_dir)
        box = QMessageBox(
            QMessageBox.Question, box_title, msg, buttons=QMessageBox.Ok | QMessageBox.Cancel, parent=toolbox
        )
        box.button(QMessageBox.Ok).setText("Overwrite")
        answer = box.exec_()
        if answer != QMessageBox.Ok:
            return False
        shutil.rmtree(new_dir)
    try:
        shutil.move(old_dir, new_dir)
    except FileExistsError:
        # This is unlikely because of the above `if`, but still possible since another concurrent process
        # might have done things in between
        msg = "Directory<br/><b>{0}</b><br/>already exists".format(new_dir)
        toolbox.information_box.emit(box_title, msg)
        return False
    except PermissionError as pe_e:
        logging.error(pe_e)
        msg = (
            "Access to directory <br/><b>{0}</b><br/>denied."
            "<br/><br/>Possible reasons:"
            "<br/>1. You don't have a permission to edit the directory"
            "<br/>2. Windows Explorer is open in the directory"
            "<br/><br/>Check these and try again.".format(old_dir)
        )
        toolbox.information_box.emit(box_title, msg)
        return False
    except OSError as os_e:
        logging.error(os_e)
        msg = (
            "Renaming directory "
            "<br/><b>{0}</b> "
            "<br/>to "
            "<br/><b>{1}</b> "
            "<br/>failed."
            "<br/><br/>Possibly reasons:"
            "<br/>1. Windows Explorer is open in the directory."
            "<br/>2. A file in the directory is open in another program. "
            "<br/><br/>Check these and try again.".format(old_dir, new_dir)
        )
        toolbox.information_box.emit(box_title, msg)
        return False
    return True
示例#9
0
 def get_permission(self, *filepaths):
     start_dir = self.datapackage.base_path
     filepaths = [os.path.relpath(path, start_dir) for path in filepaths if os.path.isfile(path)]
     if not filepaths:
         return True
     pathlist = "".join([f"<li>{path}</li>" for path in filepaths])
     msg = f"The following file(s) in <b>{os.path.basename(start_dir)}</b> will be replaced: <ul>{pathlist}</ul>. Are you sure?"
     message_box = QMessageBox(
         QMessageBox.Question, "Replacing file(s)", msg, QMessageBox.Ok | QMessageBox.Cancel, parent=self
     )
     message_box.button(QMessageBox.Ok).setText("Replace")
     return message_box.exec_() != QMessageBox.Cancel
示例#10
0
 def confirmSupprEle(self):
     print("confirm suppr")
     msgSuppr = QMessageBox()
     msgSuppr.setWindowTitle("Suppression")
     msgSuppr.setText("Confirmer la suppression de l'élève: {}".format(self.ui2.BDcbEleSupprSelectNom.currentText()))
     msgSuppr.setStandardButtons(QMessageBox.Yes| QMessageBox.No)
     buttonY = msgSuppr.button(QMessageBox.Yes)
     buttonN = msgSuppr.button(QMessageBox.No)
     msgSuppr.exec()
     if msgSuppr.clickedButton() == buttonY:
         self.supprEle()
     elif msgSuppr.clickedButton() == buttonN:
         pass
示例#11
0
 def get_msg(self, mess='修改'):
     # QMessageBox.information(self,'消息提示框','点击yes只删除设置月份,点击yesall删除所有')
     msg = QMessageBox()  # 创建一个对话框
     msg.setWindowTitle('消息提示框')  # 设置标题
     msg.setText(f'{mess}当月信息,如需{mess}所有请点击{mess}所有')  # 设置提示信息
     msg.setStandardButtons(QMessageBox.Yes | QMessageBox.YesAll)  # 添加两个标准按钮
     msg.button(QMessageBox.Yes).setText('{}'.format(mess))  # 设置按钮的文本
     msg.button(QMessageBox.YesAll).setText(f'{mess}所有')
     m = msg.exec()  # 显示提示框
     if m == QMessageBox.Yes:
         return 'yes'
     elif m == QMessageBox.YesAll:
         return 'yesall'
示例#12
0
 def confirmModifEle(self):
     print("confirm modif")
     msgModif = QMessageBox()
     msgModif.setWindowTitle("Modification")
     msgModif.setText(
         "Confirmer la modification de l'élève: {}".format(self.ui2.BDcbEleModifSelectNom.currentText()))
     msgModif.setStandardButtons(QMessageBox.Yes | QMessageBox.No)
     buttonY= msgModif.button(QMessageBox.Yes)
     buttonN= msgModif.button(QMessageBox.No)
     msgModif.exec()
     if msgModif.clickedButton() == buttonY:
         self.modifEle()
     elif msgModif.clickedButton() == buttonN:
         pass
示例#13
0
    def closeEvent(self, event):
        """It requests the user's confirmation to close the system."""
        box = QMessageBox(self)
        box.setIcon(QMessageBox.Question)
        box.setWindowTitle(App._text['close_window'])
        box.setText(App._text['msg_close_window'])
        box.setStandardButtons(QMessageBox.Yes|QMessageBox.No)
        btn_y = box.button(QMessageBox.Yes)
        btn_y.setText(App._text['yes'])
        btn_n = box.button(QMessageBox.No)
        btn_n.setText(App._text['no'])
        box.exec_()

        if box.clickedButton() == btn_y:
            event.accept()
        else:
            event.ignore()
 def ok_clicked(self):
     """Check that project name is valid and create project."""
     self.dir = self.ui.lineEdit_project_dir.text()
     if self.dir == "":
         # noinspection PyCallByClass, PyArgumentList
         QMessageBox.information(self, "Note",
                                 "Please select a project directory")
         return
     if os.path.isdir(os.path.join(self.dir, ".spinetoolbox")):
         msg = (
             "Directory \n\n{0}\n\nalready contains a Spine Toolbox project."
             "\nWould you like to overwrite the existing project?".format(
                 self.dir))
         message_box = QMessageBox(QMessageBox.Question,
                                   "Overwrite?",
                                   msg,
                                   buttons=QMessageBox.Ok
                                   | QMessageBox.Cancel,
                                   parent=self)
         message_box.button(QMessageBox.Ok).setText("Overwrite")
         answer = message_box.exec_()
         if answer != QMessageBox.Ok:
             return
     self.name = self.ui.lineEdit_project_name.text()
     self.description = self.ui.textEdit_description.toPlainText()
     if self.name == "":
         # noinspection PyCallByClass, PyArgumentList
         QMessageBox.information(self, "Note",
                                 "Please give the project a name")
         return
     # Check for invalid characters for a folder name
     if any((True for x in self.name if x in INVALID_CHARS)):
         # noinspection PyCallByClass, PyArgumentList
         QMessageBox.warning(
             self,
             "Invalid name",
             "Project name contains invalid character(s)."
             "\nCharacters {0} are not allowed.".format(
                 " ".join(INVALID_CHARS)),
         )
         return
     # Create new project
     self.call_create_project()
     self.close()
def showExceptionBox(logMessage):
    if QApplication.instance() is not None:
        errorBox = QMessageBox()
        errorBox.setWindowTitle('Error')
        errorBox.setText("Oops. An unexpected error occurred:\n{0}".format(logMessage))
        errorBox.setStandardButtons(QMessageBox.Ok | QMessageBox.Close)
        buttonCopy = errorBox.button(QMessageBox.Ok)
        buttonCopy.setText('Copy exception log...')
        buttonCopy.clicked.connect(lambda: pyperclip.copy(logMessage))
        errorBox.exec_()
示例#16
0
 def closeEvent(self, event):
     print("confirm exit")
     msgExit = QMessageBox()
     msgExit.setWindowTitle("exit")
     msgExit.setText("Voulez-vous sauvegarder les changements effectués avant de quitter ?")
     msgExit.setStandardButtons(QMessageBox.Save | QMessageBox.Cancel | QMessageBox.Discard)
     buttonS = msgExit.button(QMessageBox.Save)
     buttonD = msgExit.button(QMessageBox.Discard)
     buttonC = msgExit.button(QMessageBox.Cancel)
     msgExit.exec()
     if msgExit.clickedButton() == buttonS:
         print("save")
         # self.sauveJSON(filename)
         self.upd.emit()
         event.accept()
     elif msgExit.clickedButton() == buttonD:
         print("discard")
         event.accept()
     elif msgExit.clickedButton() == buttonC:
         print("cancel")
         event.ignore()
示例#17
0
    def on_action_chongzhi_clicked(self, checked ):
        # ret = QMessageBox.warning(self, '提示信息', '确定重置吗???!', QStringLiteral("确定"),QStringLiteral("取消"))
        msgbox = QMessageBox(self)  # 指定父窗口控件
        msgbox.setWindowTitle('提示信息')  # 对话框标题
        msgbox.setText("确定重置吗???")  # 设置文本
        msgbox.setStandardButtons(QMessageBox.Yes | QMessageBox.No) # 设置对话框有几个按钮
        msgbox.button(QMessageBox.Yes).setText("确定")    # 设置按钮文本
        msgbox.button(QMessageBox.No).setText("取消")     # 设置按钮文本
        # msgbox.button(QMessageBox.Cancel).setText("结束")   #还有abort,retry,ignore按钮
        # msgbox.setGeometry(500,500,0,0)     #消息框位置、大小设置
        msgbox.setIcon(QMessageBox.Question)  # 图标图片:QMessageBox.information信息框,QMessageBox.question问答框,
        # QMessageBox.warning警告框,QMessageBox.ctitical危险框,QMessageBox.about关于框
        result = msgbox.exec() # 执行对话框,并获取返回值

        if result == QMessageBox.Yes:
            self.ui.Text_XingMing.setText('')
            self.ui.Text_XingBie.setCurrentText('')
            self.ui.Text_BuMen.setText('')
            self.ui.Text_GongHao.setText('')
            self.ui.Text_ZuBie.setText('')
            self.ui.Text_ZhiWei.setText('')
            self.ui.Text_LianXiDianHua.setText('')
            self.ui.Text_ShenFenZhengHaoMa.setText('')
            self.ui.Text_GongZuoNianFen.setText('')
            self.ui.Text_ChuSheng_RiQi.setText('')
            self.Text_RuZhi.clear()
            self.ui.Text_DaiYu.clear()
            self.ui.Text_GongHao.setText('')
            self.ui.Text_DiZhi.clear()
            self.ui.Text_JinJiLianXiRen.clear()
            self.ui.Text_JinJiLianXiHaoMa.clear()
            # self.Text_ChuSheng.clear()
            self.Text_HeTong.clear()
            self.Text_TiaoXin.clear()
            self.Text_LiZhi.clear()
            self.ui.Text_BeiZhu.clear()
            self.ui.Text_TuPian.clear()

        else:
            pass
示例#18
0
def messageBoxQuestion(parent=None,
                       title='',
                       text='',
                       buttons=(QMessageBox.Yes | QMessageBox.No)):
    messageBox = QMessageBox(QMessageBox.Question, title, text, buttons,
                             parent)

    button_yes = messageBox.button(QMessageBox.Yes)
    if button_yes is not None:
        button_yes.setText(_('Yes'))

    button_no = messageBox.button(QMessageBox.No)
    if button_no is not None:
        button_no.setText(_('No'))

    button_save = messageBox.button(QMessageBox.Save)
    if button_save is not None:
        button_save.setText(_('Save'))

    button_cancel = messageBox.button(QMessageBox.Cancel)
    if button_cancel is not None:
        button_cancel.setText(_('Cancel'))

    return messageBox.exec_()
示例#19
0
 def remove_files(self):
     """Remove selected files from data directory."""
     indexes = self._properties_ui.treeView_dc_data.selectedIndexes()
     if not indexes:  # Nothing selected
         self._logger.msg.emit("Please select files to remove")
         return
     file_list = list()
     for index in indexes:
         file_at_index = self.data_model.itemFromIndex(index).data(
             Qt.DisplayRole)
         file_list.append(file_at_index)
     files = "\n".join(file_list)
     msg = (
         "The following files will be removed permanently from the project\n\n"
         "{0}\n\n"
         "Are you sure?".format(files))
     title = "Remove {0} File(s)".format(len(file_list))
     message_box = QMessageBox(QMessageBox.Question,
                               title,
                               msg,
                               QMessageBox.Ok | QMessageBox.Cancel,
                               parent=self._toolbox)
     message_box.button(QMessageBox.Ok).setText("Remove Files")
     answer = message_box.exec_()
     if answer == QMessageBox.Cancel:
         return
     for filename in file_list:
         path_to_remove = os.path.join(self.data_dir, filename)
         try:
             os.remove(path_to_remove)
             self._logger.msg.emit(f"File <b>{path_to_remove}</b> removed")
         except OSError:
             self._logger.msg_error.emit(
                 f"Removing file {path_to_remove} failed.\nCheck permissions."
             )
     return
示例#20
0
    def winBox(self):
        MessageBox = QMessageBox()
        MessageBox.setStyleSheet(
            "QLabel{min-width: 150px; min-height: 50px; color: #FF8C00;} QPushButton{min-width: 120px; min-height: 40px;} QMessageBox { background-color: #323232; font-size: 24px;}"
        )
        MessageBox.setText('YOU WIN!')

        MessageBox.setStandardButtons(QMessageBox.Ok)

        buttonOK = MessageBox.button(QMessageBox.Ok)
        buttonOK.setText('OK!')
        buttonOK.setStyleSheet(
            'background-color: #ff1e56; border-radius: 10px; font-size:20px')
        MessageBox.exec()

        if MessageBox.clickedButton() == buttonOK:
            MessageBox.close()
示例#21
0
    def check_and_install_requirements(self):
        """Prompts user to install IPython and ipykernel if they are missing.
        After installing the required packages, installs kernelspecs for the
        selected Python if they are missing.

        Returns:
            Boolean value depending on whether or not the user chooses to proceed.
        """
        if not self.is_package_installed("ipykernel"):
            message = ("Python Console requires package <b>ipykernel</b>."
                       "<p>Do you want to install the package now?</p>")
            message_box = QMessageBox(
                QMessageBox.Question,
                "ipykernel Missing",
                message,
                QMessageBox.Ok | QMessageBox.Cancel,
                parent=self._toolbox,
            )
            message_box.button(QMessageBox.Ok).setText("Install ipykernel")
            answer = message_box.exec_()
            if answer == QMessageBox.Cancel:
                self._control.viewport().setCursor(self.normal_cursor)
                return False
            self._toolbox.msg.emit("*** Installing ipykernel ***")
            self.start_package_install_process("ipykernel")
            return True
        # Install kernelspecs for self.kernel_name if not already present
        kernel_specs = find_kernel_specs()
        spec_exists = self.kernel_name in kernel_specs
        executable_valid = False
        if spec_exists:
            spec = get_kernel_spec(self.kernel_name)
            executable_valid = os.path.exists(spec.argv[0])
        if not spec_exists or not executable_valid:
            message = (
                "IPython kernel specifications for the selected environment are missing. "
                "<p>Do you want to install kernel <b>{0}</b> specifications now?</p>"
                .format(self.kernel_name))
            message_box = QMessageBox(
                QMessageBox.Question,
                "Kernel Specs Missing",
                message,
                QMessageBox.Ok | QMessageBox.Cancel,
                parent=self._toolbox,
            )
            message_box.button(
                QMessageBox.Ok).setText("Install specifications")
            answer = message_box.exec_()
            if answer == QMessageBox.Cancel:
                self._control.viewport().setCursor(self.normal_cursor)
                return False
            self._toolbox.msg.emit(
                "*** Installing IPython kernel <b>{0}</b> specs ***".format(
                    self.kernel_name))
            self.start_kernelspec_install_process()
            # New specs installed, update the variable
            if self.install_proc_exec_mngr.wait_for_process_finished():
                kernel_specs = find_kernel_specs()
            else:
                self._toolbox.msg_error.emit(
                    "Failed to install IPython kernel specifications.")
                return False
        # Everything ready, start Python Console
        kernel_dir = kernel_specs[self.kernel_name]
        kernel_spec_anchor = "<a style='color:#99CCFF;' title='{0}' href='#'>{1}</a>".format(
            kernel_dir, self.kernel_name)
        self._toolbox.msg.emit(
            "\tStarting IPython kernel {0}".format(kernel_spec_anchor))
        self.start_python_kernel()
        return True
示例#22
0
class FittingResultViewer(QDialog):
    PAGE_ROWS = 20
    logger = logging.getLogger("root.QGrain.ui.FittingResultViewer")
    result_marked = Signal(SSUResult)

    def __init__(self, reference_viewer: ReferenceResultViewer, parent=None):
        super().__init__(parent=parent, f=Qt.Window)
        self.setWindowTitle(self.tr("SSU Fitting Result Viewer"))
        self.__fitting_results = []  # type: list[SSUResult]
        self.retry_tasks = {}  # type: dict[UUID, SSUTask]
        self.__reference_viewer = reference_viewer
        self.init_ui()
        self.boxplot_chart = BoxplotChart(parent=self, toolbar=True)
        self.typical_chart = SSUTypicalComponentChart(parent=self,
                                                      toolbar=True)
        self.distance_chart = DistanceCurveChart(parent=self, toolbar=True)
        self.mixed_distribution_chart = MixedDistributionChart(
            parent=self, toolbar=True, use_animation=True)
        self.file_dialog = QFileDialog(parent=self)
        self.async_worker = AsyncWorker()
        self.async_worker.background_worker.task_succeeded.connect(
            self.on_fitting_succeeded)
        self.async_worker.background_worker.task_failed.connect(
            self.on_fitting_failed)
        self.update_page_list()
        self.update_page(self.page_index)

        self.normal_msg = QMessageBox(self)
        self.remove_warning_msg = QMessageBox(self)
        self.remove_warning_msg.setStandardButtons(QMessageBox.No
                                                   | QMessageBox.Yes)
        self.remove_warning_msg.setDefaultButton(QMessageBox.No)
        self.remove_warning_msg.setWindowTitle(self.tr("Warning"))
        self.remove_warning_msg.setText(
            self.tr("Are you sure to remove all SSU results?"))
        self.outlier_msg = QMessageBox(self)
        self.outlier_msg.setStandardButtons(QMessageBox.Discard
                                            | QMessageBox.Retry
                                            | QMessageBox.Ignore)
        self.outlier_msg.setDefaultButton(QMessageBox.Ignore)
        self.retry_progress_msg = QMessageBox()
        self.retry_progress_msg.addButton(QMessageBox.Ok)
        self.retry_progress_msg.button(QMessageBox.Ok).hide()
        self.retry_progress_msg.setWindowTitle(self.tr("Progress"))
        self.retry_timer = QTimer(self)
        self.retry_timer.setSingleShot(True)
        self.retry_timer.timeout.connect(
            lambda: self.retry_progress_msg.exec_())

    def init_ui(self):
        self.data_table = QTableWidget(100, 100)
        self.data_table.setEditTriggers(QAbstractItemView.NoEditTriggers)
        self.data_table.setSelectionBehavior(QAbstractItemView.SelectRows)
        self.data_table.setAlternatingRowColors(True)
        self.data_table.setContextMenuPolicy(Qt.CustomContextMenu)
        self.main_layout = QGridLayout(self)
        self.main_layout.addWidget(self.data_table, 0, 0, 1, 3)

        self.previous_button = QPushButton(
            qta.icon("mdi.skip-previous-circle"), self.tr("Previous"))
        self.previous_button.setToolTip(
            self.tr("Click to back to the previous page."))
        self.previous_button.clicked.connect(self.on_previous_button_clicked)
        self.current_page_combo_box = QComboBox()
        self.current_page_combo_box.addItem(self.tr("Page {0}").format(1))
        self.current_page_combo_box.currentIndexChanged.connect(
            self.update_page)
        self.next_button = QPushButton(qta.icon("mdi.skip-next-circle"),
                                       self.tr("Next"))
        self.next_button.setToolTip(self.tr("Click to jump to the next page."))
        self.next_button.clicked.connect(self.on_next_button_clicked)
        self.main_layout.addWidget(self.previous_button, 1, 0)
        self.main_layout.addWidget(self.current_page_combo_box, 1, 1)
        self.main_layout.addWidget(self.next_button, 1, 2)

        self.distance_label = QLabel(self.tr("Distance"))
        self.distance_label.setToolTip(
            self.
            tr("It's the function to calculate the difference (on the contrary, similarity) between two samples."
               ))
        self.distance_combo_box = QComboBox()
        self.distance_combo_box.addItems(built_in_distances)
        self.distance_combo_box.setCurrentText("log10MSE")
        self.distance_combo_box.currentTextChanged.connect(
            lambda: self.update_page(self.page_index))
        self.main_layout.addWidget(self.distance_label, 2, 0)
        self.main_layout.addWidget(self.distance_combo_box, 2, 1, 1, 2)
        self.menu = QMenu(self.data_table)
        self.menu.setShortcutAutoRepeat(True)
        self.mark_action = self.menu.addAction(
            qta.icon("mdi.marker-check"),
            self.tr("Mark Selection(s) as Reference"))
        self.mark_action.triggered.connect(self.mark_selections)
        self.remove_selection_action = self.menu.addAction(
            qta.icon("fa.remove"), self.tr("Remove Selection(s)"))
        self.remove_selection_action.triggered.connect(self.remove_selections)
        self.remove_all_action = self.menu.addAction(qta.icon("fa.remove"),
                                                     self.tr("Remove All"))
        self.remove_all_action.triggered.connect(self.remove_all_results)
        self.plot_loss_chart_action = self.menu.addAction(
            qta.icon("mdi.chart-timeline-variant"), self.tr("Plot Loss Chart"))
        self.plot_loss_chart_action.triggered.connect(self.show_distance)
        self.plot_distribution_chart_action = self.menu.addAction(
            qta.icon("fa5s.chart-area"), self.tr("Plot Distribution Chart"))
        self.plot_distribution_chart_action.triggered.connect(
            self.show_distribution)
        self.plot_distribution_animation_action = self.menu.addAction(
            qta.icon("fa5s.chart-area"),
            self.tr("Plot Distribution Chart (Animation)"))
        self.plot_distribution_animation_action.triggered.connect(
            self.show_history_distribution)

        self.detect_outliers_menu = self.menu.addMenu(
            qta.icon("mdi.magnify"), self.tr("Detect Outliers"))
        self.check_nan_and_inf_action = self.detect_outliers_menu.addAction(
            self.tr("Check NaN and Inf"))
        self.check_nan_and_inf_action.triggered.connect(self.check_nan_and_inf)
        self.check_final_distances_action = self.detect_outliers_menu.addAction(
            self.tr("Check Final Distances"))
        self.check_final_distances_action.triggered.connect(
            self.check_final_distances)
        self.check_component_mean_action = self.detect_outliers_menu.addAction(
            self.tr("Check Component Mean"))
        self.check_component_mean_action.triggered.connect(
            lambda: self.check_component_moments("mean"))
        self.check_component_std_action = self.detect_outliers_menu.addAction(
            self.tr("Check Component STD"))
        self.check_component_std_action.triggered.connect(
            lambda: self.check_component_moments("std"))
        self.check_component_skewness_action = self.detect_outliers_menu.addAction(
            self.tr("Check Component Skewness"))
        self.check_component_skewness_action.triggered.connect(
            lambda: self.check_component_moments("skewness"))
        self.check_component_kurtosis_action = self.detect_outliers_menu.addAction(
            self.tr("Check Component Kurtosis"))
        self.check_component_kurtosis_action.triggered.connect(
            lambda: self.check_component_moments("kurtosis"))
        self.check_component_fractions_action = self.detect_outliers_menu.addAction(
            self.tr("Check Component Fractions"))
        self.check_component_fractions_action.triggered.connect(
            self.check_component_fractions)
        self.degrade_results_action = self.detect_outliers_menu.addAction(
            self.tr("Degrade Results"))
        self.degrade_results_action.triggered.connect(self.degrade_results)
        self.try_align_components_action = self.detect_outliers_menu.addAction(
            self.tr("Try Align Components"))
        self.try_align_components_action.triggered.connect(
            self.try_align_components)

        self.analyse_typical_components_action = self.menu.addAction(
            qta.icon("ei.tags"), self.tr("Analyse Typical Components"))
        self.analyse_typical_components_action.triggered.connect(
            self.analyse_typical_components)
        self.load_dump_action = self.menu.addAction(
            qta.icon("fa.database"), self.tr("Load Binary Dump"))
        self.load_dump_action.triggered.connect(self.load_dump)
        self.save_dump_action = self.menu.addAction(
            qta.icon("fa.save"), self.tr("Save Binary Dump"))
        self.save_dump_action.triggered.connect(self.save_dump)
        self.save_excel_action = self.menu.addAction(
            qta.icon("mdi.microsoft-excel"), self.tr("Save Excel"))
        self.save_excel_action.triggered.connect(
            lambda: self.on_save_excel_clicked(align_components=False))
        self.save_excel_align_action = self.menu.addAction(
            qta.icon("mdi.microsoft-excel"),
            self.tr("Save Excel (Force Alignment)"))
        self.save_excel_align_action.triggered.connect(
            lambda: self.on_save_excel_clicked(align_components=True))
        self.data_table.customContextMenuRequested.connect(self.show_menu)
        # necessary to add actions of menu to this widget itself,
        # otherwise, the shortcuts will not be triggered
        self.addActions(self.menu.actions())

    def show_menu(self, pos: QPoint):
        self.menu.popup(QCursor.pos())

    def show_message(self, title: str, message: str):
        self.normal_msg.setWindowTitle(title)
        self.normal_msg.setText(message)
        self.normal_msg.exec_()

    def show_info(self, message: str):
        self.show_message(self.tr("Info"), message)

    def show_warning(self, message: str):
        self.show_message(self.tr("Warning"), message)

    def show_error(self, message: str):
        self.show_message(self.tr("Error"), message)

    @property
    def distance_name(self) -> str:
        return self.distance_combo_box.currentText()

    @property
    def distance_func(self) -> typing.Callable:
        return get_distance_func_by_name(self.distance_combo_box.currentText())

    @property
    def page_index(self) -> int:
        return self.current_page_combo_box.currentIndex()

    @property
    def n_pages(self) -> int:
        return self.current_page_combo_box.count()

    @property
    def n_results(self) -> int:
        return len(self.__fitting_results)

    @property
    def selections(self):
        start = self.page_index * self.PAGE_ROWS
        temp = set()
        for item in self.data_table.selectedRanges():
            for i in range(item.topRow(),
                           min(self.PAGE_ROWS + 1,
                               item.bottomRow() + 1)):
                temp.add(i + start)
        indexes = list(temp)
        indexes.sort()
        return indexes

    def update_page_list(self):
        last_page_index = self.page_index
        if self.n_results == 0:
            n_pages = 1
        else:
            n_pages, left = divmod(self.n_results, self.PAGE_ROWS)
            if left != 0:
                n_pages += 1
        self.current_page_combo_box.blockSignals(True)
        self.current_page_combo_box.clear()
        self.current_page_combo_box.addItems(
            [self.tr("Page {0}").format(i + 1) for i in range(n_pages)])
        if last_page_index >= n_pages:
            self.current_page_combo_box.setCurrentIndex(n_pages - 1)
        else:
            self.current_page_combo_box.setCurrentIndex(last_page_index)
        self.current_page_combo_box.blockSignals(False)

    def update_page(self, page_index: int):
        def write(row: int, col: int, value: str):
            if isinstance(value, str):
                pass
            elif isinstance(value, int):
                value = str(value)
            elif isinstance(value, float):
                value = f"{value: 0.4f}"
            else:
                value = value.__str__()
            item = QTableWidgetItem(value)
            item.setTextAlignment(Qt.AlignCenter)
            self.data_table.setItem(row, col, item)

        # necessary to clear
        self.data_table.clear()
        if page_index == self.n_pages - 1:
            start = page_index * self.PAGE_ROWS
            end = self.n_results
        else:
            start, end = page_index * self.PAGE_ROWS, (page_index +
                                                       1) * self.PAGE_ROWS
        self.data_table.setRowCount(end - start)
        self.data_table.setColumnCount(7)
        self.data_table.setHorizontalHeaderLabels([
            self.tr("Resolver"),
            self.tr("Distribution Type"),
            self.tr("N_components"),
            self.tr("N_iterations"),
            self.tr("Spent Time [s]"),
            self.tr("Final Distance"),
            self.tr("Has Reference")
        ])
        sample_names = [
            result.sample.name for result in self.__fitting_results[start:end]
        ]
        self.data_table.setVerticalHeaderLabels(sample_names)
        for row, result in enumerate(self.__fitting_results[start:end]):
            write(row, 0, result.task.resolver)
            write(row, 1,
                  self.get_distribution_name(result.task.distribution_type))
            write(row, 2, result.task.n_components)
            write(row, 3, result.n_iterations)
            write(row, 4, result.time_spent)
            write(
                row, 5,
                self.distance_func(result.sample.distribution,
                                   result.distribution))
            has_ref = result.task.initial_guess is not None or result.task.reference is not None
            write(row, 6, self.tr("Yes") if has_ref else self.tr("No"))

        self.data_table.resizeColumnsToContents()

    def on_previous_button_clicked(self):
        if self.page_index > 0:
            self.current_page_combo_box.setCurrentIndex(self.page_index - 1)

    def on_next_button_clicked(self):
        if self.page_index < self.n_pages - 1:
            self.current_page_combo_box.setCurrentIndex(self.page_index + 1)

    def get_distribution_name(self, distribution_type: DistributionType):
        if distribution_type == DistributionType.Normal:
            return self.tr("Normal")
        elif distribution_type == DistributionType.Weibull:
            return self.tr("Weibull")
        elif distribution_type == DistributionType.SkewNormal:
            return self.tr("Skew Normal")
        else:
            raise NotImplementedError(distribution_type)

    def add_result(self, result: SSUResult):
        if self.n_results == 0 or \
            (self.page_index == self.n_pages - 1 and \
            divmod(self.n_results, self.PAGE_ROWS)[-1] != 0):
            need_update = True
        else:
            need_update = False
        self.__fitting_results.append(result)
        self.update_page_list()
        if need_update:
            self.update_page(self.page_index)

    def add_results(self, results: typing.List[SSUResult]):
        if self.n_results == 0 or \
            (self.page_index == self.n_pages - 1 and \
            divmod(self.n_results, self.PAGE_ROWS)[-1] != 0):
            need_update = True
        else:
            need_update = False
        self.__fitting_results.extend(results)
        self.update_page_list()
        if need_update:
            self.update_page(self.page_index)

    def mark_selections(self):
        for index in self.selections:
            self.result_marked.emit(self.__fitting_results[index])

    def remove_results(self, indexes):
        results = []
        for i in reversed(indexes):
            res = self.__fitting_results.pop(i)
            results.append(res)
        self.update_page_list()
        self.update_page(self.page_index)

    def remove_selections(self):
        indexes = self.selections
        self.remove_results(indexes)

    def remove_all_results(self):
        res = self.remove_warning_msg.exec_()
        if res == QMessageBox.Yes:
            self.__fitting_results.clear()
            self.update_page_list()
            self.update_page(0)

    def show_distance(self):
        results = [self.__fitting_results[i] for i in self.selections]
        if results is None or len(results) == 0:
            return
        result = results[0]
        self.distance_chart.show_distance_series(result.get_distance_series(
            self.distance_name),
                                                 title=result.sample.name)
        self.distance_chart.show()

    def show_distribution(self):
        results = [self.__fitting_results[i] for i in self.selections]
        if results is None or len(results) == 0:
            return
        result = results[0]
        self.mixed_distribution_chart.show_model(result.view_model)
        self.mixed_distribution_chart.show()

    def show_history_distribution(self):
        results = [self.__fitting_results[i] for i in self.selections]
        if results is None or len(results) == 0:
            return
        result = results[0]
        self.mixed_distribution_chart.show_result(result)
        self.mixed_distribution_chart.show()

    def load_dump(self):
        filename, _ = self.file_dialog.getOpenFileName(
            self, self.tr("Select a binary dump file of SSU results"), None,
            self.tr("Binary dump (*.dump)"))
        if filename is None or filename == "":
            return
        with open(filename, "rb") as f:
            results = pickle.load(f)  # type: list[SSUResult]
            valid = True
            if isinstance(results, list):
                for result in results:
                    if not isinstance(result, SSUResult):
                        valid = False
                        break
            else:
                valid = False

            if valid:
                if self.n_results != 0 and len(results) != 0:
                    old_classes = self.__fitting_results[0].classes_φ
                    new_classes = results[0].classes_φ
                    classes_inconsistent = False
                    if len(old_classes) != len(new_classes):
                        classes_inconsistent = True
                    else:
                        classes_error = np.abs(old_classes - new_classes)
                        if not np.all(np.less_equal(classes_error, 1e-8)):
                            classes_inconsistent = True
                    if classes_inconsistent:
                        self.show_error(
                            self.
                            tr("The results in the dump file has inconsistent grain-size classes with that in your list."
                               ))
                        return
                self.add_results(results)
            else:
                self.show_error(self.tr("The binary dump file is invalid."))

    def save_dump(self):
        if self.n_results == 0:
            self.show_warning(self.tr("There is not any result in the list."))
            return
        filename, _ = self.file_dialog.getSaveFileName(
            self, self.tr("Save the SSU results to binary dump file"), None,
            self.tr("Binary dump (*.dump)"))
        if filename is None or filename == "":
            return
        with open(filename, "wb") as f:
            pickle.dump(self.__fitting_results, f)

    def save_excel(self, filename, align_components=False):
        if self.n_results == 0:
            return

        results = self.__fitting_results.copy()
        classes_μm = results[0].classes_μm
        n_components_list = [
            result.n_components for result in self.__fitting_results
        ]
        count_dict = Counter(n_components_list)
        max_n_components = max(count_dict.keys())
        self.logger.debug(
            f"N_components: {count_dict}, Max N_components: {max_n_components}"
        )

        flags = []
        if not align_components:
            for result in results:
                flags.extend(range(result.n_components))
        else:
            n_components_desc = "\n".join([
                self.tr("{0} Component(s): {1}").format(n_components, count)
                for n_components, count in count_dict.items()
            ])
            self.show_info(
                self.tr("N_components distribution of Results:\n{0}").format(
                    n_components_desc))
            stacked_components = []
            for result in self.__fitting_results:
                for component in result.components:
                    stacked_components.append(component.distribution)
            stacked_components = np.array(stacked_components)
            cluser = KMeans(n_clusters=max_n_components)
            flags = cluser.fit_predict(stacked_components)
            # check flags to make it unique
            flag_index = 0
            for i, result in enumerate(self.__fitting_results):
                result_flags = set()
                for component in result.components:
                    if flags[flag_index] in result_flags:
                        if flags[flag_index] == max_n_components:
                            flags[flag_index] = max_n_components - 1
                        else:
                            flag_index[flag_index] += 1
                        result_flags.add(flags[flag_index])
                    flag_index += 1

            flag_set = set(flags)
            picked = []
            for target_flag in flag_set:
                for i, flag in enumerate(flags):
                    if flag == target_flag:
                        picked.append(
                            (target_flag,
                             logarithmic(classes_μm,
                                         stacked_components[i])["mean"]))
                        break
            picked.sort(key=lambda x: x[1])
            flag_map = {flag: index for index, (flag, _) in enumerate(picked)}
            flags = np.array([flag_map[flag] for flag in flags])

        wb = openpyxl.Workbook()
        prepare_styles(wb)
        ws = wb.active
        ws.title = self.tr("README")
        description = \
            """
            This Excel file was generated by QGrain ({0}).

            Please cite:
            Liu, Y., Liu, X., Sun, Y., 2021. QGrain: An open-source and easy-to-use software for the comprehensive analysis of grain size distributions. Sedimentary Geology 423, 105980. https://doi.org/10.1016/j.sedgeo.2021.105980

            It contanins 4 + max(N_components) sheets:
            1. The first sheet is the sample distributions of SSU results.
            2. The second sheet is used to put the infomation of fitting.
            3. The third sheet is the statistic parameters calculated by statistic moment method.
            4. The fouth sheet is the distributions of unmixed components and their sum of each sample.
            5. Other sheets are the unmixed end-member distributions which were discretely stored.

            The SSU algorithm is implemented by QGrain.

            """.format(QGRAIN_VERSION)

        def write(row, col, value, style="normal_light"):
            cell = ws.cell(row + 1, col + 1, value=value)
            cell.style = style

        lines_of_desc = description.split("\n")
        for row, line in enumerate(lines_of_desc):
            write(row, 0, line, style="description")
        ws.column_dimensions[column_to_char(0)].width = 200

        ws = wb.create_sheet(self.tr("Sample Distributions"))
        write(0, 0, self.tr("Sample Name"), style="header")
        ws.column_dimensions[column_to_char(0)].width = 16
        for col, value in enumerate(classes_μm, 1):
            write(0, col, value, style="header")
            ws.column_dimensions[column_to_char(col)].width = 10
        for row, result in enumerate(results, 1):
            if row % 2 == 0:
                style = "normal_dark"
            else:
                style = "normal_light"
            write(row, 0, result.sample.name, style=style)
            for col, value in enumerate(result.sample.distribution, 1):
                write(row, col, value, style=style)
            QCoreApplication.processEvents()

        ws = wb.create_sheet(self.tr("Information of Fitting"))
        write(0, 0, self.tr("Sample Name"), style="header")
        ws.column_dimensions[column_to_char(0)].width = 16
        headers = [
            self.tr("Distribution Type"),
            self.tr("N_components"),
            self.tr("Resolver"),
            self.tr("Resolver Settings"),
            self.tr("Initial Guess"),
            self.tr("Reference"),
            self.tr("Spent Time [s]"),
            self.tr("N_iterations"),
            self.tr("Final Distance [log10MSE]")
        ]
        for col, value in enumerate(headers, 1):
            write(0, col, value, style="header")
            if col in (4, 5, 6):
                ws.column_dimensions[column_to_char(col)].width = 10
            else:
                ws.column_dimensions[column_to_char(col)].width = 10
        for row, result in enumerate(results, 1):
            if row % 2 == 0:
                style = "normal_dark"
            else:
                style = "normal_light"
            write(row, 0, result.sample.name, style=style)
            write(row, 1, result.distribution_type.name, style=style)
            write(row, 2, result.n_components, style=style)
            write(row, 3, result.task.resolver, style=style)
            write(row,
                  4,
                  self.tr("Default") if result.task.resolver_setting is None
                  else result.task.resolver_setting.__str__(),
                  style=style)
            write(row,
                  5,
                  self.tr("None") if result.task.initial_guess is None else
                  result.task.initial_guess.__str__(),
                  style=style)
            write(row,
                  6,
                  self.tr("None") if result.task.reference is None else
                  result.task.reference.__str__(),
                  style=style)
            write(row, 7, result.time_spent, style=style)
            write(row, 8, result.n_iterations, style=style)
            write(row, 9, result.get_distance("log10MSE"), style=style)

        ws = wb.create_sheet(self.tr("Statistic Moments"))
        write(0, 0, self.tr("Sample Name"), style="header")
        ws.merge_cells(start_row=1, start_column=1, end_row=2, end_column=1)
        ws.column_dimensions[column_to_char(0)].width = 16
        headers = []
        sub_headers = [
            self.tr("Proportion"),
            self.tr("Mean [φ]"),
            self.tr("Mean [μm]"),
            self.tr("STD [φ]"),
            self.tr("STD [μm]"),
            self.tr("Skewness"),
            self.tr("Kurtosis")
        ]
        for i in range(max_n_components):
            write(0,
                  i * len(sub_headers) + 1,
                  self.tr("C{0}").format(i + 1),
                  style="header")
            ws.merge_cells(start_row=1,
                           start_column=i * len(sub_headers) + 2,
                           end_row=1,
                           end_column=(i + 1) * len(sub_headers) + 1)
            headers.extend(sub_headers)
        for col, value in enumerate(headers, 1):
            write(1, col, value, style="header")
            ws.column_dimensions[column_to_char(col)].width = 10
        flag_index = 0
        for row, result in enumerate(results, 2):
            if row % 2 == 0:
                style = "normal_light"
            else:
                style = "normal_dark"
            write(row, 0, result.sample.name, style=style)
            for component in result.components:
                index = flags[flag_index]
                write(row,
                      index * len(sub_headers) + 1,
                      component.fraction,
                      style=style)
                write(row,
                      index * len(sub_headers) + 2,
                      component.logarithmic_moments["mean"],
                      style=style)
                write(row,
                      index * len(sub_headers) + 3,
                      component.geometric_moments["mean"],
                      style=style)
                write(row,
                      index * len(sub_headers) + 4,
                      component.logarithmic_moments["std"],
                      style=style)
                write(row,
                      index * len(sub_headers) + 5,
                      component.geometric_moments["std"],
                      style=style)
                write(row,
                      index * len(sub_headers) + 6,
                      component.logarithmic_moments["skewness"],
                      style=style)
                write(row,
                      index * len(sub_headers) + 7,
                      component.logarithmic_moments["kurtosis"],
                      style=style)
                flag_index += 1

        ws = wb.create_sheet(self.tr("Unmixed Components"))
        ws.merge_cells(start_row=1, start_column=1, end_row=1, end_column=2)
        write(0, 0, self.tr("Sample Name"), style="header")
        ws.column_dimensions[column_to_char(0)].width = 16
        for col, value in enumerate(classes_μm, 2):
            write(0, col, value, style="header")
            ws.column_dimensions[column_to_char(col)].width = 10
        row = 1
        for result_index, result in enumerate(results, 1):
            if result_index % 2 == 0:
                style = "normal_dark"
            else:
                style = "normal_light"
            write(row, 0, result.sample.name, style=style)
            ws.merge_cells(start_row=row + 1,
                           start_column=1,
                           end_row=row + result.n_components + 1,
                           end_column=1)
            for component_i, component in enumerate(result.components, 1):
                write(row, 1, self.tr("C{0}").format(component_i), style=style)
                for col, value in enumerate(
                        component.distribution * component.fraction, 2):
                    write(row, col, value, style=style)
                row += 1
            write(row, 1, self.tr("Sum"), style=style)
            for col, value in enumerate(result.distribution, 2):
                write(row, col, value, style=style)
            row += 1

        ws_dict = {}
        flag_set = set(flags)
        for flag in flag_set:
            ws = wb.create_sheet(self.tr("Unmixed EM{0}").format(flag + 1))
            write(0, 0, self.tr("Sample Name"), style="header")
            ws.column_dimensions[column_to_char(0)].width = 16
            for col, value in enumerate(classes_μm, 1):
                write(0, col, value, style="header")
                ws.column_dimensions[column_to_char(col)].width = 10
            ws_dict[flag] = ws

        flag_index = 0
        for row, result in enumerate(results, 1):
            if row % 2 == 0:
                style = "normal_dark"
            else:
                style = "normal_light"

            for component in result.components:
                flag = flags[flag_index]
                ws = ws_dict[flag]
                write(row, 0, result.sample.name, style=style)
                for col, value in enumerate(component.distribution, 1):
                    write(row, col, value, style=style)
                flag_index += 1

        wb.save(filename)
        wb.close()

    def on_save_excel_clicked(self, align_components=False):
        if self.n_results == 0:
            self.show_warning(self.tr("There is not any SSU result."))
            return
        filename, _ = self.file_dialog.getSaveFileName(
            None, self.tr("Choose a filename to save SSU Results"), None,
            "Microsoft Excel (*.xlsx)")
        if filename is None or filename == "":
            return
        try:
            self.save_excel(filename, align_components)
            self.show_info(
                self.tr("SSU results have been saved to:\n    {0}").format(
                    filename))
        except Exception as e:
            self.show_error(
                self.
                tr("Error raised while save SSU results to Excel file.\n    {0}"
                   ).format(e.__str__()))

    def on_fitting_succeeded(self, result: SSUResult):
        result_replace_index = self.retry_tasks[result.task.uuid]
        self.__fitting_results[result_replace_index] = result
        self.retry_tasks.pop(result.task.uuid)
        self.retry_progress_msg.setText(
            self.tr("Tasks to be retried: {0}").format(len(self.retry_tasks)))
        if len(self.retry_tasks) == 0:
            self.retry_progress_msg.close()

        self.logger.debug(
            f"Retried task succeeded, sample name={result.task.sample.name}, distribution_type={result.task.distribution_type.name}, n_components={result.task.n_components}"
        )
        self.update_page(self.page_index)

    def on_fitting_failed(self, failed_info: str, task: SSUTask):
        # necessary to remove it from the dict
        self.retry_tasks.pop(task.uuid)
        if len(self.retry_tasks) == 0:
            self.retry_progress_msg.close()
        self.show_error(
            self.tr("Failed to retry task, sample name={0}.\n{1}").format(
                task.sample.name, failed_info))
        self.logger.warning(
            f"Failed to retry task, sample name={task.sample.name}, distribution_type={task.distribution_type.name}, n_components={task.n_components}"
        )

    def retry_results(self, indexes, results):
        assert len(indexes) == len(results)
        if len(results) == 0:
            return
        self.retry_progress_msg.setText(
            self.tr("Tasks to be retried: {0}").format(len(results)))
        self.retry_timer.start(1)
        for index, result in zip(indexes, results):
            query = self.__reference_viewer.query_reference(result.sample)
            ref_result = None
            if query is None:
                nearby_results = self.__fitting_results[
                    index - 5:index] + self.__fitting_results[index + 1:index +
                                                              6]
                ref_result = self.__reference_viewer.find_similar(
                    result.sample, nearby_results)
            else:
                ref_result = query
            keys = ["mean", "std", "skewness"]
            # reference = [{key: comp.logarithmic_moments[key] for key in keys} for comp in ref_result.components]
            task = SSUTask(
                result.sample,
                ref_result.distribution_type,
                ref_result.n_components,
                resolver=ref_result.task.resolver,
                resolver_setting=ref_result.task.resolver_setting,
                #    reference=reference)
                initial_guess=ref_result.last_func_args)

            self.logger.debug(
                f"Retry task: sample name={task.sample.name}, distribution_type={task.distribution_type.name}, n_components={task.n_components}"
            )
            self.retry_tasks[task.uuid] = index
            self.async_worker.execute_task(task)

    def degrade_results(self):
        degrade_results = []  # type: list[SSUResult]
        degrade_indexes = []  # type: list[int]
        for i, result in enumerate(self.__fitting_results):
            for component in result.components:
                if component.fraction < 1e-3:
                    degrade_results.append(result)
                    degrade_indexes.append(i)
                    break
        self.logger.debug(
            f"Results should be degrade (have a redundant component): {[result.sample.name for result in degrade_results]}"
        )
        if len(degrade_results) == 0:
            self.show_info(
                self.tr("No fitting result was evaluated as an outlier."))
            return
        self.show_info(
            self.
            tr("The results below should be degrade (have a redundant component:\n    {0}"
               ).format(", ".join(
                   [result.sample.name for result in degrade_results])))

        self.retry_progress_msg.setText(
            self.tr("Tasks to be retried: {0}").format(len(degrade_results)))
        self.retry_timer.start(1)
        for index, result in zip(degrade_indexes, degrade_results):
            reference = []
            n_redundant = 0
            for component in result.components:
                if component.fraction < 1e-3:
                    n_redundant += 1
                else:
                    reference.append(
                        dict(mean=component.logarithmic_moments["mean"],
                             std=component.logarithmic_moments["std"],
                             skewness=component.logarithmic_moments["skewness"]
                             ))
            task = SSUTask(
                result.sample,
                result.distribution_type,
                result.n_components -
                n_redundant if result.n_components > n_redundant else 1,
                resolver=result.task.resolver,
                resolver_setting=result.task.resolver_setting,
                reference=reference)
            self.logger.debug(
                f"Retry task: sample name={task.sample.name}, distribution_type={task.distribution_type.name}, n_components={task.n_components}"
            )
            self.retry_tasks[task.uuid] = index
            self.async_worker.execute_task(task)

    def ask_deal_outliers(self, outlier_results: typing.List[SSUResult],
                          outlier_indexes: typing.List[int]):
        assert len(outlier_indexes) == len(outlier_results)
        if len(outlier_results) == 0:
            self.show_info(
                self.tr("No fitting result was evaluated as an outlier."))
        else:
            if len(outlier_results) > 100:
                self.outlier_msg.setText(
                    self.
                    tr("The fitting results have the component that its fraction is near zero:\n    {0}...(total {1} outliers)\nHow to deal with them?"
                       ).format(
                           ", ".join([
                               result.sample.name
                               for result in outlier_results[:100]
                           ]), len(outlier_results)))
            else:
                self.outlier_msg.setText(
                    self.
                    tr("The fitting results have the component that its fraction is near zero:\n    {0}\nHow to deal with them?"
                       ).format(", ".join([
                           result.sample.name for result in outlier_results
                       ])))
            res = self.outlier_msg.exec_()
            if res == QMessageBox.Discard:
                self.remove_results(outlier_indexes)
            elif res == QMessageBox.Retry:
                self.retry_results(outlier_indexes, outlier_results)
            else:
                pass

    def check_nan_and_inf(self):
        if self.n_results == 0:
            self.show_warning(self.tr("There is not any result in the list."))
            return
        outlier_results = []
        outlier_indexes = []
        for i, result in enumerate(self.__fitting_results):
            if not result.is_valid:
                outlier_results.append(result)
                outlier_indexes.append(i)
        self.logger.debug(
            f"Outlier results with the nan or inf value(s): {[result.sample.name for result in outlier_results]}"
        )
        self.ask_deal_outliers(outlier_results, outlier_indexes)

    def check_final_distances(self):
        if self.n_results == 0:
            self.show_warning(self.tr("There is not any result in the list."))
            return
        elif self.n_results < 10:
            self.show_warning(self.tr("The results in list are too less."))
            return
        distances = []
        for result in self.__fitting_results:
            distances.append(result.get_distance(self.distance_name))
        distances = np.array(distances)
        self.boxplot_chart.show_dataset([distances],
                                        xlabels=[self.distance_name],
                                        ylabel=self.tr("Distance"))
        self.boxplot_chart.show()

        # calculate the 1/4, 1/2, and 3/4 postion value to judge which result is invalid
        # 1. the mean squared errors are much higher in the results which are lack of components
        # 2. with the component number getting higher, the mean squared error will get lower and finally reach the minimum
        median = np.median(distances)
        upper_group = distances[np.greater(distances, median)]
        lower_group = distances[np.less(distances, median)]
        value_1_4 = np.median(lower_group)
        value_3_4 = np.median(upper_group)
        distance_QR = value_3_4 - value_1_4
        outlier_results = []
        outlier_indexes = []
        for i, (result,
                distance) in enumerate(zip(self.__fitting_results, distances)):
            if distance > value_3_4 + distance_QR * 1.5:
                # which error too small is not outlier
                # if distance > value_3_4 + distance_QR * 1.5 or distance < value_1_4 - distance_QR * 1.5:
                outlier_results.append(result)
                outlier_indexes.append(i)
        self.logger.debug(
            f"Outlier results with too greater distances: {[result.sample.name for result in outlier_results]}"
        )
        self.ask_deal_outliers(outlier_results, outlier_indexes)

    def check_component_moments(self, key: str):
        if self.n_results == 0:
            self.show_warning(self.tr("There is not any result in the list."))
            return
        elif self.n_results < 10:
            self.show_warning(self.tr("The results in list are too less."))
            return
        max_n_components = 0
        for result in self.__fitting_results:
            if result.n_components > max_n_components:
                max_n_components = result.n_components
        moments = []
        for i in range(max_n_components):
            moments.append([])

        for result in self.__fitting_results:
            for i, component in enumerate(result.components):
                if np.isnan(component.logarithmic_moments[key]) or np.isinf(
                        component.logarithmic_moments[key]):
                    pass
                else:
                    moments[i].append(component.logarithmic_moments[key])

        # key_trans = {"mean": self.tr("Mean"), "std": self.tr("STD"), "skewness": self.tr("Skewness"), "kurtosis": self.tr("Kurtosis")}
        key_label_trans = {
            "mean": self.tr("Mean [φ]"),
            "std": self.tr("STD [φ]"),
            "skewness": self.tr("Skewness"),
            "kurtosis": self.tr("Kurtosis")
        }
        self.boxplot_chart.show_dataset(
            moments,
            xlabels=[f"C{i+1}" for i in range(max_n_components)],
            ylabel=key_label_trans[key])
        self.boxplot_chart.show()

        outlier_dict = {}

        for i in range(max_n_components):
            stacked_moments = np.array(moments[i])
            # calculate the 1/4, 1/2, and 3/4 postion value to judge which result is invalid
            # 1. the mean squared errors are much higher in the results which are lack of components
            # 2. with the component number getting higher, the mean squared error will get lower and finally reach the minimum
            median = np.median(stacked_moments)
            upper_group = stacked_moments[np.greater(stacked_moments, median)]
            lower_group = stacked_moments[np.less(stacked_moments, median)]
            value_1_4 = np.median(lower_group)
            value_3_4 = np.median(upper_group)
            distance_QR = value_3_4 - value_1_4

            for j, result in enumerate(self.__fitting_results):
                if result.n_components > i:
                    distance = result.components[i].logarithmic_moments[key]
                    if distance > value_3_4 + distance_QR * 1.5 or distance < value_1_4 - distance_QR * 1.5:
                        outlier_dict[j] = result

        outlier_results = []
        outlier_indexes = []
        for index, result in sorted(outlier_dict.items(), key=lambda x: x[0]):
            outlier_indexes.append(index)
            outlier_results.append(result)
        self.logger.debug(
            f"Outlier results with abnormal {key} values of their components: {[result.sample.name for result in outlier_results]}"
        )
        self.ask_deal_outliers(outlier_results, outlier_indexes)

    def check_component_fractions(self):
        outlier_results = []
        outlier_indexes = []
        for i, result in enumerate(self.__fitting_results):
            for component in result.components:
                if component.fraction < 1e-3:
                    outlier_results.append(result)
                    outlier_indexes.append(i)
                    break
        self.logger.debug(
            f"Outlier results with the component that its fraction is near zero: {[result.sample.name for result in outlier_results]}"
        )
        self.ask_deal_outliers(outlier_results, outlier_indexes)

    def try_align_components(self):
        if self.n_results == 0:
            self.show_warning(self.tr("There is not any result in the list."))
            return
        elif self.n_results < 10:
            self.show_warning(self.tr("The results in list are too less."))
            return
        import matplotlib.pyplot as plt
        n_components_list = [
            result.n_components for result in self.__fitting_results
        ]
        count_dict = Counter(n_components_list)
        max_n_components = max(count_dict.keys())
        self.logger.debug(
            f"N_components: {count_dict}, Max N_components: {max_n_components}"
        )
        n_components_desc = "\n".join([
            self.tr("{0} Component(s): {1}").format(n_components, count)
            for n_components, count in count_dict.items()
        ])
        self.show_info(
            self.tr("N_components distribution of Results:\n{0}").format(
                n_components_desc))

        x = self.__fitting_results[0].classes_μm
        stacked_components = []
        for result in self.__fitting_results:
            for component in result.components:
                stacked_components.append(component.distribution)
        stacked_components = np.array(stacked_components)

        cluser = KMeans(n_clusters=max_n_components)
        flags = cluser.fit_predict(stacked_components)

        figure = plt.figure(figsize=(6, 4))
        cmap = plt.get_cmap("tab10")
        axes = figure.add_subplot(1, 1, 1)
        for flag, distribution in zip(flags, stacked_components):
            plt.plot(x, distribution, c=cmap(flag), zorder=flag)
        axes.set_xscale("log")
        axes.set_xlabel(self.tr("Grain-size [μm]"))
        axes.set_ylabel(self.tr("Frequency"))
        figure.tight_layout()
        figure.show()

        outlier_results = []
        outlier_indexes = []
        flag_index = 0
        for i, result in enumerate(self.__fitting_results):
            result_flags = set()
            for component in result.components:
                if flags[flag_index] in result_flags:
                    outlier_results.append(result)
                    outlier_indexes.append(i)
                    break
                else:
                    result_flags.add(flags[flag_index])
                flag_index += 1
        self.logger.debug(
            f"Outlier results that have two components in the same cluster: {[result.sample.name for result in outlier_results]}"
        )
        self.ask_deal_outliers(outlier_results, outlier_indexes)

    def analyse_typical_components(self):
        if self.n_results == 0:
            self.show_warning(self.tr("There is not any result in the list."))
            return
        elif self.n_results < 10:
            self.show_warning(self.tr("The results in list are too less."))
            return

        self.typical_chart.show_typical(self.__fitting_results)
        self.typical_chart.show()