feat(i18n): 新增中英文国际化支持,语言下拉框即时切换
This commit is contained in:
+141
-116
@@ -58,6 +58,7 @@ except ImportError as exc: # pragma: no cover
|
||||
raise SystemExit("PySide6 is required to run the GUI. Install it with: pip install PySide6") from exc
|
||||
|
||||
from . import APP_NAME, APP_TITLE
|
||||
from .i18n import t, switch as i18n_switch, current_locale
|
||||
from .printer import PrinterConfig, send_to_printer
|
||||
from .batch import IndividualBuildTask, individual_batch_worker_count, run_individual_batch_builds
|
||||
from .core import (
|
||||
@@ -396,7 +397,7 @@ class MainWindow(QMainWindow):
|
||||
# =====================================================================
|
||||
# --- 文件输入区 ---
|
||||
# =====================================================================
|
||||
file_group = QGroupBox("输入 3MF 文件")
|
||||
file_group = QGroupBox(t("gui.file_group.title"))
|
||||
self.file_group = file_group
|
||||
file_layout = QVBoxLayout(file_group)
|
||||
file_body = QHBoxLayout()
|
||||
@@ -405,7 +406,7 @@ class MainWindow(QMainWindow):
|
||||
table_layout = QVBoxLayout()
|
||||
self.table = DropTableWidget(self.add_paths, self.remove_selected)
|
||||
self.table.setColumnCount(5)
|
||||
self.table.setHorizontalHeaderLabels(["顺序", "3MF 文件", "份数", "时间", "耗材"])
|
||||
self.table.setHorizontalHeaderLabels([t("gui.file_group.table.header.order"), t("gui.file_group.table.header.file"), t("gui.file_group.table.header.copies"), t("gui.file_group.table.header.time"), t("gui.file_group.table.header.filament")])
|
||||
self.table.horizontalHeader().setSectionResizeMode(ORDER_COLUMN, QHeaderView.ResizeToContents)
|
||||
self.table.horizontalHeader().setSectionResizeMode(FILE_COLUMN, QHeaderView.Stretch)
|
||||
self.table.horizontalHeader().setSectionResizeMode(COPIES_COLUMN, QHeaderView.ResizeToContents)
|
||||
@@ -417,20 +418,20 @@ class MainWindow(QMainWindow):
|
||||
self.table.setMinimumHeight(120)
|
||||
self.table.setSelectionBehavior(QTableWidget.SelectRows)
|
||||
self.table.setAlternatingRowColors(True)
|
||||
self.table.setToolTip("将 .3mf 文件或文件夹拖放到此处。文件夹将添加所有顶层 .3mf 文件。")
|
||||
self.table.setToolTip(t("gui.file_group.table.tooltip"))
|
||||
self.table.itemChanged.connect(self.on_table_item_changed)
|
||||
self.table.itemSelectionChanged.connect(self.update_thumbnail_preview)
|
||||
table_layout.addWidget(self.table)
|
||||
|
||||
# --- 合计统计标签 ---
|
||||
self.total_summary_label = QLabel("合计:0 板 | 时间:0s | 耗材:0.00 g")
|
||||
self.total_summary_label = QLabel(t("gui.file_group.total_summary.empty"))
|
||||
table_layout.addWidget(self.total_summary_label)
|
||||
file_body.addLayout(table_layout, 1)
|
||||
|
||||
# --- 缩略图预览 ---
|
||||
preview_group = QGroupBox("选中文件的缩略图")
|
||||
preview_group = QGroupBox(t("gui.preview.title"))
|
||||
preview_layout = QVBoxLayout(preview_group)
|
||||
self.thumbnail_label = QLabel("选择一个输入文件")
|
||||
self.thumbnail_label = QLabel(t("gui.preview.select_hint"))
|
||||
self.thumbnail_label.setAlignment(Qt.AlignCenter)
|
||||
self.thumbnail_label.setFixedSize(240, 170)
|
||||
self.thumbnail_label.setStyleSheet(
|
||||
@@ -453,11 +454,11 @@ class MainWindow(QMainWindow):
|
||||
|
||||
# --- 文件操作按钮栏 ---
|
||||
file_buttons = QHBoxLayout()
|
||||
add_button = QPushButton("添加 3MF")
|
||||
remove_button = QPushButton("移除")
|
||||
remove_all_button = QPushButton("全部移除")
|
||||
apply_default_copies_button = QPushButton("应用默认份数至选中行")
|
||||
self.build_button = QPushButton("构建 3MF")
|
||||
add_button = QPushButton(t("gui.button.add_3mf"))
|
||||
remove_button = QPushButton(t("gui.button.remove"))
|
||||
remove_all_button = QPushButton(t("gui.button.remove_all"))
|
||||
apply_default_copies_button = QPushButton(t("gui.button.apply_copies"))
|
||||
self.build_button = QPushButton(t("gui.button.build"))
|
||||
self.build_button.setMinimumHeight(48)
|
||||
self.build_button.setMinimumWidth(160)
|
||||
build_font = self.build_button.font()
|
||||
@@ -482,7 +483,7 @@ class MainWindow(QMainWindow):
|
||||
# =====================================================================
|
||||
# --- 打包选项区 ---
|
||||
# =====================================================================
|
||||
options_group = QGroupBox("打包选项")
|
||||
options_group = QGroupBox(t("gui.options.group.title"))
|
||||
grid = QGridLayout(options_group)
|
||||
grid.setColumnStretch(1, 1)
|
||||
grid.setColumnStretch(3, 1)
|
||||
@@ -494,8 +495,8 @@ class MainWindow(QMainWindow):
|
||||
self.swap_gcode_combo = QComboBox()
|
||||
self.swap_gcode_combo.setMinimumWidth(240)
|
||||
self.swap_gcode_combo.setMaximumWidth(440)
|
||||
refresh_button = QPushButton("刷新")
|
||||
open_folder_button = QPushButton("打开文件夹")
|
||||
refresh_button = QPushButton(t("gui.options.refresh"))
|
||||
open_folder_button = QPushButton(t("gui.options.open_folder"))
|
||||
refresh_button.setFixedWidth(refresh_button.sizeHint().width())
|
||||
open_folder_button.setFixedWidth(open_folder_button.sizeHint().width())
|
||||
refresh_button.clicked.connect(self.load_swap_gcode_to_combo)
|
||||
@@ -507,7 +508,7 @@ class MainWindow(QMainWindow):
|
||||
self.default_copies_edit.setFixedWidth(64)
|
||||
|
||||
# 热床降温
|
||||
self.bed_cooldown_check = QCheckBox("等待热床降温")
|
||||
self.bed_cooldown_check = QCheckBox(t("gui.options.bed_cooldown"))
|
||||
self.bed_cooldown_check.setChecked(True)
|
||||
self.cool_bed_edit = QLineEdit("45")
|
||||
self.cool_bed_edit.setValidator(QIntValidator(0, 120))
|
||||
@@ -515,11 +516,11 @@ class MainWindow(QMainWindow):
|
||||
bed_row = QHBoxLayout()
|
||||
bed_row.addWidget(self.bed_cooldown_check)
|
||||
bed_row.addWidget(self.cool_bed_edit)
|
||||
bed_row.addWidget(QLabel("°C"))
|
||||
bed_row.addWidget(QLabel(t("gui.options.bed_cooldown.suffix")))
|
||||
bed_row.addStretch(1)
|
||||
|
||||
# 换盘后等待时间(勾选后启用,不勾选则不插入等待指令)
|
||||
self.eject_wait_check = QCheckBox("换盘后等待")
|
||||
self.eject_wait_check = QCheckBox(t("gui.options.eject_wait"))
|
||||
self.eject_wait_check.setChecked(True)
|
||||
self.wait_edit = QLineEdit("30")
|
||||
self.wait_edit.setValidator(QIntValidator(0, 3600))
|
||||
@@ -527,23 +528,23 @@ class MainWindow(QMainWindow):
|
||||
eject_wait_row = QHBoxLayout()
|
||||
eject_wait_row.addWidget(self.eject_wait_check)
|
||||
eject_wait_row.addWidget(self.wait_edit)
|
||||
eject_wait_row.addWidget(QLabel("秒"))
|
||||
eject_wait_row.addWidget(QLabel(t("gui.options.wait_suffix")))
|
||||
eject_wait_row.addStretch(1)
|
||||
|
||||
# 剩余时间板号显示
|
||||
self.show_plate_number_check = QCheckBox("在剩余时间的百位显示当前板号")
|
||||
self.show_plate_number_check = QCheckBox(t("gui.options.plate_number"))
|
||||
self.show_plate_number_check.setChecked(True)
|
||||
|
||||
# 最后换盘
|
||||
self.swap_final_check = QCheckBox("最后一块盘后也执行换盘 G-code")
|
||||
self.swap_final_check = QCheckBox(t("gui.options.final_swap"))
|
||||
self.swap_final_check.setChecked(True)
|
||||
|
||||
# G-code 补丁
|
||||
self.patch_check = QCheckBox("应用可编辑的 G-code 补丁")
|
||||
self.patch_check.setToolTip("使用 gcode_patches.ini")
|
||||
self.patch_check = QCheckBox(t("gui.options.gcode_patches"))
|
||||
self.patch_check.setToolTip(t("gui.options.gcode_patches.tooltip"))
|
||||
self.patch_check.setChecked(True)
|
||||
patch_row = QHBoxLayout()
|
||||
open_patch_button = QPushButton("打开配置文件")
|
||||
open_patch_button = QPushButton(t("gui.options.open_config"))
|
||||
open_patch_button.clicked.connect(self.open_patch_config)
|
||||
patch_row.addWidget(self.patch_check)
|
||||
patch_row.addWidget(open_patch_button)
|
||||
@@ -551,46 +552,46 @@ class MainWindow(QMainWindow):
|
||||
|
||||
# 3MF 元数据模式
|
||||
self.metadata_combo = QComboBox()
|
||||
self.metadata_combo.addItem("保留原始预测和重量", "source")
|
||||
self.metadata_combo.addItem("累加预测和耗材用量", "sum")
|
||||
self.metadata_combo.addItem(t("gui.options.metadata.source"), "source")
|
||||
self.metadata_combo.addItem(t("gui.options.metadata.sum"), "sum")
|
||||
self.metadata_combo.setFixedWidth(220)
|
||||
|
||||
# ZIP 压缩级别
|
||||
self.zip_level_combo = QComboBox()
|
||||
for level in range(1, 10):
|
||||
self.zip_level_combo.addItem(f"级别 {level}", level)
|
||||
self.zip_level_combo.addItem(t("gui.options.zip_compression.level_format", level=level), level)
|
||||
self.zip_level_combo.setCurrentIndex(DEFAULT_ZIP_COMPRESS_LEVEL - 1)
|
||||
self.zip_level_combo.setFixedWidth(120)
|
||||
self.zip_level_combo.setToolTip("输出 3MF 的 zlib-ng Deflate 压缩级别。")
|
||||
self.zip_level_combo.setToolTip(t("gui.options.zip_compression.tooltip"))
|
||||
|
||||
# 独立批处理模式
|
||||
self.individual_batch_check = QCheckBox("独立批处理模式")
|
||||
self.individual_batch_check = QCheckBox(t("gui.options.batch_mode"))
|
||||
self.individual_batch_check.setToolTip(
|
||||
"将每行输入构建为独立的输出文件,使用该行的份数。"
|
||||
t("gui.options.batch_mode.tooltip")
|
||||
)
|
||||
|
||||
# 构建后清空 / 跳过重复
|
||||
self.clear_after_build_check = QCheckBox("构建成功后清空输入列表")
|
||||
self.clear_after_build_check = QCheckBox(t("gui.options.clear_after_build"))
|
||||
self.clear_after_build_check.setChecked(False)
|
||||
self.skip_duplicates_check = QCheckBox("添加输入时跳过重复文件路径")
|
||||
self.skip_duplicates_check = QCheckBox(t("gui.options.skip_duplicates"))
|
||||
self.skip_duplicates_check.setChecked(True)
|
||||
|
||||
# 标签列固定宽度(左对齐),控件列自动对齐(stretch=1 平分空间)
|
||||
grid.setColumnMinimumWidth(0, 120)
|
||||
grid.setColumnMinimumWidth(2, 90)
|
||||
for label_text, row, col in [
|
||||
("换盘 G-code", 0, 0),
|
||||
("新输入的默认份数", 1, 0),
|
||||
("换盘后等待时间", 2, 0),
|
||||
("最后换盘", 3, 0),
|
||||
("3MF 元数据", 4, 0),
|
||||
("批处理模式", 5, 0),
|
||||
("热床降温", 1, 2),
|
||||
("剩余时间板号", 2, 2),
|
||||
("G-code 补丁", 3, 2),
|
||||
("ZIP 压缩", 4, 2),
|
||||
("输入处理", 5, 2),
|
||||
("构建成功后", 6, 0),
|
||||
(t("gui.options.swap_gcode.label"), 0, 0),
|
||||
(t("gui.options.default_copies"), 1, 0),
|
||||
(t("gui.options.eject_wait.label"), 2, 0),
|
||||
(t("gui.options.final_swap.label"), 3, 0),
|
||||
(t("gui.options.metadata.label"), 4, 0),
|
||||
(t("gui.options.batch_mode.label"), 5, 0),
|
||||
(t("gui.options.bed_cooldown.label"), 1, 2),
|
||||
(t("gui.options.plate_number.label"), 2, 2),
|
||||
(t("gui.options.gcode_patches.label"), 3, 2),
|
||||
(t("gui.options.zip_compression.label"), 4, 2),
|
||||
(t("gui.options.input_handling"), 5, 2),
|
||||
(t("gui.options.clear_after_build.label"), 6, 0),
|
||||
]:
|
||||
label = QLabel(label_text)
|
||||
label.setAlignment(Qt.AlignLeft | Qt.AlignVCenter)
|
||||
@@ -626,50 +627,59 @@ class MainWindow(QMainWindow):
|
||||
|
||||
# 第 6 行:构建成功后清空输入列表(独立一行)
|
||||
grid.addWidget(self.clear_after_build_check, 6, 1, 1, 3)
|
||||
|
||||
# 语言选择
|
||||
self.language_combo = QComboBox()
|
||||
self.language_combo.addItem("简体中文", "zh_CN")
|
||||
self.language_combo.addItem("English", "en")
|
||||
self.language_combo.setFixedWidth(120)
|
||||
grid.addWidget(QLabel(t("gui.options.language")), 6, 0)
|
||||
grid.addWidget(self.language_combo, 6, 1)
|
||||
|
||||
root.addWidget(options_group)
|
||||
|
||||
# =====================================================================
|
||||
# --- 输出区 ---
|
||||
# =====================================================================
|
||||
output_group = QGroupBox("输出")
|
||||
output_group = QGroupBox(t("gui.output.group.title"))
|
||||
output_layout = QFormLayout(output_group)
|
||||
|
||||
# 输出目录
|
||||
output_dir_row = QHBoxLayout()
|
||||
self.output_dir_edit = QLineEdit()
|
||||
self.output_dir_edit.setPlaceholderText("留空则写入输入文件所在目录")
|
||||
self.output_dir_edit.setPlaceholderText(t("gui.output.dir.placeholder"))
|
||||
self.output_dir_edit.setMinimumWidth(240)
|
||||
self.output_dir_edit.setMaximumWidth(520)
|
||||
browse_output_dir_button = QPushButton("浏览")
|
||||
browse_output_dir_button = QPushButton(t("gui.output.dir.browse"))
|
||||
browse_output_dir_button.setFixedWidth(browse_output_dir_button.sizeHint().width())
|
||||
browse_output_dir_button.clicked.connect(self.choose_output_dir)
|
||||
output_dir_row.addWidget(self.output_dir_edit, 1)
|
||||
output_dir_row.addWidget(browse_output_dir_button)
|
||||
output_layout.addRow("输出目录", output_dir_row)
|
||||
output_layout.addRow(t("gui.output.dir.label"), output_dir_row)
|
||||
|
||||
# 输出文件名规则
|
||||
self.output_name_edit = QLineEdit(DEFAULT_OUTPUT_PATTERN)
|
||||
self.output_name_edit.setPlaceholderText("可使用 {source}、{sources}、{plates}、{copies}、{date}、{time} 等标记")
|
||||
self.output_name_edit.setPlaceholderText(t("gui.output.filename_rule.placeholder"))
|
||||
self.output_name_edit.setMinimumWidth(240)
|
||||
self.output_name_edit.setMaximumWidth(520)
|
||||
filename_rule_row = QHBoxLayout()
|
||||
output_rule_help_button = QPushButton("?")
|
||||
output_rule_help_button = QPushButton(t("gui.output.filename_rule.help"))
|
||||
output_rule_help_button.setFixedWidth(34)
|
||||
output_rule_help_button.setToolTip("显示文件名标记帮助")
|
||||
output_rule_help_button.setToolTip(t("gui.output.filename_rule.help.tooltip"))
|
||||
output_rule_help_button.clicked.connect(self.show_output_rule_help)
|
||||
filename_rule_row.addWidget(self.output_name_edit, 1)
|
||||
filename_rule_row.addWidget(output_rule_help_button)
|
||||
output_layout.addRow("输出文件名规则", filename_rule_row)
|
||||
output_layout.addRow(t("gui.output.filename_rule"), filename_rule_row)
|
||||
|
||||
# 输出预览
|
||||
self.output_preview_label = QLabel("-")
|
||||
output_layout.addRow("预览", self.output_preview_label)
|
||||
output_layout.addRow(t("gui.output.preview"), self.output_preview_label)
|
||||
|
||||
# ---- 发送至打印机 ----
|
||||
self.send_to_printer_check = QCheckBox("构建后发送至打印机")
|
||||
self.send_to_printer_check = QCheckBox(t("gui.output.printer.send"))
|
||||
self.send_to_printer_check.setChecked(False)
|
||||
self.send_to_printer_check.setToolTip("通过局域网将生成的 3MF 发送到 A1 打印机并开始打印")
|
||||
output_layout.addRow("打印机", self.send_to_printer_check)
|
||||
self.send_to_printer_check.setToolTip(t("gui.output.printer.send.tooltip"))
|
||||
output_layout.addRow(t("gui.output.printer.send"), self.send_to_printer_check)
|
||||
|
||||
# 打印机配置行(IP、访问码、序列号)
|
||||
printer_config_widget = QWidget()
|
||||
@@ -678,13 +688,13 @@ class MainWindow(QMainWindow):
|
||||
printer_config_layout.setSpacing(6)
|
||||
|
||||
self.printer_ip_edit = QLineEdit()
|
||||
self.printer_ip_edit.setPlaceholderText("IP 地址")
|
||||
self.printer_ip_edit.setPlaceholderText(t("gui.output.printer.ip"))
|
||||
self.printer_ip_edit.setMaximumWidth(140)
|
||||
self.printer_access_code_edit = QLineEdit()
|
||||
self.printer_access_code_edit.setPlaceholderText("访问码")
|
||||
self.printer_access_code_edit.setPlaceholderText(t("gui.output.printer.access_code"))
|
||||
self.printer_access_code_edit.setMaximumWidth(120)
|
||||
self.printer_serial_edit = QLineEdit()
|
||||
self.printer_serial_edit.setPlaceholderText("序列号")
|
||||
self.printer_serial_edit.setPlaceholderText(t("gui.output.printer.serial"))
|
||||
self.printer_serial_edit.setMaximumWidth(180)
|
||||
|
||||
printer_config_layout.addWidget(self.printer_ip_edit)
|
||||
@@ -759,6 +769,7 @@ class MainWindow(QMainWindow):
|
||||
self.printer_serial_edit.textChanged.connect(self.save_current_settings)
|
||||
self.output_dir_edit.textChanged.connect(self.on_output_rule_changed)
|
||||
self.output_name_edit.textChanged.connect(self.on_output_rule_changed)
|
||||
self.language_combo.currentIndexChanged.connect(self.on_language_changed)
|
||||
|
||||
def restore_settings_to_ui(self) -> None:
|
||||
"""从加载的设置字典恢复所有 UI 控件的状态。"""
|
||||
@@ -776,6 +787,10 @@ class MainWindow(QMainWindow):
|
||||
self.individual_batch_check.setChecked(bool(options.get("individual_batch_mode", False)))
|
||||
self.clear_after_build_check.setChecked(bool(options.get("clear_after_build", False)))
|
||||
self.skip_duplicates_check.setChecked(bool(options.get("skip_duplicates", True)))
|
||||
saved_locale = str(options.get("locale", "zh_CN"))
|
||||
locale_index = self.language_combo.findData(saved_locale)
|
||||
if locale_index >= 0:
|
||||
self.language_combo.setCurrentIndex(locale_index)
|
||||
self.send_to_printer_check.setChecked(bool(options.get("send_to_printer_enabled", False)))
|
||||
self.printer_ip_edit.setText(str(options.get("printer_ip", "")))
|
||||
self.printer_access_code_edit.setText(str(options.get("printer_access_code", "")))
|
||||
@@ -818,6 +833,7 @@ class MainWindow(QMainWindow):
|
||||
"individual_batch_mode": self.individual_batch_check.isChecked(),
|
||||
"clear_after_build": self.clear_after_build_check.isChecked(),
|
||||
"skip_duplicates": self.skip_duplicates_check.isChecked(),
|
||||
"locale": self.language_combo.currentData() or "zh_CN",
|
||||
"send_to_printer_enabled": self.send_to_printer_check.isChecked(),
|
||||
"printer_ip": self.printer_ip_edit.text().strip(),
|
||||
"printer_access_code": self.printer_access_code_edit.text().strip(),
|
||||
@@ -848,30 +864,16 @@ class MainWindow(QMainWindow):
|
||||
if state != 0:
|
||||
QMessageBox.information(
|
||||
self,
|
||||
"独立批处理模式",
|
||||
"独立批处理模式将每一行输入作为独立的构建任务。\n\n"
|
||||
"示例:如果你添加了 20 个单盘 3MF 文件并设置份数为 5,"
|
||||
"点击“构建 3MF”将创建 20 个独立的打包文件。"
|
||||
"每个输出文件仅包含该源文件重复 5 次的内容。\n\n"
|
||||
"这个功能适用于快速将大量独立的 3MF 作业批量转换为多份数的 "
|
||||
"SwapMod 包。它不会将所有输入文件合并到一个 3MF 中。",
|
||||
t("gui.dialog.individual_batch.title"),
|
||||
t("gui.dialog.individual_batch.text"),
|
||||
)
|
||||
|
||||
def show_output_rule_help(self) -> None:
|
||||
"""显示输出文件名规则的帮助对话框。"""
|
||||
QMessageBox.information(
|
||||
self,
|
||||
"输出文件名规则",
|
||||
"可用的文件名标记:\n\n"
|
||||
"{source} — 第一个输入文件的文件名(不含 .3mf)\n"
|
||||
"{sources} — 源文件名称摘要。多个不同输入时,显示为 first_source_and_N_more\n"
|
||||
"{plates} — 输出中的总板数\n"
|
||||
"{copies} — 输出中使用的总份数\n"
|
||||
"{date} — 当前日期,格式 YYYYMMDD\n"
|
||||
"{time} — 当前时间,格式 HHMMSS\n\n"
|
||||
"默认规则:\n"
|
||||
"{plates}-Plates-{sources}.3mf\n\n"
|
||||
"在独立批处理模式下,这些标记会针对每行输入单独计算。",
|
||||
t("gui.dialog.filename_help.title"),
|
||||
t("gui.dialog.filename_help.text"),
|
||||
)
|
||||
|
||||
# =========================================================================
|
||||
@@ -895,7 +897,7 @@ class MainWindow(QMainWindow):
|
||||
if index >= 0:
|
||||
self.swap_gcode_combo.setCurrentIndex(index)
|
||||
if not files:
|
||||
self.log.append(f"在 {default_swap_gcode_dir()} 中未找到换盘 G-code 文件")
|
||||
self.log.append(t("gui.log.no_swap_files", dir=str(default_swap_gcode_dir())))
|
||||
|
||||
def open_path(self, path: Path) -> None:
|
||||
"""跨平台打开文件或文件夹(Windows/Mac/Linux)。"""
|
||||
@@ -918,7 +920,7 @@ class MainWindow(QMainWindow):
|
||||
"""打开 G-code 补丁配置文件。"""
|
||||
path = default_patch_config_path()
|
||||
if not path.exists():
|
||||
QMessageBox.information(self, APP_NAME, f"补丁配置文件不存在:\n{path}")
|
||||
QMessageBox.information(self, t("gui.dialog.warning.title"), t("gui.dialog.patch_config_not_found", path=str(path)))
|
||||
return
|
||||
self.open_path(path)
|
||||
|
||||
@@ -928,7 +930,7 @@ class MainWindow(QMainWindow):
|
||||
|
||||
def add_files(self) -> None:
|
||||
"""通过文件对话框添加 3MF 文件到输入列表。"""
|
||||
files, _ = QFileDialog.getOpenFileNames(self, "添加 3MF 文件", "", "3MF 文件 (*.3mf);;所有文件 (*)")
|
||||
files, _ = QFileDialog.getOpenFileNames(self, t("gui.dialog.add_files_title"), "", f'{t("gui.dialog.file_filter.3mf")};;{t("gui.dialog.file_filter.all")}')
|
||||
self.add_paths([Path(file_name) for file_name in files])
|
||||
|
||||
def create_order_widget(self, row: int) -> QWidget:
|
||||
@@ -944,7 +946,7 @@ class MainWindow(QMainWindow):
|
||||
|
||||
up_button = QToolButton()
|
||||
up_button.setIcon(self.style().standardIcon(QStyle.StandardPixmap.SP_ArrowUp))
|
||||
up_button.setToolTip("上移")
|
||||
up_button.setToolTip(t("gui.file_group.table.move_up"))
|
||||
up_button.setAutoRaise(True)
|
||||
up_button.setFixedSize(22, 22)
|
||||
up_button.setEnabled(row > 0)
|
||||
@@ -952,7 +954,7 @@ class MainWindow(QMainWindow):
|
||||
|
||||
down_button = QToolButton()
|
||||
down_button.setIcon(self.style().standardIcon(QStyle.StandardPixmap.SP_ArrowDown))
|
||||
down_button.setToolTip("下移")
|
||||
down_button.setToolTip(t("gui.file_group.table.move_down"))
|
||||
down_button.setAutoRaise(True)
|
||||
down_button.setFixedSize(28, 28)
|
||||
down_button.setEnabled(row < self.table.rowCount() - 1)
|
||||
@@ -1010,18 +1012,18 @@ class MainWindow(QMainWindow):
|
||||
path = self.row_path(row) if row is not None else None
|
||||
self.thumbnail_label.clear()
|
||||
if path is None:
|
||||
self.thumbnail_label.setText("选择一个输入文件")
|
||||
self.thumbnail_label.setText(t("gui.preview.select_hint"))
|
||||
self.thumbnail_name_label.setText("")
|
||||
return
|
||||
self.thumbnail_name_label.setText(path.name)
|
||||
try:
|
||||
pixmap = self.load_thumbnail_pixmap(path)
|
||||
except Exception as exc:
|
||||
self.thumbnail_label.setText("预览不可用")
|
||||
self.thumbnail_label.setText(t("gui.preview.unavailable"))
|
||||
self.thumbnail_label.setToolTip(str(exc))
|
||||
return
|
||||
if pixmap is None:
|
||||
self.thumbnail_label.setText("无预览图")
|
||||
self.thumbnail_label.setText(t("gui.preview.no_image"))
|
||||
self.thumbnail_label.setToolTip("")
|
||||
return
|
||||
scaled = pixmap.scaled(self.thumbnail_label.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation)
|
||||
@@ -1060,7 +1062,7 @@ class MainWindow(QMainWindow):
|
||||
else:
|
||||
skipped += 1
|
||||
if added or skipped:
|
||||
self.log.append(f"已添加 {added} 个输入文件。跳过 {skipped} 个。")
|
||||
self.log.append(t("gui.log.added_files", added=added, skipped=skipped))
|
||||
self.update_total_summary()
|
||||
self.update_output_preview()
|
||||
|
||||
@@ -1070,7 +1072,7 @@ class MainWindow(QMainWindow):
|
||||
try:
|
||||
summary = read_3mf_summary(path)
|
||||
except Exception as exc:
|
||||
self.log.append(f"已跳过 {path}:{exc}")
|
||||
self.log.append(t("gui.log.skipped_file", path=str(path), reason=str(exc)))
|
||||
return False
|
||||
row = self.table.rowCount()
|
||||
self._updating_table = True
|
||||
@@ -1114,11 +1116,11 @@ class MainWindow(QMainWindow):
|
||||
|
||||
def base_summary_tooltip(self, summary: ThreeMfSummary) -> str:
|
||||
"""生成 3MF 文件的基本摘要信息(用于工具提示)。"""
|
||||
return (
|
||||
f"基础板数:{summary.plate_count}\n"
|
||||
f"基础时间:{format_duration(summary.prediction_seconds)}\n"
|
||||
f"基础耗材:{format_filament(summary.weight_grams, summary.filament_used_m)}"
|
||||
)
|
||||
return "\n".join((
|
||||
t("gui.file_group.table.tooltip_base_plates", count=summary.plate_count),
|
||||
t("gui.file_group.table.tooltip_base_time", time=format_duration(summary.prediction_seconds)),
|
||||
t("gui.file_group.table.tooltip_base_filament", filament=format_filament(summary.weight_grams, summary.filament_used_m))
|
||||
))
|
||||
|
||||
def get_row_copies(self, row: int) -> int:
|
||||
"""获取指定行的份数值。"""
|
||||
@@ -1279,7 +1281,7 @@ class MainWindow(QMainWindow):
|
||||
first_path = self.row_path(0) if self.table.rowCount() > 0 else None
|
||||
if not start_dir and first_path is not None:
|
||||
start_dir = str(first_path.parent)
|
||||
directory = QFileDialog.getExistingDirectory(self, "选择输出目录", start_dir or "")
|
||||
directory = QFileDialog.getExistingDirectory(self, t("gui.dialog.choose_output_dir"), start_dir or "")
|
||||
if directory:
|
||||
self.output_dir_edit.setText(directory)
|
||||
|
||||
@@ -1322,10 +1324,10 @@ class MainWindow(QMainWindow):
|
||||
"""刷新底部合计统计标签。"""
|
||||
summary = self.current_total_summary()
|
||||
self.total_summary_label.setText(
|
||||
"合计:"
|
||||
f"{summary.plate_count} 板 | "
|
||||
f"时间:{format_duration(summary.prediction_seconds)} | "
|
||||
f"耗材:{format_filament(summary.weight_grams, summary.filament_used_m)}"
|
||||
t("gui.file_group.total_summary",
|
||||
plates=summary.plate_count,
|
||||
time=format_duration(summary.prediction_seconds),
|
||||
filament=format_filament(summary.weight_grams, summary.filament_used_m))
|
||||
)
|
||||
|
||||
def update_output_preview(self) -> None:
|
||||
@@ -1342,7 +1344,7 @@ class MainWindow(QMainWindow):
|
||||
if self.individual_batch_check.isChecked():
|
||||
first_path = resolve_output_path([jobs[0]], self.output_naming_options(), self.summary_for_path)
|
||||
self.output_preview_label.setText(
|
||||
f"{first_path} | {len(jobs)} 个输出文件"
|
||||
t("gui.output.batch_preview", first=str(first_path), count=len(jobs))
|
||||
)
|
||||
else:
|
||||
output_path = resolve_output_path(jobs, self.output_naming_options(), self.summary_for_path)
|
||||
@@ -1358,7 +1360,7 @@ class MainWindow(QMainWindow):
|
||||
"""根据当前 UI 选项构建 BuildOptions 对象。"""
|
||||
swap_gcode_path = self.swap_gcode_combo.currentData()
|
||||
if not swap_gcode_path:
|
||||
raise ValueError("请在 swap_gcode 文件夹中放入至少一个换盘 G-code 文件并选择它。")
|
||||
raise ValueError(t("gui.dialog.no_swap_gcode"))
|
||||
cool_bed_temp = int(self.cool_bed_edit.text() or 45) if self.bed_cooldown_check.isChecked() else None
|
||||
eject_wait_seconds = int(self.wait_edit.text() or 30) if self.eject_wait_check.isChecked() else 0
|
||||
return BuildOptions(
|
||||
@@ -1375,11 +1377,11 @@ class MainWindow(QMainWindow):
|
||||
|
||||
def log_build_result(self, result: Any) -> None:
|
||||
"""将单次构建结果信息写入日志。"""
|
||||
self.log.append(f"输出:{result.output_3mf}")
|
||||
self.log.append(f"板数:{result.plate_count}")
|
||||
self.log.append(f"预估源文件时间:{format_duration(result.total_prediction_seconds)}")
|
||||
self.log.append(f"预估源文件耗材:{format_filament(result.total_weight_grams)}")
|
||||
self.log.append(f"G-code MD5:{result.gcode_md5}")
|
||||
self.log.append(t("gui.log.output", path=str(result.output_3mf)))
|
||||
self.log.append(t("gui.log.plates", count=result.plate_count))
|
||||
self.log.append(t("gui.log.time", time=format_duration(result.total_prediction_seconds)))
|
||||
self.log.append(t("gui.log.filament", filament=format_filament(result.total_weight_grams)))
|
||||
self.log.append(t("gui.log.md5", md5=result.gcode_md5))
|
||||
|
||||
def show_success_toast(self, message: str) -> None:
|
||||
"""显示构建成功的浮动提示条。"""
|
||||
@@ -1394,17 +1396,17 @@ class MainWindow(QMainWindow):
|
||||
)
|
||||
try:
|
||||
send_to_printer(file_path, config)
|
||||
self.log.append(f"已发送至打印机:{file_path.name}")
|
||||
self.log.append(t("gui.toast.send_success", name=file_path.name))
|
||||
except ImportError:
|
||||
self.log.append("错误:未安装 bambulabs_api,请执行 pip install bambulabs_api")
|
||||
self.log.append(t("gui.log.printer_missing"))
|
||||
except Exception as exc:
|
||||
self.log.append(f"发送至打印机失败:{exc}")
|
||||
self.log.append(t("gui.toast.send_failed", error=str(exc)))
|
||||
|
||||
def build_output(self) -> None:
|
||||
"""主构建入口:根据批处理模式分发到合并构建或独立构建。"""
|
||||
jobs = self.collect_jobs()
|
||||
if not jobs:
|
||||
QMessageBox.warning(self, APP_NAME, "请至少添加一个 3MF 文件。")
|
||||
QMessageBox.warning(self, t("gui.dialog.warning.title"), t("gui.dialog.no_files"))
|
||||
return
|
||||
try:
|
||||
self.save_current_settings()
|
||||
@@ -1413,8 +1415,8 @@ class MainWindow(QMainWindow):
|
||||
else:
|
||||
self.build_combined_output(jobs)
|
||||
except Exception as exc:
|
||||
QMessageBox.critical(self, APP_NAME, str(exc))
|
||||
self.log.append(f"错误:{exc}")
|
||||
QMessageBox.critical(self, t("gui.dialog.warning.title"), str(exc))
|
||||
self.log.append(t("gui.log.build_error", path=type(exc).__name__, error=str(exc)))
|
||||
return
|
||||
|
||||
def build_combined_output(self, jobs: list[PlateJob]) -> None:
|
||||
@@ -1425,7 +1427,7 @@ class MainWindow(QMainWindow):
|
||||
self.log_build_result(result)
|
||||
if self.send_to_printer_check.isChecked():
|
||||
self._send_to_printer(result.output_3mf)
|
||||
self.show_success_toast("已创建打包的 3MF 文件。")
|
||||
self.show_success_toast(t("gui.toast.combined_success"))
|
||||
if self.clear_after_build_check.isChecked():
|
||||
self.remove_all()
|
||||
|
||||
@@ -1444,13 +1446,13 @@ class MainWindow(QMainWindow):
|
||||
options = self.build_options_for_output(output_path)
|
||||
except Exception as exc:
|
||||
errors.append(f"{job.source_3mf.name}: {exc}")
|
||||
self.log.append(f"构建 {job.source_3mf} 时出错:{exc}")
|
||||
self.log.append(t("gui.log.build_error", path=str(job.source_3mf), error=str(exc)))
|
||||
continue
|
||||
tasks.append(IndividualBuildTask(job, options))
|
||||
|
||||
if tasks:
|
||||
worker_count = individual_batch_worker_count(len(tasks))
|
||||
self.log.append(f"正在使用 {worker_count} 个工作线程构建 {len(tasks)} 个独立输出。")
|
||||
self.log.append(t("gui.log.building_individual", count=len(tasks), workers=worker_count))
|
||||
batch_result = run_individual_batch_builds(tasks, max_workers=worker_count)
|
||||
for result in batch_result.results:
|
||||
self.log_build_result(result)
|
||||
@@ -1459,18 +1461,18 @@ class MainWindow(QMainWindow):
|
||||
success_count = len(batch_result.results)
|
||||
for failure in batch_result.failures:
|
||||
errors.append(f"{failure.job.source_3mf.name}: {failure.error}")
|
||||
self.log.append(f"构建 {failure.job.source_3mf} 时出错:{failure.error}")
|
||||
self.log.append(t("gui.log.build_error", path=str(failure.job.source_3mf), error=str(failure.error)))
|
||||
|
||||
if errors:
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
APP_NAME,
|
||||
f"已构建 {success_count} 个文件,但有 {len(errors)} 个文件失败。\n\n"
|
||||
t("gui.dialog.warning.title"),
|
||||
t("gui.dialog.batch_partial_success", success=success_count, failed=len(errors))
|
||||
+ "\n".join(errors[:8])
|
||||
+ ("\n..." if len(errors) > 8 else ""),
|
||||
)
|
||||
return
|
||||
self.show_success_toast(f"已创建 {success_count} 个打包的 3MF 文件。")
|
||||
self.show_success_toast(t("gui.toast.batch_success", count=success_count))
|
||||
if self.clear_after_build_check.isChecked():
|
||||
self.remove_all()
|
||||
|
||||
@@ -1497,6 +1499,29 @@ class MainWindow(QMainWindow):
|
||||
self.save_current_settings()
|
||||
super().closeEvent(event)
|
||||
|
||||
def on_language_changed(self) -> None:
|
||||
"""语言切换时重新加载翻译并刷新 UI 文本。"""
|
||||
new_locale = self.language_combo.currentData()
|
||||
if new_locale:
|
||||
i18n_switch(new_locale)
|
||||
self._refresh_ui_texts()
|
||||
self.save_current_settings()
|
||||
|
||||
def _refresh_ui_texts(self) -> None:
|
||||
"""刷新所有可通过 t() 获取的 UI 文本(窗口标题、组框等)。"""
|
||||
self.file_group.setTitle(t("gui.file_group.title"))
|
||||
self.table.setHorizontalHeaderLabels([
|
||||
t("gui.file_group.table.header.order"),
|
||||
t("gui.file_group.table.header.file"),
|
||||
t("gui.file_group.table.header.copies"),
|
||||
t("gui.file_group.table.header.time"),
|
||||
t("gui.file_group.table.header.filament"),
|
||||
])
|
||||
self.table.setToolTip(t("gui.file_group.table.tooltip"))
|
||||
self.update_total_summary()
|
||||
self.update_output_preview()
|
||||
self.update_thumbnail_preview()
|
||||
|
||||
|
||||
def main() -> int:
|
||||
"""应用程序入口:初始化 QApplication,创建并显示主窗口,进入事件循环。"""
|
||||
|
||||
Reference in New Issue
Block a user