From 0d80fb6f4605493d224c8b8e818e42d8cb901f45 Mon Sep 17 00:00:00 2001 From: 283375 Date: Sun, 17 Sep 2023 03:36:00 +0800 Subject: [PATCH] impr: `ScoreEditor` validation --- ui/implements/components/scoreEditor.py | 273 ++++++++++++++++-------- ui/resources/lang/en_US.ts | 153 ++++++++----- ui/resources/lang/zh_CN.ts | 153 ++++++++----- 3 files changed, 389 insertions(+), 190 deletions(-) diff --git a/ui/implements/components/scoreEditor.py b/ui/implements/components/scoreEditor.py index cbc9916..6a6aa19 100644 --- a/ui/implements/components/scoreEditor.py +++ b/ui/implements/components/scoreEditor.py @@ -1,3 +1,4 @@ +from dataclasses import dataclass from enum import IntEnum from typing import Any, Optional @@ -17,20 +18,74 @@ from PySide6.QtWidgets import ( from ui.designer.components.scoreEditor_ui import Ui_ScoreEditor from ui.extends.shared.language import LanguageChangeEventFilter -# TODO: use bit flags + class ScoreValidateResult(IntEnum): - Ok = 0 - ScoreMismatch = 1 - ScoreEmpty = 2 - ChartInvalid = 50 - ChartIncomplete = 51 - ScoreIncomplete = 100 + Ok = 0x001 + + ScoreMismatch = 0x010 + ScoreEmpty = 0x020 + ScoreIncomplete = 0x040 + ScoreIncompleteForValidate = 0x080 + + ChartNotSet = 0x100 + ChartIncomplete = 0x200 + + +@dataclass +class ScoreEditorValidationItem: + flag: int + title: str = "" + text: str = "" + warnIfIncomplete: bool = False class ScoreEditor(Ui_ScoreEditor, QWidget): valueChanged = Signal() accepted = Signal() + VALIDATION_ITEMS = [ + ScoreEditorValidationItem( + ScoreValidateResult.ChartIncomplete, + warnIfIncomplete=True, + ), + ScoreEditorValidationItem( + ScoreValidateResult.ScoreMismatch, + ), + ScoreEditorValidationItem( + ScoreValidateResult.ScoreEmpty, + ), + ScoreEditorValidationItem( + ScoreValidateResult.ScoreIncompleteForValidate, warnIfIncomplete=True + ), + ] + + VALIDATION_ITEMS_TEXT = [ + [ + # fmt: off + lambda: QCoreApplication.translate("ScoreEditor", "confirmDialog.chartIncomplete.title"), + lambda: QCoreApplication.translate("ScoreEditor", "confirmDialog.chartIncomplete.text"), + # fmt: on + ], + [ + # fmt: off + lambda: QCoreApplication.translate("ScoreEditor", "confirmDialog.scoreMismatch.title"), + lambda: QCoreApplication.translate("ScoreEditor", "confirmDialog.scoreMismatch.text"), + # fmt: on + ], + [ + # fmt: off + lambda: QCoreApplication.translate("ScoreEditor", "confirmDialog.emptyScore.title"), + lambda: QCoreApplication.translate("ScoreEditor", "confirmDialog.emptyScore.text"), + # fmt: on + ], + [ + # fmt: off + lambda: QCoreApplication.translate("ScoreEditor", "confirmDialog.scoreIncompleteForValidate.title"), + lambda: QCoreApplication.translate("ScoreEditor", "confirmDialog.scoreIncompleteForValidate.text"), + # fmt: on, + ], + ] + def __init__(self, parent=None): super().__init__(parent) self.setupUi(self) @@ -80,6 +135,16 @@ class ScoreEditor(Ui_ScoreEditor, QWidget): self.dateTimeEdit.setDateTime(QDateTime.currentDateTime()) + def retranslateUi(self, *args): + super().retranslateUi(self) + + for item, itemTextCallables in zip( + self.VALIDATION_ITEMS, self.VALIDATION_ITEMS_TEXT + ): + titleCallable, textCallable = itemTextCallables + item.title = titleCallable() + item.text = textCallable() + def validateBeforeAccept(self): return self.__validateBeforeAccept @@ -94,71 +159,78 @@ class ScoreEditor(Ui_ScoreEditor, QWidget): self.warnIfIncompleteCheckBox.setChecked(__bool) self.__warnIfIncomplete = __bool + def __triggerMessageBox( + self, methodStr: str, title: str, text: str, userConfirmButton: bool = False + ) -> QMessageBox.StandardButton: + if methodStr == "critical": + method = QMessageBox.critical + elif methodStr == "warning": + method = QMessageBox.warning + else: + method = QMessageBox.information + + if userConfirmButton: + return method( + self, + title, + text, + QMessageBox.StandardButton.Yes, + QMessageBox.StandardButton.No, + ) + else: + return method(self, title, text) + def triggerValidateMessageBox(self): validate = self.validateScore() - if validate == ScoreValidateResult.Ok: + if validate & ScoreValidateResult.Ok: return True - if validate == ScoreValidateResult.ChartInvalid: - QMessageBox.critical( - self, + if validate & ScoreValidateResult.ChartNotSet: + self.__triggerMessageBox( + "critical", # fmt: off - QCoreApplication.translate("ScoreEditor", "chartInvalidDialog.title"), - QCoreApplication.translate("ScoreEditor", "chartInvalidDialog.title"), + QCoreApplication.translate("ScoreEditor", "confirmDialog.chartNotSet.title"), + QCoreApplication.translate("ScoreEditor", "confirmDialog.chartNotSet.text"), # fmt: on ) return False - if validate == ScoreValidateResult.ChartIncomplete: - if not self.__warnIfIncomplete: - return True - result = QMessageBox.warning( - self, + if validate & ScoreValidateResult.ScoreIncomplete: + self.__triggerMessageBox( + "critical", # fmt: off - QCoreApplication.translate("ScoreEditor", "chartIncompleteDialog.title"), - QCoreApplication.translate("ScoreEditor", "chartIncompleteDialog.content"), + QCoreApplication.translate("ScoreEditor", "confirmDialog.scoreIncomplete.title"), + QCoreApplication.translate("ScoreEditor", "confirmDialog.scoreIncomplete.text"), # fmt: on - QMessageBox.StandardButton.Yes, - QMessageBox.StandardButton.No, ) - return result == QMessageBox.StandardButton.Yes - if validate == ScoreValidateResult.ScoreMismatch: - result = QMessageBox.warning( - self, - # fmt: off - QCoreApplication.translate("ScoreEditor", "scoreMismatchDialog.title"), - QCoreApplication.translate("ScoreEditor", "scoreMismatchDialog.content"), - # fmt: on - QMessageBox.StandardButton.Yes, - QMessageBox.StandardButton.No, - ) - return result == QMessageBox.StandardButton.Yes - elif validate == ScoreValidateResult.ScoreEmpty: - result = QMessageBox.warning( - self, - # fmt: off - QCoreApplication.translate("ScoreEditor", "emptyScoreDialog.title"), - QCoreApplication.translate("ScoreEditor", "emptyScoreDialog.content"), - # fmt: on - QMessageBox.StandardButton.Yes, - QMessageBox.StandardButton.No, - ) - return result == QMessageBox.StandardButton.Yes - elif validate == ScoreValidateResult.ScoreIncomplete: - if not self.__warnIfIncomplete: - return True - result = QMessageBox.warning( - self, - # fmt: off - QCoreApplication.translate("ScoreEditor", "scoreIncompleteDialog.title"), - QCoreApplication.translate("ScoreEditor", "scoreIncompleteDialog.content"), - # fmt: on - QMessageBox.StandardButton.Yes, - QMessageBox.StandardButton.No, - ) - return result == QMessageBox.StandardButton.Yes - else: return False + # since validate may have multiple results + # ask user step by step, then return the final result + finalResult = True + + for item in self.VALIDATION_ITEMS: + if not finalResult: + # user canceled commit, break then return + break + + if not validate & item.flag: + continue + + if item.warnIfIncomplete and not self.warnIfIncomplete(): + # if the item requires `warnIfIncomplete` + # and the user set the `warnIfIncomplete` option to `False` + # skip this validation + continue + + finalResult = ( + self.__triggerMessageBox( + "warning", item.title, item.text, userConfirmButton=True + ) + == QMessageBox.StandardButton.Yes + ) + + return finalResult + @Slot() def on_commitButton_clicked(self): userAccept = ( @@ -170,7 +242,7 @@ class ScoreEditor(Ui_ScoreEditor, QWidget): def score(self): score_text = self.scoreLineEdit.text().replace("'", "") - return int(score_text) if score_text else 0 + return int(score_text) if score_text else None def setComboBoxMaximums(self, max: int): self.pureSpinBox.setMaximum(max) @@ -198,44 +270,73 @@ class ScoreEditor(Ui_ScoreEditor, QWidget): def validateScore(self) -> ScoreValidateResult: if not isinstance(self.__chart, Chart): - return ScoreValidateResult.ChartInvalid + return ScoreValidateResult.ChartNotSet + + flags = 0x000 if self.__chart.notes is None: - return ScoreValidateResult.ChartIncomplete + flags |= ScoreValidateResult.ChartIncomplete score = self.value() - if score.pure is None or score.far is None: - return ScoreValidateResult.ScoreIncomplete + if score.score is None: + flags |= ScoreValidateResult.ScoreIncomplete + elif score.pure is None or score.far is None: + flags |= ScoreValidateResult.ScoreIncompleteForValidate + elif self.__chart.notes is not None: + score_range = calculate_score_range( + self.__chart.notes, score.pure, score.far + ) + note_in_range = score.pure + score.far + score.lost <= self.__chart.notes + score_in_range = score_range[0] <= score.score <= score_range[1] + if not score_in_range or not note_in_range: + flags |= ScoreValidateResult.ScoreMismatch - score_range = calculate_score_range(self.__chart.notes, score.pure, score.far) - note_in_range = score.pure + score.far + score.lost <= self.__chart.notes - score_in_range = score_range[0] <= score.score <= score_range[1] - if not score_in_range or not note_in_range: - return ScoreValidateResult.ScoreMismatch if score.score == 0: - return ScoreValidateResult.ScoreEmpty - return ScoreValidateResult.Ok + flags |= ScoreValidateResult.ScoreEmpty + + return ScoreValidateResult.Ok if flags == 0x000 else flags def updateValidateLabel(self): validate = self.validateScore() - if validate == ScoreValidateResult.Ok: - text = QCoreApplication.translate("ScoreEditor", "validate.ok") - elif validate == ScoreValidateResult.ChartInvalid: - text = QCoreApplication.translate("ScoreEditor", "validate.chartInvalid") - elif validate == ScoreValidateResult.ChartIncomplete: - text = QCoreApplication.translate("ScoreEditor", "validate.chartIncomple") - elif validate == ScoreValidateResult.ScoreMismatch: - text = QCoreApplication.translate("ScoreEditor", "validate.scoreMismatch") - elif validate == ScoreValidateResult.ScoreEmpty: - text = QCoreApplication.translate("ScoreEditor", "validate.scoreEmpty") - elif validate == ScoreValidateResult.ScoreIncomplete: - text = QCoreApplication.translate("ScoreEditor", "validate.scoreIncomplete") - else: - text = QCoreApplication.translate("ScoreEditor", "validate.unknownState") + texts = [] - self.validateLabel.setText(text) + if validate & ScoreValidateResult.Ok: + texts.append(QCoreApplication.translate("ScoreEditor", "validate.ok")) + if validate & ScoreValidateResult.ChartNotSet: + texts.append( + QCoreApplication.translate("ScoreEditor", "validate.chartNotSet") + ) + if validate & ScoreValidateResult.ChartIncomplete: + texts.append( + QCoreApplication.translate("ScoreEditor", "validate.chartIncomple") + ) + if validate & ScoreValidateResult.ScoreMismatch: + texts.append( + QCoreApplication.translate("ScoreEditor", "validate.scoreMismatch") + ) + if validate & ScoreValidateResult.ScoreEmpty: + texts.append( + QCoreApplication.translate("ScoreEditor", "validate.scoreEmpty") + ) + if validate & ScoreValidateResult.ScoreIncomplete: + texts.append( + QCoreApplication.translate("ScoreEditor", "validate.scoreIncomplete") + ) + if validate & ScoreValidateResult.ScoreIncompleteForValidate: + texts.append( + # fmt: off + QCoreApplication.translate("ScoreEditor", "validate.scoreIncompleteForValidate") + # fmt: on + ) + + if not texts: + texts.append( + QCoreApplication.translate("ScoreEditor", "validate.unknownState") + ) + + self.validateLabel.setText(" | ".join(texts)) def __getItemBaseName(self, item: QLineEdit | QSpinBox | QDateTimeEdit | QComboBox): if isinstance(item, QSpinBox): diff --git a/ui/resources/lang/en_US.ts b/ui/resources/lang/en_US.ts index 39b5fab..65c0b13 100644 --- a/ui/resources/lang/en_US.ts +++ b/ui/resources/lang/en_US.ts @@ -315,94 +315,143 @@ validation ScoreEditor - - - - - - - + + + + + + + setNone None - + formLabel.date Time - + formLabel.comment Comment - + formLabel.score Score - + idAutoInsert (Auto Insert) - + + warnIfIncomplete + Warn if incomplete + + + commitButton Commit - - emptyScoreDialog.title - Empty Score - - - - emptyScoreDialog.content - Are you sure to commit an empty score? - - - - - chartInvalidDialog.title - Chart Invalid - - - - scoreMismatchDialog.title - Possible Invalid Score - - - - scoreMismatchDialog.content - The entered score may not match the selected chart. Commit this score anyway? - - - + validate.ok OK - - validate.chartInvalid - Chart invalid + + confirmDialog.chartIncomplete.title + No chart data - + + confirmDialog.chartIncomplete.text + Chart data incomplete, cannot verify score. Commit anyway? + + + + confirmDialog.scoreMismatch.title + Score mismatch + + + + confirmDialog.scoreMismatch.text + The entered score may not match the selected chart. Commit anyway? + + + + confirmDialog.emptyScore.title + Empty score + + + + confirmDialog.emptyScore.text + Score empty. Commit anyway? + + + + confirmDialog.scoreIncompleteForValidate.title + Score incomplete + + + + confirmDialog.scoreIncompleteForValidate.text + Cannot verify an incomplete score. Commit anyway? + + + + confirmDialog.chartNotSet.title + Chart not set + + + + confirmDialog.chartNotSet.text + Chart not set, cannot commit. + + + + confirmDialog.scoreIncomplete.title + Score incomplete + + + + confirmDialog.scoreIncomplete.text + Necessary score field missing, cannot commit. + + + + validate.chartNotSet + Chart not set + + + + validate.chartIncomple + No chart data, cannot verify + + + validate.scoreMismatch Possible invalid score - + validate.scoreEmpty Empty score - + validate.scoreIncomplete + Missing necessary score field + + + + validate.scoreIncompleteForValidate Score incomplete, cannot verify - + validate.unknownState Unknown @@ -422,7 +471,7 @@ validation - + @@ -436,27 +485,27 @@ validation Andreal Executable - + general.dbUrlResetWarning Application will now delete this setting and exit. Reboot application manually to specify a new database file. Continue? - + general.title General - + general.language.label Language - + general.language.followSystem Follow system - + general.dbUrl.label Database URL diff --git a/ui/resources/lang/zh_CN.ts b/ui/resources/lang/zh_CN.ts index 4083b95..3bbe3a8 100644 --- a/ui/resources/lang/zh_CN.ts +++ b/ui/resources/lang/zh_CN.ts @@ -314,94 +314,143 @@ ScoreEditor - - - - - - - + + + + + + + setNone 置空 - + formLabel.date 时间 - + formLabel.comment 注释 - + formLabel.score 分数 - + idAutoInsert (自动插入) - + + warnIfIncomplete + 不完整时要求确认 + + + commitButton 提交 - - emptyScoreDialog.title - 分数为空 - - - - emptyScoreDialog.content - 确定提交空分数吗? - - - - - chartInvalidDialog.title - 谱面无效 - - - - scoreMismatchDialog.title - 分数可能有误 - - - - scoreMismatchDialog.content - 输入的分数不在理论计算范围内。是否确认提交? - - - + validate.ok OK - - validate.chartInvalid - 谱面无效 + + confirmDialog.chartIncomplete.title + 谱面数据缺失 - + + confirmDialog.chartIncomplete.text + 谱面数据缺失,无法验证分数。继续提交吗? + + + + confirmDialog.scoreMismatch.title + 分数可能有误 + + + + confirmDialog.scoreMismatch.text + 输入的分数不在理论计算范围内。继续提交吗? + + + + confirmDialog.emptyScore.title + 分数为空 + + + + confirmDialog.emptyScore.text + 分数为空,继续提交吗? + + + + confirmDialog.scoreIncompleteForValidate.title + 分数不完整 + + + + confirmDialog.scoreIncompleteForValidate.text + 无法验证不完整的分数。继续提交吗? + + + + confirmDialog.chartNotSet.title + 未指定谱面 + + + + confirmDialog.chartNotSet.text + 未指定谱面,无法提交。 + + + + confirmDialog.scoreIncomplete.title + 分数不完整 + + + + confirmDialog.scoreIncomplete.text + 缺失必要的分数数据,无法提交。 + + + + validate.chartNotSet + 未指定谱面 + + + + validate.chartIncomple + 谱面数据缺失,无法验证分数 + + + validate.scoreMismatch 分数可能有误 - + validate.scoreEmpty 分数为空 - + validate.scoreIncomplete + 缺失必要分数数据 + + + + validate.scoreIncompleteForValidate 分数不完整,无法验证 - + validate.unknownState 未知 @@ -421,7 +470,7 @@ - + @@ -435,27 +484,27 @@ Andreal 可执行文件 - + general.dbUrlResetWarning 即将删除该设置项并关闭应用,手动重启后即可再次指定数据库路径。是否继续? - + general.title 通用 - + general.language.label 语言 - + general.language.followSystem 跟随系统 - + general.dbUrl.label 数据库 URL