"""
translate5_GUI.py
概要: translate5のGUIランナーアプリケーション。
詳細説明: translate5_comモジュールと連携し、ユーザーが翻訳設定を視覚的に操作できるようにするTkinterベースのGUIを提供する。
設定ファイルの読み書き、翻訳実行、エラー表示などの機能を持つ。
関連リンク: :doc:`translate5_GUI_usage`
"""
import os
import threading
import configparser
import tkinter as tk
from tkinter import ttk, filedialog, messagebox
import traceback
try:
from ttkthemes import ThemedTk
except Exception:
ThemedTk = None
#from translate5 import (
from translate5_com import (
initialize,
update_variables,
execute,
)
PROGRAM_NAME = "translate5 GUI Runner"
# translate5.py に合わせた API 一覧
API_CHOICES = ["openai5", "openai", "google", "gemini", "deepl"]
OPENAI_MODELS_V4 = ["gpt-4o", "gpt-4.5", "gpt-3.5-turbo"]
OPENAI_MODELS_V5 = ["gpt-5", "gpt-5-mini", "gpt-5-nano", "gpt-5-chat-latest"]
GOOGLE_MODELS = ["gemini-2.5-flash", "gemini-2.5-pro", "gemini-2.5-light", "gemini-3-pro-preview", "gemini-3.0-flash", "gemini-3.0-pro"]
DEEPL_MODELS = ["(auto)"]
REASONING_EFFORTS = ["low", "medium", "high"]
PROCESS_UNITS = ["paragraph", "run", "md"] # md は GUI 専用で use_md フラグにマップ
MODES = [("日本語→英語 (je)", "je"), ("英語→日本語 (ej)", "ej")]
CONFIG_INI = os.path.splitext(os.path.abspath(__file__))[0] + ".ini"
[ドキュメント]
class CopyableErrorDialog(tk.Toplevel):
"""
概要: エラーメッセージをコピー可能な形で表示するカスタムダイアログ。
詳細説明: TkinterのToplevelウィジェットを継承し、表示されたエラーテキストを選択・コピーできるようにする。
親ウィンドウの中央に表示され、モーダルな動作をする。
"""
def __init__(self, parent, title, message):
"""
概要: CopyableErrorDialogのコンストラクタ。
詳細説明: ダイアログのUIを構築し、エラーメッセージを設定する。モーダルな動作を実現するためにgrab_set()を呼び出し、親ウィンドウの中央に配置する。
:param parent: tk.Widget: 親となるTkinterウィジェット。
:param title: str: ダイアログのタイトル。
:param message: str: ダイアログに表示するエラーメッセージ。
"""
super().__init__(parent)
self.title(title)
self.geometry("700x400")
self.transient(parent)
self.grab_set()
text_widget = tk.Text(self, wrap="word", font=("Courier", 10))
text_widget.pack(padx=10, pady=10, fill="both", expand=True)
text_widget.insert("1.0", message)
text_widget.config(state="disabled")
button = ttk.Button(self, text="Close", command=self.destroy)
button.pack(pady=10)
self.update_idletasks()
x = parent.winfo_x() + (parent.winfo_width() // 2) - (self.winfo_width() // 2)
y = parent.winfo_y() + (parent.winfo_height() // 2) - (self.winfo_height() // 2)
self.geometry(f"+{x}+{y}")
button.focus_set()
self.wait_window(self)
[ドキュメント]
class App:
"""
概要: translate5 GUIアプリケーションのメインクラス。
詳細説明: Tkinterを使用してユーザーインターフェースを構築し、translate5翻訳エンジンの設定を管理し、
翻訳処理を実行する。設定の読み込み、保存、ファイル選択、APIとモデルの動的な切り替え、
エラー処理、ステータス表示などの機能を提供する。
:param master: tk.Tk or ttkthemes.ThemedTk: アプリケーションのルートウィンドウ。
"""
def __init__(self, master):
"""
概要: Appクラスのコンストラクタ。
詳細説明: ルートウィンドウを設定し、translate5の設定を初期化し、GUIを構築する。
既存の設定ファイルをロードし、APIごとのモデル候補を適用する。
ウィンドウクローズ時のハンドラも設定する。
:param master: tk.Tk or ttkthemes.ThemedTk: アプリケーションのルートウィンドウ。
"""
self.master = master
master.title(PROGRAM_NAME)
master.geometry("960x800")
self.config = configparser.ConfigParser(comment_prefixes=('~',))
print("--- [INFO] Initialize translate5 config ---")
self.cfg, self.parser = initialize()
self.cfg = update_variables(self.cfg, self.parser)
print("endpoint (from env/args) =", getattr(self.cfg, "endpoint", None))
self._busy = False
self._build_ui()
self._load_config()
self._apply_api_to_models()
master.protocol("WM_DELETE_WINDOW", self.on_close)
# ---------------- UI 構築 ----------------
def _build_ui(self):
"""
概要: アプリケーションのユーザーインターフェースを構築する。
詳細説明: 入力ファイル、HTMLテンプレート、翻訳モード、API設定、モデル、生成パラメータ、
翻訳ルール、プロンプトなどの各種ウィジェット(LabelFrame, Entry, Button,
Radiobutton, Combobox, Text, Checkbutton)を配置する。
:returns: None
"""
# Input / Template
io_frame = ttk.LabelFrame(self.master, text="Input / Template")
io_frame.pack(fill="x", padx=10, pady=8)
ttk.Label(io_frame, text="Input file (.docx/.pptx/.pdf/.html/.md/.txt):").grid(
row=0, column=0, sticky="w", padx=6, pady=6
)
self.infile_var = tk.StringVar(
value=self.cfg.infile if hasattr(self.cfg, "infile") else ""
)
e_in = ttk.Entry(io_frame, textvariable=self.infile_var)
e_in.grid(row=0, column=1, sticky="ew", padx=6, pady=6)
ttk.Button(io_frame, text="Browse", command=self._pick_infile).grid(
row=0, column=2, padx=6, pady=6
)
ttk.Label(io_frame, text="HTML template (template_translate.html):").grid(
row=1, column=0, sticky="w", padx=6, pady=6
)
self.tpl_var = tk.StringVar(
value=getattr(self.cfg, "html_template_path", "template_translate.html")
)
e_tpl = ttk.Entry(io_frame, textvariable=self.tpl_var)
e_tpl.grid(row=1, column=1, sticky="ew", padx=6, pady=6)
ttk.Button(io_frame, text="Browse", command=self._pick_template).grid(
row=1, column=2, padx=6, pady=6
)
io_frame.grid_columnconfigure(1, weight=1)
# Mode / Options
mode_frame = ttk.LabelFrame(self.master, text="Mode / Options")
mode_frame.pack(fill="x", padx=10, pady=8)
self.mode_var = tk.StringVar(value=getattr(self.cfg, "mode", "je"))
col = 0
for label, val in MODES:
ttk.Radiobutton(
mode_frame, text=label, variable=self.mode_var, value=val
).grid(row=0, column=col, padx=6, pady=6, sticky="w")
col += 1
ttk.Label(mode_frame, text="Process unit:").grid(
row=0, column=col, padx=12, pady=6, sticky="e"
)
self.proc_unit_var = tk.StringVar(
value=getattr(self.cfg, "process_unit", "paragraph")
)
cb_proc = ttk.Combobox(
mode_frame,
state="readonly",
values=PROCESS_UNITS,
textvariable=self.proc_unit_var,
width=10,
)
cb_proc.grid(row=0, column=col + 1, padx=6, pady=6, sticky="w")
# API
api_frame = ttk.LabelFrame(self.master, text="API")
api_frame.pack(fill="x", padx=10, pady=8)
ttk.Label(api_frame, text="API:").grid(
row=0, column=0, padx=6, pady=6, sticky="w"
)
self.api_var = tk.StringVar(
value=getattr(self.cfg, "api", "openai5")
)
self.api_cb = ttk.Combobox(
api_frame,
state="readonly",
values=API_CHOICES,
textvariable=self.api_var,
width=10,
)
self.api_cb.grid(row=0, column=1, padx=6, pady=6, sticky="w")
self.api_cb.bind("<<ComboboxSelected>>", lambda e: self._apply_api_to_models())
ttk.Label(api_frame, text="Model:").grid(
row=0, column=2, padx=12, pady=6, sticky="e"
)
self.model_var = tk.StringVar(value="")
self.model_cb = ttk.Combobox(
api_frame,
state="readonly",
values=[],
textvariable=self.model_var,
width=24,
)
self.model_cb.grid(row=0, column=3, padx=6, pady=6, sticky="w")
# Endpointは DeepL/custom 用のみ
ttk.Label(api_frame, text="Endpoint (DeepL / custom):").grid(
row=0, column=4, padx=12, pady=6, sticky="e"
)
endpoint_default = os.environ.get("endpoint", "")
print("endpoint_default(env) =", endpoint_default)
self.endpoint_var = tk.StringVar(
value=endpoint_default if endpoint_default else ""
)
ttk.Entry(api_frame, textvariable=self.endpoint_var, width=50).grid(
row=0, column=5, padx=6, pady=6, sticky="w"
)
api_frame.grid_columnconfigure(6, weight=1)
# Generation Parameters
gen_frame = ttk.LabelFrame(self.master, text="Generation Parameters")
gen_frame.pack(fill="x", padx=10, pady=8)
ttk.Label(gen_frame, text="temperature (OpenAI/Google/Gemini):").grid(
row=0, column=0, padx=6, pady=6, sticky="e"
)
self.temp_var = tk.StringVar(
value=str(getattr(self.cfg, "temperature", 0.3))
)
ttk.Entry(gen_frame, textvariable=self.temp_var, width=8).grid(
row=0, column=1, padx=6, pady=6, sticky="w"
)
ttk.Label(gen_frame, text="max_tokens (or max_output_tokens):").grid(
row=0, column=2, padx=12, pady=6, sticky="e"
)
self.max_tok_var = tk.StringVar(
value=str(getattr(self.cfg, "max_tokens", 2000))
)
ttk.Entry(gen_frame, textvariable=self.max_tok_var, width=10).grid(
row=0, column=3, padx=6, pady=6, sticky="w"
)
ttk.Label(gen_frame, text="reasoning_effort (OpenAI5):").grid(
row=0, column=4, padx=12, pady=6, sticky="e"
)
self.effort_var = tk.StringVar(
value=getattr(self.cfg, "reasoning_effort", "low")
)
ttk.Combobox(
gen_frame,
state="readonly",
values=REASONING_EFFORTS,
textvariable=self.effort_var,
width=8,
).grid(row=0, column=5, padx=6, pady=6, sticky="w")
# Translation Rules
rule_frame = ttk.LabelFrame(self.master, text="Translation Rules")
rule_frame.pack(fill="x", padx=10, pady=8)
ttk.Label(rule_frame, text="min_translate_length:").grid(
row=0, column=0, padx=6, pady=6, sticky="e"
)
self.min_len_var = tk.StringVar(
value=str(getattr(self.cfg, "min_translate_length", 5))
)
ttk.Entry(rule_frame, textvariable=self.min_len_var, width=6).grid(
row=0, column=1, padx=6, pady=6, sticky="w"
)
ttk.Label(rule_frame, text="allowed_translation_length_ratio:").grid(
row=0, column=2, padx=12, pady=6, sticky="e"
)
self.len_ratio_var = tk.StringVar(
value=str(getattr(self.cfg, "allowed_translation_length_ratio", 5.0))
)
ttk.Entry(rule_frame, textvariable=self.len_ratio_var, width=6).grid(
row=0, column=3, padx=6, pady=6, sticky="w"
)
self.limit_mb_var = tk.BooleanVar(
value=bool(
getattr(
self.cfg,
"limit_to_multibyte_str",
self._mode_implies_mb(getattr(self.cfg, "mode", "je")),
)
)
)
ttk.Checkbutton(
rule_frame,
text="Limit to multibyte strings (JA source)",
variable=self.limit_mb_var,
).grid(row=0, column=4, padx=12, pady=6, sticky="w")
# Prompts(translate_prompt / reformat_prompt のみ)
rp_frame = ttk.LabelFrame(self.master, text="Prompts")
rp_frame.pack(fill="both", expand=True, padx=10, pady=8)
ttk.Label(rp_frame, text="Translate Prompt (full instructions):").grid(
row=0, column=0, padx=6, pady=6, sticky="nw"
)
self.tr_prompt = tk.Text(rp_frame, height=8, wrap="word")
self.tr_prompt.grid(row=0, column=1, padx=6, pady=6, sticky="nsew")
self.tr_prompt.insert(
"1.0",
getattr(self.cfg, "translate_prompt", ""),
)
ttk.Label(rp_frame, text="Reformat Prompt (PDF→MD etc.):").grid(
row=1, column=0, padx=6, pady=6, sticky="nw"
)
self.rf_prompt = tk.Text(rp_frame, height=6, wrap="word")
self.rf_prompt.grid(row=1, column=1, padx=6, pady=6, sticky="nsew")
self.rf_prompt.insert(
"1.0",
getattr(self.cfg, "reformat_prompt", ""),
)
rp_frame.grid_columnconfigure(1, weight=1)
rp_frame.grid_rowconfigure(0, weight=1)
rp_frame.grid_rowconfigure(1, weight=1)
# Buttons
btn_frame = ttk.Frame(self.master)
btn_frame.pack(pady=10)
self.run_btn = ttk.Button(
btn_frame, text="Run execute()", command=self._run_execute_thread
)
self.run_btn.grid(row=0, column=0, padx=6)
ttk.Button(btn_frame, text="Close", command=self.on_close).grid(
row=0, column=1, padx=6
)
# Status bar
self.status = ttk.Label(
self.master, text="Ready", relief="sunken", anchor="w"
)
self.status.pack(fill="x", padx=0, pady=(4, 0), ipady=2)
def _mode_implies_mb(self, mode):
"""
概要: 翻訳モードがマルチバイト文字列(日本語)を意味するかどうかを判定する。
詳細説明: モード文字列が"j"で始まる場合、日本語ソースであるとみなし、Trueを返す。
:param mode: str: 翻訳モード文字列(例: "je", "ej")。
:returns: bool: マルチバイト文字列を意味する場合はTrue、それ以外はFalse。
"""
return True if mode and mode.startswith("j") else False
def _pick_infile(self):
"""
概要: 入力ファイルを選択するためのファイルダイアログを開く。
詳細説明: ユーザーがサポートされているファイルタイプ(.docx, .pptx, .pdf, .html, .md, .txt)の中から
ファイルを選択できるようにする。選択されたファイルのパスを `infile_var` に設定する。
:returns: None
"""
fp = filedialog.askopenfilename(
title="Select input file",
filetypes=[
(
"Supported",
"*.docx;*.pptx;*.pdf;*.html;*.htm;*.md;*.txt",
),
("All files", "*.*"),
],
)
if fp:
self.infile_var.set(fp)
def _pick_template(self):
"""
概要: HTMLテンプレートファイルを選択するためのファイルダイアログを開く。
詳細説明: ユーザーがHTMLファイル(.html, .htm)の中からファイルを選択できるようにする。
選択されたファイルのパスを `tpl_var` に設定する。
:returns: None
"""
fp = filedialog.askopenfilename(
title="Select HTML template",
filetypes=[("HTML", "*.html;*.htm"), ("All files", "*.*")],
)
if fp:
self.tpl_var.set(fp)
def _apply_api_to_models(self):
"""
概要: 現在選択されているAPIに基づいて、利用可能なモデルの選択肢を更新する。
詳細説明: `api_var` に設定されているAPIの種類(openai, openai5, google, gemini, deepl)に応じて、
`model_cb` コンボボックスの`values`を適切なモデルリストに更新し、デフォルト値を選択する。
:returns: None
"""
api = self.api_var.get()
if api == "openai":
self.model_cb["values"] = OPENAI_MODELS_V4
default = getattr(self.cfg, "openai_model", "gpt-4o")
self.model_var.set(
default if default in OPENAI_MODELS_V4 else "gpt-4o"
)
elif api == "openai5":
self.model_cb["values"] = OPENAI_MODELS_V5
default = getattr(self.cfg, "openai_model5", "gpt-5-nano")
self.model_var.set(
default if default in OPENAI_MODELS_V5 else "gpt-5-nano"
)
elif api in ("google", "gemini"):
self.model_cb["values"] = GOOGLE_MODELS
new_default = GOOGLE_MODELS[0] if GOOGLE_MODELS else ""
default = getattr(self.cfg, "google_model", new_default)
self.model_var.set(
default if default in GOOGLE_MODELS else new_default
)
elif api == "deepl":
self.model_cb["values"] = DEEPL_MODELS
self.model_var.set("(auto)")
else:
self.model_cb["values"] = []
self.model_var.set("")
def _worker_run(self):
"""
概要: 翻訳処理 `execute()` を別スレッドで実行するワーカー関数。
詳細説明: `translate5_com.execute()` を呼び出し、処理の成否に応じてステータスバーを更新する。
エラーが発生した場合は、コピー可能なエラーダイアログを表示する。
処理完了後、Runボタンを再度有効にする。
:returns: None
"""
try:
execute(self.cfg)
self._set_status("Done.")
except SystemExit:
self._set_status("Finished (SystemExit).")
except Exception as e:
error_details = traceback.format_exc()
self._set_status(f"Error: {e}")
self._show_error_async("Execution Error", error_details)
finally:
self.master.after(
0, lambda: self.run_btn.config(state="normal")
)
self._busy = False
def _show_error_async(self, title, msg):
"""
概要: 非同期でエラーダイアログを表示する。
詳細説明: GUIスレッドで `CopyableErrorDialog` を表示するようにスケジュールする。
これは、バックグラウンドスレッドからGUIを安全に更新するために使用される。
:param title: str: エラーダイアログのタイトル。
:param msg: str: エラーダイアログに表示するメッセージ。
:returns: None
"""
self.master.after(
0, lambda: CopyableErrorDialog(self.master, title, msg)
)
def _run_execute_thread(self):
"""
概要: 翻訳処理 `execute()` を新しいスレッドで開始する。
詳細説明: 実行中の場合や設定エラー、入力ファイルが見つからない場合は処理を中断する。
UIから設定を更新し、ステータスバーを「Running」に設定し、Runボタンを無効にして、
翻訳処理を実行する新しいデーモンスレッドを開始する。
:returns: None
"""
if self._busy:
return
try:
self._update_cfg_from_ui()
except Exception as e:
messagebox.showerror(
"設定エラー",
f"設定の読み取りに失敗しました:\n{e}",
)
return
if not os.path.isfile(self.cfg.infile):
messagebox.showerror(
"入力エラー",
f"入力ファイルが見つかりません:\n{self.cfg.infile}",
)
return
self._busy = True
self.status.config(text="Running execute() ...")
self.run_btn.config(state="disabled")
t = threading.Thread(target=self._worker_run)
t.daemon = True
t.start()
def _set_status(self, text):
"""
概要: ステータスバーのテキストを更新する。
詳細説明: GUIスレッドでステータスバーのテキストを安全に更新するために `master.after()` を使用する。
:param text: str: ステータスバーに表示するテキスト。
:returns: None
"""
self.master.after(0, lambda: self.status.config(text=text))
def _update_cfg_from_ui(self):
"""
概要: GUIの各入力値に基づいて、translate5の設定オブジェクト (`self.cfg`) を更新する。
詳細説明: 入力ファイルパス、テンプレートパス、翻訳モード、プロセス単位、API、モデル、
各種パラメータ(temperature, max_tokensなど)、プロンプト、APIキー(環境変数から)、
言語方向などを設定オブジェクトに反映させる。
:returns: None
"""
# 基本情報
self.cfg.infile = self.infile_var.get().strip()
self.cfg.html_template_path = (
self.tpl_var.get().strip() or "template_translate.html"
)
self.cfg.mode = self.mode_var.get()
# process_unit / use_md (md は GUI 独自指定として use_md=True にマップ)
proc_unit = self.proc_unit_var.get()
if proc_unit == "md":
self.cfg.use_md = True
# 実際の処理で使わないが一応 paragraph にしておく
self.cfg.process_unit = "paragraph"
else:
self.cfg.use_md = False
self.cfg.process_unit = proc_unit or "paragraph"
# API / model
api = self.api_var.get()
self.cfg.api = api
model = self.model_var.get()
if api == "openai":
self.cfg.openai_model = model
self.cfg.openai_model5 = None
self.cfg.google_model = None
elif api == "openai5":
self.cfg.openai_model5 = model
self.cfg.openai_model = None
self.cfg.google_model = None
elif api in ("google", "gemini"):
self.cfg.google_model = model
self.cfg.openai_model = None
self.cfg.openai_model5 = None
elif api == "deepl":
self.cfg.openai_model = None
self.cfg.openai_model5 = None
self.cfg.google_model = None
else:
self.cfg.openai_model = None
self.cfg.openai_model5 = None
self.cfg.google_model = None
# Endpoint は DeepL / custom のみ使用
if api == "deepl":
self.cfg.endpoint = (
self.endpoint_var.get().strip()
or getattr(self.cfg, "endpoint", None)
)
else:
self.cfg.endpoint = None
# 各種パラメータ
self.cfg.temperature = float(self.temp_var.get())
self.cfg.max_tokens = int(float(self.max_tok_var.get()))
self.cfg.reasoning_effort = self.effort_var.get()
self.cfg.min_translate_length = int(float(self.min_len_var.get()))
self.cfg.allowed_translation_length_ratio = float(
self.len_ratio_var.get()
)
self.cfg.limit_to_multibyte_str = bool(self.limit_mb_var.get())
# translate5 新仕様: prompt は一体型のみ
self.cfg.translate_prompt = (
self.tr_prompt.get("1.0", "end").strip()
or getattr(self.cfg, "translate_prompt", "")
)
self.cfg.reformat_prompt = (
self.rf_prompt.get("1.0", "end").strip()
or getattr(self.cfg, "reformat_prompt", "")
)
# APIキーなど(環境変数から)
self.cfg.openai_api_key = os.getenv("OPENAI_API_KEY")
self.cfg.google_api_key = os.getenv("GOOGLE_API_KEY")
self.cfg.deepl_api_key = os.getenv("DEEPL_API_KEY")
# その他固定値(translate5 と同等の既定値)
self.cfg.output_html_path = None
self.cfg.force_server_charcode = "utf-8"
self.cfg.tsleep_rpm = getattr(self.cfg, "tsleep_rpm", 0.0)
self.cfg.top_p = getattr(self.cfg, "top_p", 0.8)
self.cfg.top_k = getattr(self.cfg, "top_k", 40)
# mode から言語方向(translate5 と同じロジックに合わせて再計算)
m = self.cfg.mode
if m[0] == "j":
self.cfg.source_lang = "JA"
# GUIのチェック優先だが、je系ならデフォルトTrueでもよい
if not hasattr(self.cfg, "limit_to_multibyte_str"):
self.cfg.limit_to_multibyte_str = True
else:
self.cfg.source_lang = "EN"
if m[1] == "j":
self.cfg.target_lang = "JA"
else:
self.cfg.target_lang = "EN"
def _load_config(self):
"""
概要: 設定ファイル (`CONFIG_INI`) からアプリケーション設定を読み込む。
詳細説明: `CONFIG_INI` ファイルが存在する場合、その内容を読み込み、
GUIの各ウィジェット(Entry, Combobox, Textなど)に値を設定する。
読み込みに失敗してもエラーは表示せず、デフォルト値で続行する。
:returns: None
"""
if os.path.exists(CONFIG_INI):
try:
self.config.read(CONFIG_INI, encoding="utf-8")
s = self.config["Settings"]
if "geometry" in s:
self.master.geometry(s.get("geometry"))
self.infile_var.set(
s.get("infile", self.infile_var.get())
)
self.tpl_var.set(
s.get("html_template_path", self.tpl_var.get())
)
self.mode_var.set(
s.get("mode", self.mode_var.get())
)
self.proc_unit_var.set(
s.get("process_unit", self.proc_unit_var.get())
)
self.api_var.set(
s.get("api", self.api_var.get())
)
self.model_var.set(
s.get("model", self.model_var.get())
)
self.endpoint_var.set(
s.get("endpoint", self.endpoint_var.get())
)
self.temp_var.set(
s.get("temperature", self.temp_var.get())
)
self.max_tok_var.set(
s.get("max_tokens", self.max_tok_var.get())
)
self.effort_var.set(
s.get("reasoning_effort", self.effort_var.get())
)
self.min_len_var.set(
s.get(
"min_translate_length",
self.min_len_var.get(),
)
)
self.len_ratio_var.set(
s.get(
"allowed_translation_length_ratio",
self.len_ratio_var.get(),
)
)
self.limit_mb_var.set(
s.getboolean(
"limit_to_multibyte_str",
self.limit_mb_var.get(),
)
)
# promptのみ
tr_p = s.get("translate_prompt", "").strip()
if tr_p:
self.tr_prompt.delete("1.0", "end")
self.tr_prompt.insert("1.0", tr_p)
rf_p = s.get("reformat_prompt", "").strip()
if rf_p:
self.rf_prompt.delete("1.0", "end")
self.rf_prompt.insert("1.0", rf_p)
except Exception:
# 壊れていても無視してデフォルトで続行
pass
def _save_config(self):
"""
概要: アプリケーションの現在の設定をINIファイルに保存する。
詳細説明: GUIの各ウィジェットから現在の値を取得し、それを `CONFIG_INI` ファイルに書き込む。
設定保存中にエラーが発生した場合は警告メッセージを表示する。
:returns: None
"""
if "Settings" not in self.config:
self.config["Settings"] = {}
s = self.config["Settings"]
s["geometry"] = self.master.winfo_geometry()
s["infile"] = self.infile_var.get()
s["html_template_path"] = self.tpl_var.get()
s["mode"] = self.mode_var.get()
s["process_unit"] = self.proc_unit_var.get()
s["api"] = self.api_var.get()
s["model"] = self.model_var.get()
s["endpoint"] = self.endpoint_var.get()
s["temperature"] = self.temp_var.get()
s["max_tokens"] = self.max_tok_var.get()
s["reasoning_effort"] = self.effort_var.get()
s["min_translate_length"] = self.min_len_var.get()
s["allowed_translation_length_ratio"] = self.len_ratio_var.get()
s["limit_to_multibyte_str"] = str(bool(self.limit_mb_var.get()))
s["translate_prompt"] = self.tr_prompt.get("1.0", "end").strip()
s["reformat_prompt"] = self.rf_prompt.get("1.0", "end").strip()
try:
with open(CONFIG_INI, "w", encoding="utf-8") as f:
self.config.write(f)
except Exception as e:
messagebox.showwarning(
"保存警告",
f"設定保存に失敗しました: {e}",
)
[ドキュメント]
def on_close(self):
"""
概要: アプリケーションが閉じられる際に呼び出されるハンドラ。
詳細説明: 現在の設定をINIファイルに保存し、その後、ルートウィンドウを破棄する。
:returns: None
"""
try:
self._save_config()
finally:
self.master.destroy()
[ドキュメント]
def main():
"""
概要: アプリケーションのエントリポイント。
詳細説明: Tkinterのルートウィンドウを初期化し、可能であれば `ttkthemes` を使用してテーマを適用する。
`App` クラスのインスタンスを作成し、GUIアプリケーションのメインループを開始する。
:returns: None
"""
root = ThemedTk(theme="plastik") if ThemedTk else tk.Tk()
if not ThemedTk:
try:
style = ttk.Style()
style.theme_use("clam")
except Exception:
pass
App(root)
root.mainloop()
if __name__ == "__main__":
main()