2026-05-27 11:26:28 +08:00
|
|
|
|
"""灰阶 Pattern 配置面板(Gamma / CCT / 对比度 / EOTF 共用)。
|
|
|
|
|
|
|
|
|
|
|
|
特性:
|
|
|
|
|
|
- 多预设管理:内置 + 用户保存,支持 新建 / 另存为 / 复制 / 重命名 / 删除 / 导入 / 导出
|
|
|
|
|
|
- 内置预设锁定,不可覆盖、删除、改名
|
|
|
|
|
|
- 行内色块预览,前景色根据亮度自适应
|
|
|
|
|
|
- 生成器:N 点等分 / PQ 编码 / Gamma 2.2/2.4 曲线
|
|
|
|
|
|
- 剪贴板批量粘贴(每行 "R,G,B" / "R G B" / "灰度%")
|
|
|
|
|
|
- 自动校验:重复 / 越界 / 非灰阶 / 非单调
|
|
|
|
|
|
|
|
|
|
|
|
为保持向后兼容,仍导出 ``create_gamma_pattern_panel`` / ``toggle_gamma_pattern_panel``。
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
import json
|
|
|
|
|
|
import os
|
|
|
|
|
|
import sys
|
|
|
|
|
|
import tkinter as tk
|
|
|
|
|
|
from pathlib import Path
|
|
|
|
|
|
from tkinter import filedialog, messagebox, simpledialog
|
|
|
|
|
|
from typing import TYPE_CHECKING
|
|
|
|
|
|
|
|
|
|
|
|
import ttkbootstrap as ttk
|
|
|
|
|
|
|
|
|
|
|
|
from app.pq import pq_config
|
|
|
|
|
|
|
|
|
|
|
|
if TYPE_CHECKING:
|
|
|
|
|
|
from pqAutomationApp import PQAutomationApp
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ============================================================
|
|
|
|
|
|
# 列定义
|
|
|
|
|
|
# ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
_COLUMNS = ("idx", "pct", "r", "g", "b", "hex")
|
|
|
|
|
|
_HEADINGS = {
|
|
|
|
|
|
"idx": "#",
|
|
|
|
|
|
"pct": "灰度 %",
|
|
|
|
|
|
"r": "R",
|
|
|
|
|
|
"g": "G",
|
|
|
|
|
|
"b": "B",
|
|
|
|
|
|
"hex": "HEX",
|
|
|
|
|
|
}
|
|
|
|
|
|
_WIDTHS = {"idx": 40, "pct": 70, "r": 60, "g": 60, "b": 60, "hex": 80}
|
|
|
|
|
|
|
|
|
|
|
|
_TEST_KIND = "gray" # 当前仅管理灰阶系测试共用的 pattern
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ============================================================
|
|
|
|
|
|
# 工具
|
|
|
|
|
|
# ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
def _gray_pct_of(rgb) -> str:
|
|
|
|
|
|
try:
|
|
|
|
|
|
r = int(rgb[0])
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
return ""
|
|
|
|
|
|
return str(int(round(r / 255 * 100)))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _hex_of(rgb) -> str:
|
|
|
|
|
|
try:
|
|
|
|
|
|
r, g, b = int(rgb[0]), int(rgb[1]), int(rgb[2])
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
return ""
|
|
|
|
|
|
return f"#{r:02X}{g:02X}{b:02X}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _luminance(rgb) -> float:
|
|
|
|
|
|
r, g, b = rgb
|
|
|
|
|
|
return 0.2126 * r + 0.7152 * g + 0.0722 * b
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _fg_for_bg(rgb) -> str:
|
|
|
|
|
|
return "#000000" if _luminance(rgb) >= 140 else "#ffffff"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _get_settings_dir(self: "PQAutomationApp") -> str:
|
|
|
|
|
|
if getattr(self, "config_file", None):
|
|
|
|
|
|
return os.path.dirname(self.config_file)
|
|
|
|
|
|
if getattr(sys, "frozen", False):
|
|
|
|
|
|
base_dir = os.path.dirname(sys.executable)
|
|
|
|
|
|
else:
|
|
|
|
|
|
base_dir = os.path.dirname(
|
|
|
|
|
|
os.path.dirname(
|
|
|
|
|
|
os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
|
|
|
|
|
)
|
|
|
|
|
|
)
|
|
|
|
|
|
return os.path.join(base_dir, "settings")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ============================================================
|
|
|
|
|
|
# 内置生成器
|
|
|
|
|
|
# ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
def _gen_even(n: int) -> list[list[int]]:
|
|
|
|
|
|
n = max(2, n)
|
|
|
|
|
|
out = []
|
|
|
|
|
|
for i in range(n):
|
|
|
|
|
|
pct = 100.0 - (100.0 / (n - 1)) * i
|
|
|
|
|
|
v = int(round(pct / 100.0 * 255))
|
|
|
|
|
|
out.append([v, v, v])
|
|
|
|
|
|
return out
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _gen_pq(n: int) -> list[list[int]]:
|
|
|
|
|
|
"""N 点:线性 nits(0..10000)等分后 PQ 编码到 8-bit(低端更密集)。"""
|
|
|
|
|
|
n = max(2, n)
|
|
|
|
|
|
c1, c2, c3 = 0.8359375, 18.8515625, 18.6875
|
|
|
|
|
|
m1, m2 = 0.1593017578125, 78.84375
|
|
|
|
|
|
out = []
|
|
|
|
|
|
for i in range(n):
|
|
|
|
|
|
nits = 10000.0 * (1.0 - i / (n - 1))
|
|
|
|
|
|
L = max(0.0, nits / 10000.0)
|
|
|
|
|
|
if L <= 0:
|
|
|
|
|
|
v_pq = 0.0
|
|
|
|
|
|
else:
|
|
|
|
|
|
Lm1 = L ** m1
|
|
|
|
|
|
v_pq = ((c1 + c2 * Lm1) / (1.0 + c3 * Lm1)) ** m2
|
|
|
|
|
|
v = int(round(max(0.0, min(1.0, v_pq)) * 255))
|
|
|
|
|
|
out.append([v, v, v])
|
|
|
|
|
|
return out
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _gen_gamma(n: int, gamma: float = 2.2) -> list[list[int]]:
|
|
|
|
|
|
"""N 点:线性光 0..1 等分后 ^(1/gamma) 编码到 8-bit(暗端更密集)。"""
|
|
|
|
|
|
n = max(2, n)
|
|
|
|
|
|
out = []
|
|
|
|
|
|
for i in range(n):
|
|
|
|
|
|
lin = 1.0 - i / (n - 1)
|
|
|
|
|
|
v = int(round((lin ** (1.0 / gamma)) * 255))
|
|
|
|
|
|
out.append([v, v, v])
|
|
|
|
|
|
return out
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
_GENERATORS = {
|
|
|
|
|
|
"等分 (linear code)": _gen_even,
|
|
|
|
|
|
"PQ 编码 (HDR)": _gen_pq,
|
|
|
|
|
|
"Gamma 2.2 (SDR)": lambda n: _gen_gamma(n, 2.2),
|
|
|
|
|
|
"Gamma 2.4 (SDR)": lambda n: _gen_gamma(n, 2.4),
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ============================================================
|
|
|
|
|
|
# 面板入口
|
|
|
|
|
|
# ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
def create_gamma_pattern_panel(self: "PQAutomationApp"):
|
|
|
|
|
|
"""创建灰阶 Pattern 配置面板。"""
|
|
|
|
|
|
frame = ttk.Frame(self.content_frame)
|
|
|
|
|
|
self.gamma_pattern_frame = frame
|
|
|
|
|
|
self.gamma_pattern_visible = False
|
|
|
|
|
|
|
|
|
|
|
|
# 内部状态
|
|
|
|
|
|
self._gamma_pattern_params: list[list[int]] = []
|
|
|
|
|
|
self._gamma_current_preset: str | None = None
|
|
|
|
|
|
self._gamma_preset_locked: bool = False
|
|
|
|
|
|
self._gamma_dirty: bool = False
|
|
|
|
|
|
self._gamma_preset_list: list[dict] = []
|
|
|
|
|
|
|
|
|
|
|
|
root = ttk.Frame(frame, padding=10)
|
|
|
|
|
|
root.pack(fill=tk.BOTH, expand=True)
|
|
|
|
|
|
|
|
|
|
|
|
# ===== 标题 =====
|
|
|
|
|
|
title_row = ttk.Frame(root)
|
|
|
|
|
|
title_row.pack(fill=tk.X, pady=(0, 6))
|
|
|
|
|
|
ttk.Label(
|
|
|
|
|
|
title_row, text="灰阶 Pattern 配置", font=("微软雅黑", 14, "bold")
|
|
|
|
|
|
).pack(side=tk.LEFT)
|
|
|
|
|
|
ttk.Label(
|
|
|
|
|
|
title_row,
|
|
|
|
|
|
text="(Gamma / CCT / 对比度 / EOTF 共用此列表)",
|
2026-06-04 10:36:15 +08:00
|
|
|
|
style="Muted.TLabel",
|
2026-05-27 11:26:28 +08:00
|
|
|
|
).pack(side=tk.LEFT, padx=(8, 0))
|
|
|
|
|
|
|
|
|
|
|
|
# ===== 预设管理行 =====
|
|
|
|
|
|
preset_box = ttk.LabelFrame(root, text="预设", padding=8)
|
|
|
|
|
|
preset_box.pack(fill=tk.X, pady=(0, 8))
|
|
|
|
|
|
|
|
|
|
|
|
preset_row1 = ttk.Frame(preset_box)
|
|
|
|
|
|
preset_row1.pack(fill=tk.X)
|
|
|
|
|
|
|
|
|
|
|
|
ttk.Label(preset_row1, text="选择:").pack(side=tk.LEFT)
|
|
|
|
|
|
self._gamma_preset_var = tk.StringVar()
|
|
|
|
|
|
self._gamma_preset_combo = ttk.Combobox(
|
|
|
|
|
|
preset_row1,
|
|
|
|
|
|
textvariable=self._gamma_preset_var,
|
|
|
|
|
|
state="readonly",
|
|
|
|
|
|
width=32,
|
|
|
|
|
|
)
|
|
|
|
|
|
self._gamma_preset_combo.pack(side=tk.LEFT, padx=(4, 6))
|
|
|
|
|
|
self._gamma_preset_combo.bind(
|
|
|
|
|
|
"<<ComboboxSelected>>", lambda e: _on_preset_selected(self)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
ttk.Button(
|
|
|
|
|
|
preset_row1, text="加载",
|
|
|
|
|
|
bootstyle="info-outline", width=8,
|
|
|
|
|
|
command=lambda: _load_selected_preset(self),
|
|
|
|
|
|
).pack(side=tk.LEFT, padx=2)
|
|
|
|
|
|
|
|
|
|
|
|
ttk.Button(
|
|
|
|
|
|
preset_row1, text="应用为当前",
|
|
|
|
|
|
bootstyle="success", width=12,
|
|
|
|
|
|
command=lambda: _activate_selected_preset(self),
|
|
|
|
|
|
).pack(side=tk.LEFT, padx=2)
|
|
|
|
|
|
|
|
|
|
|
|
self._gamma_active_label = ttk.Label(
|
2026-06-04 10:36:15 +08:00
|
|
|
|
preset_row1, text="", style="SuccessState.TLabel", font=("微软雅黑", 9, "bold")
|
2026-05-27 11:26:28 +08:00
|
|
|
|
)
|
|
|
|
|
|
self._gamma_active_label.pack(side=tk.LEFT, padx=(10, 0))
|
|
|
|
|
|
|
|
|
|
|
|
# 第二行:CRUD
|
|
|
|
|
|
preset_row2 = ttk.Frame(preset_box)
|
|
|
|
|
|
preset_row2.pack(fill=tk.X, pady=(6, 0))
|
|
|
|
|
|
|
|
|
|
|
|
for txt, style, cmd in [
|
|
|
|
|
|
("新建空预设", "secondary-outline", lambda: _new_preset(self)),
|
|
|
|
|
|
("另存为...", "primary-outline", lambda: _save_as_preset(self)),
|
|
|
|
|
|
("复制...", "secondary-outline", lambda: _duplicate_preset(self)),
|
|
|
|
|
|
("重命名...", "secondary-outline", lambda: _rename_preset(self)),
|
|
|
|
|
|
("删除", "danger-outline", lambda: _delete_preset(self)),
|
|
|
|
|
|
("导入...", "secondary-outline", lambda: _import_preset(self)),
|
|
|
|
|
|
("导出...", "secondary-outline", lambda: _export_preset(self)),
|
|
|
|
|
|
]:
|
|
|
|
|
|
ttk.Button(
|
|
|
|
|
|
preset_row2, text=txt, bootstyle=style, width=11, command=cmd
|
|
|
|
|
|
).pack(side=tk.LEFT, padx=2)
|
|
|
|
|
|
|
|
|
|
|
|
# 描述行
|
|
|
|
|
|
self._gamma_meta_label = ttk.Label(
|
2026-06-04 10:36:15 +08:00
|
|
|
|
preset_box, text="", style="Muted.TLabel", font=("微软雅黑", 9)
|
2026-05-27 11:26:28 +08:00
|
|
|
|
)
|
|
|
|
|
|
self._gamma_meta_label.pack(anchor=tk.W, pady=(6, 0))
|
|
|
|
|
|
|
|
|
|
|
|
# ===== 中部:左表格 + 右编辑区 =====
|
|
|
|
|
|
mid = ttk.Frame(root)
|
|
|
|
|
|
mid.pack(fill=tk.BOTH, expand=True)
|
|
|
|
|
|
mid.columnconfigure(0, weight=1)
|
|
|
|
|
|
mid.columnconfigure(1, weight=0)
|
|
|
|
|
|
mid.rowconfigure(0, weight=1)
|
|
|
|
|
|
|
|
|
|
|
|
# ---- 左:表格 ----
|
|
|
|
|
|
table_frame = ttk.LabelFrame(mid, text="灰阶点列表", padding=6)
|
|
|
|
|
|
table_frame.grid(row=0, column=0, sticky=tk.NSEW, padx=(0, 8))
|
|
|
|
|
|
|
|
|
|
|
|
tree_container = ttk.Frame(table_frame)
|
|
|
|
|
|
tree_container.pack(fill=tk.BOTH, expand=True)
|
|
|
|
|
|
|
|
|
|
|
|
self.gamma_pattern_tree = ttk.Treeview(
|
|
|
|
|
|
tree_container,
|
|
|
|
|
|
columns=_COLUMNS,
|
|
|
|
|
|
show="headings",
|
|
|
|
|
|
height=18,
|
|
|
|
|
|
selectmode="browse",
|
|
|
|
|
|
)
|
|
|
|
|
|
for col in _COLUMNS:
|
|
|
|
|
|
self.gamma_pattern_tree.heading(col, text=_HEADINGS[col])
|
|
|
|
|
|
self.gamma_pattern_tree.column(col, width=_WIDTHS[col], anchor=tk.CENTER)
|
|
|
|
|
|
self.gamma_pattern_tree.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
|
|
|
|
|
|
|
|
|
|
|
scrollbar = ttk.Scrollbar(
|
|
|
|
|
|
tree_container, orient=tk.VERTICAL, command=self.gamma_pattern_tree.yview
|
|
|
|
|
|
)
|
|
|
|
|
|
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
|
|
|
|
|
self.gamma_pattern_tree.configure(yscrollcommand=scrollbar.set)
|
|
|
|
|
|
|
|
|
|
|
|
self.gamma_pattern_tree.bind(
|
|
|
|
|
|
"<<TreeviewSelect>>", lambda e: _on_select(self)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
# ---- 右:编辑区 ----
|
|
|
|
|
|
right = ttk.Frame(mid)
|
|
|
|
|
|
right.grid(row=0, column=1, sticky=tk.NS)
|
|
|
|
|
|
|
|
|
|
|
|
edit_frame = ttk.LabelFrame(right, text="编辑选中点", padding=8)
|
|
|
|
|
|
edit_frame.pack(fill=tk.X)
|
|
|
|
|
|
|
|
|
|
|
|
self._gamma_edit_r_var = tk.StringVar()
|
|
|
|
|
|
self._gamma_edit_g_var = tk.StringVar()
|
|
|
|
|
|
self._gamma_edit_b_var = tk.StringVar()
|
|
|
|
|
|
self._gamma_edit_pct_var = tk.StringVar()
|
|
|
|
|
|
|
|
|
|
|
|
ttk.Label(edit_frame, text="灰度 %").grid(row=0, column=0, sticky=tk.W, pady=3)
|
|
|
|
|
|
ttk.Entry(edit_frame, textvariable=self._gamma_edit_pct_var, width=10).grid(
|
|
|
|
|
|
row=0, column=1, sticky=tk.W, pady=3, padx=(4, 0)
|
|
|
|
|
|
)
|
|
|
|
|
|
ttk.Button(
|
|
|
|
|
|
edit_frame, text="按 % 填充",
|
|
|
|
|
|
bootstyle="secondary-outline", width=10,
|
|
|
|
|
|
command=lambda: _fill_rgb_from_pct(self),
|
|
|
|
|
|
).grid(row=0, column=2, padx=(6, 0), pady=3)
|
|
|
|
|
|
|
|
|
|
|
|
for i, ch in enumerate(("R", "G", "B"), start=1):
|
|
|
|
|
|
ttk.Label(edit_frame, text=ch).grid(row=i, column=0, sticky=tk.W, pady=3)
|
|
|
|
|
|
var = getattr(self, f"_gamma_edit_{ch.lower()}_var")
|
|
|
|
|
|
ttk.Entry(edit_frame, textvariable=var, width=10).grid(
|
|
|
|
|
|
row=i, column=1, sticky=tk.W, pady=3, padx=(4, 0)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
btn_row1 = ttk.Frame(edit_frame)
|
|
|
|
|
|
btn_row1.grid(row=4, column=0, columnspan=3, sticky=tk.EW, pady=(8, 0))
|
|
|
|
|
|
for txt, style, cmd in [
|
|
|
|
|
|
("更新选中", "primary", lambda: _update_selected(self)),
|
|
|
|
|
|
("新增", "success", lambda: _add_new(self)),
|
|
|
|
|
|
("删除", "danger-outline", lambda: _delete_selected(self)),
|
|
|
|
|
|
]:
|
|
|
|
|
|
ttk.Button(btn_row1, text=txt, bootstyle=style, width=8, command=cmd).pack(
|
|
|
|
|
|
side=tk.LEFT, padx=(0, 4)
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
btn_row2 = ttk.Frame(edit_frame)
|
|
|
|
|
|
btn_row2.grid(row=5, column=0, columnspan=3, sticky=tk.EW, pady=(4, 0))
|
|
|
|
|
|
ttk.Button(
|
|
|
|
|
|
btn_row2, text="↑ 上移",
|
|
|
|
|
|
bootstyle="secondary-outline", width=8,
|
|
|
|
|
|
command=lambda: _move_selected(self, -1),
|
|
|
|
|
|
).pack(side=tk.LEFT, padx=(0, 4))
|
|
|
|
|
|
ttk.Button(
|
|
|
|
|
|
btn_row2, text="↓ 下移",
|
|
|
|
|
|
bootstyle="secondary-outline", width=8,
|
|
|
|
|
|
command=lambda: _move_selected(self, 1),
|
|
|
|
|
|
).pack(side=tk.LEFT)
|
|
|
|
|
|
|
|
|
|
|
|
# ---- 生成器 ----
|
|
|
|
|
|
gen_frame = ttk.LabelFrame(right, text="按曲线生成", padding=8)
|
|
|
|
|
|
gen_frame.pack(fill=tk.X, pady=(10, 0))
|
|
|
|
|
|
|
|
|
|
|
|
ttk.Label(gen_frame, text="类型").grid(row=0, column=0, sticky=tk.W)
|
|
|
|
|
|
self._gamma_gen_type_var = tk.StringVar(value=next(iter(_GENERATORS)))
|
|
|
|
|
|
ttk.Combobox(
|
|
|
|
|
|
gen_frame, textvariable=self._gamma_gen_type_var,
|
|
|
|
|
|
values=list(_GENERATORS.keys()), state="readonly", width=18,
|
|
|
|
|
|
).grid(row=0, column=1, sticky=tk.W, padx=(4, 0))
|
|
|
|
|
|
|
|
|
|
|
|
ttk.Label(gen_frame, text="点数 N").grid(row=1, column=0, sticky=tk.W, pady=(6, 0))
|
|
|
|
|
|
self._gamma_n_points_var = tk.StringVar(value="11")
|
|
|
|
|
|
ttk.Entry(gen_frame, textvariable=self._gamma_n_points_var, width=10).grid(
|
|
|
|
|
|
row=1, column=1, sticky=tk.W, padx=(4, 0), pady=(6, 0)
|
|
|
|
|
|
)
|
|
|
|
|
|
ttk.Button(
|
|
|
|
|
|
gen_frame, text="生成",
|
|
|
|
|
|
bootstyle="info-outline",
|
|
|
|
|
|
command=lambda: _generate_by_curve(self),
|
|
|
|
|
|
).grid(row=2, column=0, columnspan=2, sticky=tk.EW, pady=(6, 0))
|
|
|
|
|
|
|
|
|
|
|
|
# ---- 剪贴板粘贴 ----
|
|
|
|
|
|
paste_frame = ttk.LabelFrame(right, text="批量粘贴", padding=8)
|
|
|
|
|
|
paste_frame.pack(fill=tk.X, pady=(10, 0))
|
|
|
|
|
|
ttk.Label(
|
|
|
|
|
|
paste_frame, text="每行:R,G,B 或 R G B\n或:灰度% (如 50%)",
|
2026-06-04 10:36:15 +08:00
|
|
|
|
style="Muted.TLabel", justify=tk.LEFT,
|
2026-05-27 11:26:28 +08:00
|
|
|
|
).pack(anchor=tk.W)
|
|
|
|
|
|
ttk.Button(
|
|
|
|
|
|
paste_frame, text="从剪贴板导入",
|
|
|
|
|
|
bootstyle="secondary-outline",
|
|
|
|
|
|
command=lambda: _paste_from_clipboard(self),
|
|
|
|
|
|
).pack(fill=tk.X, pady=(6, 0))
|
|
|
|
|
|
|
|
|
|
|
|
# ===== 底部 =====
|
|
|
|
|
|
bottom = ttk.LabelFrame(root, text="校验与保存", padding=8)
|
|
|
|
|
|
bottom.pack(fill=tk.X, pady=(10, 0))
|
|
|
|
|
|
|
|
|
|
|
|
self._gamma_validate_label = ttk.Label(
|
2026-06-04 10:36:15 +08:00
|
|
|
|
bottom, text="", style="Muted.TLabel", justify=tk.LEFT
|
2026-05-27 11:26:28 +08:00
|
|
|
|
)
|
|
|
|
|
|
self._gamma_validate_label.pack(anchor=tk.W)
|
|
|
|
|
|
|
|
|
|
|
|
save_row = ttk.Frame(bottom)
|
|
|
|
|
|
save_row.pack(fill=tk.X, pady=(6, 0))
|
|
|
|
|
|
ttk.Button(
|
|
|
|
|
|
save_row, text="保存改动到当前预设",
|
|
|
|
|
|
bootstyle="primary",
|
|
|
|
|
|
command=lambda: _save_to_current_preset(self),
|
|
|
|
|
|
).pack(side=tk.LEFT)
|
|
|
|
|
|
ttk.Button(
|
|
|
|
|
|
save_row, text="应用到运行时 (gray.json)",
|
|
|
|
|
|
bootstyle="success",
|
|
|
|
|
|
command=lambda: _apply_current_to_runtime(self),
|
|
|
|
|
|
).pack(side=tk.LEFT, padx=(6, 0))
|
|
|
|
|
|
ttk.Button(
|
|
|
|
|
|
save_row, text="另存为新预设...",
|
|
|
|
|
|
bootstyle="info-outline",
|
|
|
|
|
|
command=lambda: _save_as_preset(self),
|
|
|
|
|
|
).pack(side=tk.RIGHT)
|
|
|
|
|
|
|
|
|
|
|
|
# 注册并初始化
|
|
|
|
|
|
self.register_panel("gamma_pattern", frame, None, "gamma_pattern_visible")
|
|
|
|
|
|
_refresh_preset_combo(self)
|
|
|
|
|
|
_load_initial(self)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def toggle_gamma_pattern_panel(self: "PQAutomationApp"):
|
|
|
|
|
|
"""切换面板显隐。"""
|
|
|
|
|
|
self.show_panel("gamma_pattern")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ============================================================
|
|
|
|
|
|
# 预设管理
|
|
|
|
|
|
# ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
def _refresh_preset_combo(self: "PQAutomationApp", select: str | None = None):
|
|
|
|
|
|
presets = pq_config.list_presets(_TEST_KIND)
|
|
|
|
|
|
self._gamma_preset_list = presets
|
|
|
|
|
|
names = [p["name"] for p in presets]
|
|
|
|
|
|
labels = [
|
|
|
|
|
|
f"{'🔒 ' if p['locked'] else ''}{p['name']} ({p['point_count']}点)"
|
|
|
|
|
|
for p in presets
|
|
|
|
|
|
]
|
|
|
|
|
|
self._gamma_preset_combo["values"] = labels
|
|
|
|
|
|
|
|
|
|
|
|
target = select or self._gamma_current_preset
|
|
|
|
|
|
if target and target in names:
|
|
|
|
|
|
self._gamma_preset_var.set(labels[names.index(target)])
|
|
|
|
|
|
elif labels:
|
|
|
|
|
|
self._gamma_preset_var.set(labels[0])
|
|
|
|
|
|
|
|
|
|
|
|
_update_active_label(self)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _selected_preset_name(self: "PQAutomationApp") -> str | None:
|
|
|
|
|
|
label = self._gamma_preset_var.get()
|
|
|
|
|
|
if not label:
|
|
|
|
|
|
return None
|
|
|
|
|
|
for p in self._gamma_preset_list:
|
|
|
|
|
|
prefix = "🔒 " if p["locked"] else ""
|
|
|
|
|
|
if label.startswith(prefix + p["name"]):
|
|
|
|
|
|
return p["name"]
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _update_active_label(self: "PQAutomationApp"):
|
|
|
|
|
|
active = pq_config.get_active_preset_name(_TEST_KIND)
|
|
|
|
|
|
current = self._gamma_current_preset
|
|
|
|
|
|
if active and current == active and not self._gamma_dirty:
|
|
|
|
|
|
self._gamma_active_label.config(
|
2026-06-04 10:36:15 +08:00
|
|
|
|
text=f"✔ 当前激活:{active}", style="SuccessState.TLabel"
|
2026-05-27 11:26:28 +08:00
|
|
|
|
)
|
|
|
|
|
|
elif active:
|
|
|
|
|
|
extra = "(有未保存改动)" if self._gamma_dirty else ""
|
|
|
|
|
|
self._gamma_active_label.config(
|
|
|
|
|
|
text=f"● 激活:{active} 编辑中:{current or '-'}{extra}",
|
2026-06-04 10:36:15 +08:00
|
|
|
|
style="WarningState.TLabel" if self._gamma_dirty else "Muted.TLabel",
|
2026-05-27 11:26:28 +08:00
|
|
|
|
)
|
|
|
|
|
|
else:
|
2026-06-04 10:36:15 +08:00
|
|
|
|
self._gamma_active_label.config(text="● 未激活任何预设", style="Muted.TLabel")
|
2026-05-27 11:26:28 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _on_preset_selected(self: "PQAutomationApp"):
|
|
|
|
|
|
"""选中下拉项时直接加载(含未保存提醒)。"""
|
|
|
|
|
|
if self._gamma_dirty:
|
|
|
|
|
|
if not messagebox.askyesno(
|
|
|
|
|
|
"未保存改动", "当前预设有未保存改动,确定切换?切换后改动将丢失。"
|
|
|
|
|
|
):
|
|
|
|
|
|
_refresh_preset_combo(self, select=self._gamma_current_preset)
|
|
|
|
|
|
return
|
|
|
|
|
|
_load_selected_preset(self)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _load_selected_preset(self: "PQAutomationApp"):
|
|
|
|
|
|
name = _selected_preset_name(self)
|
|
|
|
|
|
if not name:
|
|
|
|
|
|
return
|
|
|
|
|
|
try:
|
|
|
|
|
|
data = pq_config.load_preset(_TEST_KIND, name)
|
|
|
|
|
|
except (FileNotFoundError, json.JSONDecodeError) as exc:
|
|
|
|
|
|
messagebox.showerror("加载失败", str(exc))
|
|
|
|
|
|
return
|
|
|
|
|
|
self._gamma_current_preset = name
|
|
|
|
|
|
self._gamma_preset_locked = bool((data.get("_meta") or {}).get("locked"))
|
|
|
|
|
|
self._gamma_pattern_params = [
|
|
|
|
|
|
list(map(int, rgb)) for rgb in (data.get("pattern_params") or [])
|
|
|
|
|
|
]
|
|
|
|
|
|
self._gamma_dirty = False
|
|
|
|
|
|
_refresh_tree(self)
|
|
|
|
|
|
_update_meta_label(self, data)
|
|
|
|
|
|
_update_active_label(self)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _activate_selected_preset(self: "PQAutomationApp"):
|
|
|
|
|
|
name = _selected_preset_name(self)
|
|
|
|
|
|
if not name:
|
|
|
|
|
|
messagebox.showinfo("提示", "请先选择一个预设")
|
|
|
|
|
|
return
|
|
|
|
|
|
if self._gamma_dirty and self._gamma_current_preset == name:
|
|
|
|
|
|
if not messagebox.askyesno(
|
|
|
|
|
|
"未保存改动", "当前编辑未保存,将使用磁盘上原始预设进行激活。继续?"
|
|
|
|
|
|
):
|
|
|
|
|
|
return
|
|
|
|
|
|
try:
|
|
|
|
|
|
pq_config.activate_preset(_TEST_KIND, name)
|
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
|
messagebox.showerror("激活失败", str(exc))
|
|
|
|
|
|
return
|
|
|
|
|
|
if hasattr(self, "log_gui"):
|
|
|
|
|
|
self.log_gui.log(f"已激活灰阶预设:{name}", level="success")
|
|
|
|
|
|
_update_active_label(self)
|
|
|
|
|
|
messagebox.showinfo("成功", f"已应用预设 '{name}' 到运行时。")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _new_preset(self: "PQAutomationApp"):
|
|
|
|
|
|
name = simpledialog.askstring(
|
|
|
|
|
|
"新建预设", "预设名(建议英文/数字/下划线):",
|
|
|
|
|
|
parent=self.gamma_pattern_frame,
|
|
|
|
|
|
)
|
|
|
|
|
|
if not name:
|
|
|
|
|
|
return
|
|
|
|
|
|
try:
|
|
|
|
|
|
path = pq_config.save_preset(
|
|
|
|
|
|
_TEST_KIND, name,
|
|
|
|
|
|
{"pattern_params": _gen_even(11)},
|
|
|
|
|
|
description="新建空白预设",
|
|
|
|
|
|
generator="manual",
|
|
|
|
|
|
overwrite=False,
|
|
|
|
|
|
)
|
|
|
|
|
|
except (FileExistsError, PermissionError, ValueError) as exc:
|
|
|
|
|
|
messagebox.showerror("创建失败", str(exc))
|
|
|
|
|
|
return
|
|
|
|
|
|
_refresh_preset_combo(self, select=Path(path).stem)
|
|
|
|
|
|
_load_selected_preset(self)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _save_as_preset(self: "PQAutomationApp"):
|
|
|
|
|
|
if not self._gamma_pattern_params:
|
|
|
|
|
|
messagebox.showerror("错误", "当前列表为空,无法另存")
|
|
|
|
|
|
return
|
|
|
|
|
|
name = simpledialog.askstring(
|
|
|
|
|
|
"另存为预设", "新预设名:", parent=self.gamma_pattern_frame,
|
|
|
|
|
|
initialvalue=(self._gamma_current_preset or "untitled").lstrip("_") + "_copy",
|
|
|
|
|
|
)
|
|
|
|
|
|
if not name:
|
|
|
|
|
|
return
|
|
|
|
|
|
try:
|
|
|
|
|
|
path = pq_config.save_preset(
|
|
|
|
|
|
_TEST_KIND, name,
|
|
|
|
|
|
{"pattern_params": self._gamma_pattern_params},
|
|
|
|
|
|
description="用户另存",
|
|
|
|
|
|
generator="manual",
|
|
|
|
|
|
overwrite=False,
|
|
|
|
|
|
)
|
|
|
|
|
|
except (FileExistsError, PermissionError, ValueError) as exc:
|
|
|
|
|
|
messagebox.showerror("保存失败", str(exc))
|
|
|
|
|
|
return
|
|
|
|
|
|
self._gamma_dirty = False
|
|
|
|
|
|
_refresh_preset_combo(self, select=Path(path).stem)
|
|
|
|
|
|
_load_selected_preset(self)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _duplicate_preset(self: "PQAutomationApp"):
|
|
|
|
|
|
src = _selected_preset_name(self)
|
|
|
|
|
|
if not src:
|
|
|
|
|
|
return
|
|
|
|
|
|
new_name = simpledialog.askstring(
|
|
|
|
|
|
"复制预设", "新名字:", parent=self.gamma_pattern_frame,
|
|
|
|
|
|
initialvalue=src.lstrip("_") + "_copy",
|
|
|
|
|
|
)
|
|
|
|
|
|
if not new_name:
|
|
|
|
|
|
return
|
|
|
|
|
|
try:
|
|
|
|
|
|
path = pq_config.duplicate_preset(_TEST_KIND, src, new_name)
|
|
|
|
|
|
except (FileExistsError, FileNotFoundError, ValueError) as exc:
|
|
|
|
|
|
messagebox.showerror("复制失败", str(exc))
|
|
|
|
|
|
return
|
|
|
|
|
|
_refresh_preset_combo(self, select=Path(path).stem)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _rename_preset(self: "PQAutomationApp"):
|
|
|
|
|
|
src = _selected_preset_name(self)
|
|
|
|
|
|
if not src:
|
|
|
|
|
|
return
|
|
|
|
|
|
new_name = simpledialog.askstring(
|
|
|
|
|
|
"重命名预设", "新名字:", parent=self.gamma_pattern_frame, initialvalue=src
|
|
|
|
|
|
)
|
|
|
|
|
|
if not new_name or new_name == src:
|
|
|
|
|
|
return
|
|
|
|
|
|
try:
|
|
|
|
|
|
path = pq_config.rename_preset(_TEST_KIND, src, new_name)
|
|
|
|
|
|
except (FileExistsError, FileNotFoundError, PermissionError, ValueError) as exc:
|
|
|
|
|
|
messagebox.showerror("重命名失败", str(exc))
|
|
|
|
|
|
return
|
|
|
|
|
|
if self._gamma_current_preset == src:
|
|
|
|
|
|
self._gamma_current_preset = Path(path).stem
|
|
|
|
|
|
_refresh_preset_combo(self, select=Path(path).stem)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _delete_preset(self: "PQAutomationApp"):
|
|
|
|
|
|
src = _selected_preset_name(self)
|
|
|
|
|
|
if not src:
|
|
|
|
|
|
return
|
|
|
|
|
|
if not messagebox.askyesno("确认", f"确定删除预设 '{src}' ?"):
|
|
|
|
|
|
return
|
|
|
|
|
|
try:
|
|
|
|
|
|
pq_config.delete_preset(_TEST_KIND, src)
|
|
|
|
|
|
except (FileNotFoundError, PermissionError) as exc:
|
|
|
|
|
|
messagebox.showerror("删除失败", str(exc))
|
|
|
|
|
|
return
|
|
|
|
|
|
if self._gamma_current_preset == src:
|
|
|
|
|
|
self._gamma_current_preset = None
|
|
|
|
|
|
self._gamma_pattern_params = []
|
|
|
|
|
|
_refresh_tree(self)
|
|
|
|
|
|
_refresh_preset_combo(self)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _import_preset(self: "PQAutomationApp"):
|
|
|
|
|
|
fp = filedialog.askopenfilename(
|
|
|
|
|
|
title="选择 Pattern JSON 文件",
|
|
|
|
|
|
filetypes=[("JSON", "*.json"), ("All", "*.*")],
|
|
|
|
|
|
initialdir=_get_settings_dir(self),
|
|
|
|
|
|
)
|
|
|
|
|
|
if not fp:
|
|
|
|
|
|
return
|
|
|
|
|
|
name = simpledialog.askstring(
|
|
|
|
|
|
"导入预设", "保存为预设名:", parent=self.gamma_pattern_frame,
|
|
|
|
|
|
initialvalue=Path(fp).stem,
|
|
|
|
|
|
)
|
|
|
|
|
|
if not name:
|
|
|
|
|
|
return
|
|
|
|
|
|
try:
|
|
|
|
|
|
path = pq_config.import_preset_from_file(_TEST_KIND, fp, name=name)
|
|
|
|
|
|
except (ValueError, json.JSONDecodeError, OSError) as exc:
|
|
|
|
|
|
messagebox.showerror("导入失败", str(exc))
|
|
|
|
|
|
return
|
|
|
|
|
|
_refresh_preset_combo(self, select=Path(path).stem)
|
|
|
|
|
|
_load_selected_preset(self)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _export_preset(self: "PQAutomationApp"):
|
|
|
|
|
|
src = _selected_preset_name(self)
|
|
|
|
|
|
if not src:
|
|
|
|
|
|
return
|
|
|
|
|
|
fp = filedialog.asksaveasfilename(
|
|
|
|
|
|
title="导出预设到文件",
|
|
|
|
|
|
filetypes=[("JSON", "*.json")],
|
|
|
|
|
|
defaultextension=".json",
|
|
|
|
|
|
initialfile=f"{src}.json",
|
|
|
|
|
|
initialdir=_get_settings_dir(self),
|
|
|
|
|
|
)
|
|
|
|
|
|
if not fp:
|
|
|
|
|
|
return
|
|
|
|
|
|
try:
|
|
|
|
|
|
pq_config.export_preset_to_file(_TEST_KIND, src, fp)
|
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
|
messagebox.showerror("导出失败", str(exc))
|
|
|
|
|
|
return
|
|
|
|
|
|
messagebox.showinfo("导出成功", f"已导出到\n{fp}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _save_to_current_preset(self: "PQAutomationApp"):
|
|
|
|
|
|
name = self._gamma_current_preset
|
|
|
|
|
|
if not name:
|
|
|
|
|
|
messagebox.showinfo("提示", "没有当前预设,请先'新建'或'另存为'")
|
|
|
|
|
|
return
|
|
|
|
|
|
if self._gamma_preset_locked:
|
|
|
|
|
|
messagebox.showwarning(
|
|
|
|
|
|
"预设已锁定",
|
|
|
|
|
|
f"'{name}' 是内置预设,不能直接覆盖。请使用'另存为'保存为新预设。",
|
|
|
|
|
|
)
|
|
|
|
|
|
return
|
|
|
|
|
|
if len(self._gamma_pattern_params) < 2:
|
|
|
|
|
|
messagebox.showerror("错误", "至少需要 2 个灰阶点")
|
|
|
|
|
|
return
|
|
|
|
|
|
try:
|
|
|
|
|
|
existing_meta = (pq_config.load_preset(_TEST_KIND, name).get("_meta") or {})
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
existing_meta = {}
|
|
|
|
|
|
try:
|
|
|
|
|
|
pq_config.save_preset(
|
|
|
|
|
|
_TEST_KIND, name,
|
|
|
|
|
|
{"pattern_params": self._gamma_pattern_params},
|
|
|
|
|
|
description=existing_meta.get("description", ""),
|
|
|
|
|
|
generator=existing_meta.get("generator", "manual"),
|
|
|
|
|
|
overwrite=True,
|
|
|
|
|
|
)
|
|
|
|
|
|
except (PermissionError, ValueError) as exc:
|
|
|
|
|
|
messagebox.showerror("保存失败", str(exc))
|
|
|
|
|
|
return
|
|
|
|
|
|
self._gamma_dirty = False
|
|
|
|
|
|
_refresh_preset_combo(self, select=name)
|
|
|
|
|
|
if hasattr(self, "log_gui"):
|
|
|
|
|
|
self.log_gui.log(f"已保存预设:{name}", level="success")
|
|
|
|
|
|
messagebox.showinfo("成功", f"已保存到预设 '{name}'。\n如需生效请点'应用为当前'或'应用到运行时'。")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _apply_current_to_runtime(self: "PQAutomationApp"):
|
|
|
|
|
|
"""把当前编辑结果直接写入 gray.json 并热加载(不必先保存为预设)。"""
|
|
|
|
|
|
if len(self._gamma_pattern_params) < 2:
|
|
|
|
|
|
messagebox.showerror("错误", "至少需要 2 个灰阶点")
|
|
|
|
|
|
return
|
|
|
|
|
|
pattern = {
|
|
|
|
|
|
"pattern_mode": "SolidColor",
|
|
|
|
|
|
"measurement_bit_depth": 8,
|
|
|
|
|
|
"measurement_max_value": len(self._gamma_pattern_params) - 1,
|
|
|
|
|
|
"pattern_params": [list(map(int, rgb)) for rgb in self._gamma_pattern_params],
|
|
|
|
|
|
}
|
|
|
|
|
|
path = Path(_get_settings_dir(self)) / "patterns" / "gray.json"
|
|
|
|
|
|
try:
|
|
|
|
|
|
pq_config.save_pattern_file(path, pattern)
|
|
|
|
|
|
pq_config.reload_gray_pattern()
|
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
|
messagebox.showerror("应用失败", str(exc))
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
# 若当前编辑与磁盘上同名预设内容一致,则把该预设标记为激活
|
|
|
|
|
|
if self._gamma_current_preset:
|
|
|
|
|
|
try:
|
|
|
|
|
|
disk = pq_config.load_preset(_TEST_KIND, self._gamma_current_preset)
|
|
|
|
|
|
if (disk.get("pattern_params") or []) == self._gamma_pattern_params:
|
|
|
|
|
|
pq_config.activate_preset(_TEST_KIND, self._gamma_current_preset)
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
if hasattr(self, "log_gui"):
|
|
|
|
|
|
self.log_gui.log(
|
|
|
|
|
|
f"已应用 {len(self._gamma_pattern_params)} 点灰阶 pattern 到运行时",
|
|
|
|
|
|
level="success",
|
|
|
|
|
|
)
|
|
|
|
|
|
_update_active_label(self)
|
|
|
|
|
|
messagebox.showinfo("成功", "已应用到运行时,立即生效。")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ============================================================
|
|
|
|
|
|
# 表格 / 编辑
|
|
|
|
|
|
# ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
def _refresh_tree(self: "PQAutomationApp"):
|
|
|
|
|
|
tree = self.gamma_pattern_tree
|
|
|
|
|
|
tree.delete(*tree.get_children())
|
|
|
|
|
|
for i, rgb in enumerate(self._gamma_pattern_params):
|
|
|
|
|
|
try:
|
|
|
|
|
|
r, g, b = int(rgb[0]), int(rgb[1]), int(rgb[2])
|
|
|
|
|
|
except (TypeError, ValueError, IndexError):
|
|
|
|
|
|
r = g = b = 0
|
|
|
|
|
|
tag = f"row_{i}"
|
|
|
|
|
|
bg = f"#{r:02x}{g:02x}{b:02x}"
|
|
|
|
|
|
fg = _fg_for_bg([r, g, b])
|
|
|
|
|
|
tree.tag_configure(tag, background=bg, foreground=fg)
|
|
|
|
|
|
tree.insert(
|
|
|
|
|
|
"", tk.END, iid=str(i),
|
|
|
|
|
|
values=(i, _gray_pct_of([r, g, b]), r, g, b, _hex_of([r, g, b])),
|
|
|
|
|
|
tags=(tag,),
|
|
|
|
|
|
)
|
|
|
|
|
|
_run_validation(self)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _selected_index(self: "PQAutomationApp"):
|
|
|
|
|
|
sel = self.gamma_pattern_tree.selection()
|
|
|
|
|
|
if not sel:
|
|
|
|
|
|
return None
|
|
|
|
|
|
try:
|
|
|
|
|
|
return int(sel[0])
|
|
|
|
|
|
except ValueError:
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _on_select(self: "PQAutomationApp"):
|
|
|
|
|
|
idx = _selected_index(self)
|
|
|
|
|
|
if idx is None or idx >= len(self._gamma_pattern_params):
|
|
|
|
|
|
return
|
|
|
|
|
|
r, g, b = self._gamma_pattern_params[idx]
|
|
|
|
|
|
self._gamma_edit_r_var.set(str(r))
|
|
|
|
|
|
self._gamma_edit_g_var.set(str(g))
|
|
|
|
|
|
self._gamma_edit_b_var.set(str(b))
|
|
|
|
|
|
self._gamma_edit_pct_var.set(_gray_pct_of([r, g, b]))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _parse_rgb_from_form(self: "PQAutomationApp"):
|
|
|
|
|
|
try:
|
|
|
|
|
|
r = int(self._gamma_edit_r_var.get().strip())
|
|
|
|
|
|
g = int(self._gamma_edit_g_var.get().strip())
|
|
|
|
|
|
b = int(self._gamma_edit_b_var.get().strip())
|
|
|
|
|
|
except ValueError:
|
|
|
|
|
|
messagebox.showerror("输入错误", "R/G/B 必须为整数")
|
|
|
|
|
|
return None
|
|
|
|
|
|
if not all(0 <= v <= 255 for v in (r, g, b)):
|
|
|
|
|
|
messagebox.showerror("输入错误", "R/G/B 必须在 0~255 范围内")
|
|
|
|
|
|
return None
|
|
|
|
|
|
return [r, g, b]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _fill_rgb_from_pct(self: "PQAutomationApp"):
|
|
|
|
|
|
raw = self._gamma_edit_pct_var.get().strip().rstrip("%")
|
|
|
|
|
|
try:
|
|
|
|
|
|
pct = float(raw)
|
|
|
|
|
|
except ValueError:
|
|
|
|
|
|
messagebox.showerror("输入错误", "灰度 % 必须为数字")
|
|
|
|
|
|
return
|
|
|
|
|
|
pct = max(0.0, min(100.0, pct))
|
|
|
|
|
|
v = int(round(pct / 100.0 * 255))
|
|
|
|
|
|
self._gamma_edit_r_var.set(str(v))
|
|
|
|
|
|
self._gamma_edit_g_var.set(str(v))
|
|
|
|
|
|
self._gamma_edit_b_var.set(str(v))
|
|
|
|
|
|
self._gamma_edit_pct_var.set(str(int(round(pct))))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _mark_dirty(self: "PQAutomationApp"):
|
|
|
|
|
|
self._gamma_dirty = True
|
|
|
|
|
|
_update_active_label(self)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _check_locked_or_prompt(self: "PQAutomationApp") -> bool:
|
|
|
|
|
|
"""锁定预设编辑前提示用户。返回 True 表示允许继续编辑。"""
|
|
|
|
|
|
if not self._gamma_preset_locked:
|
|
|
|
|
|
return True
|
|
|
|
|
|
return messagebox.askyesno(
|
|
|
|
|
|
"预设已锁定",
|
|
|
|
|
|
"当前是内置预设,改动无法直接覆盖(保存时需'另存为')。仍要编辑吗?",
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _update_selected(self: "PQAutomationApp"):
|
|
|
|
|
|
idx = _selected_index(self)
|
|
|
|
|
|
if idx is None:
|
|
|
|
|
|
messagebox.showinfo("提示", "请先在列表中选中一行")
|
|
|
|
|
|
return
|
|
|
|
|
|
rgb = _parse_rgb_from_form(self)
|
|
|
|
|
|
if rgb is None:
|
|
|
|
|
|
return
|
|
|
|
|
|
if not _check_locked_or_prompt(self):
|
|
|
|
|
|
return
|
|
|
|
|
|
self._gamma_pattern_params[idx] = rgb
|
|
|
|
|
|
_mark_dirty(self)
|
|
|
|
|
|
_refresh_tree(self)
|
|
|
|
|
|
try:
|
|
|
|
|
|
self.gamma_pattern_tree.selection_set(str(idx))
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _add_new(self: "PQAutomationApp"):
|
|
|
|
|
|
rgb = _parse_rgb_from_form(self)
|
|
|
|
|
|
if rgb is None:
|
|
|
|
|
|
return
|
|
|
|
|
|
if not _check_locked_or_prompt(self):
|
|
|
|
|
|
return
|
|
|
|
|
|
idx = _selected_index(self)
|
|
|
|
|
|
insert_at = (idx + 1) if idx is not None else len(self._gamma_pattern_params)
|
|
|
|
|
|
self._gamma_pattern_params.insert(insert_at, rgb)
|
|
|
|
|
|
_mark_dirty(self)
|
|
|
|
|
|
_refresh_tree(self)
|
|
|
|
|
|
try:
|
|
|
|
|
|
self.gamma_pattern_tree.selection_set(str(insert_at))
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _delete_selected(self: "PQAutomationApp"):
|
|
|
|
|
|
idx = _selected_index(self)
|
|
|
|
|
|
if idx is None:
|
|
|
|
|
|
messagebox.showinfo("提示", "请先在列表中选中一行")
|
|
|
|
|
|
return
|
|
|
|
|
|
if len(self._gamma_pattern_params) <= 2:
|
|
|
|
|
|
messagebox.showwarning("提示", "至少保留 2 个灰阶点")
|
|
|
|
|
|
return
|
|
|
|
|
|
if not _check_locked_or_prompt(self):
|
|
|
|
|
|
return
|
|
|
|
|
|
del self._gamma_pattern_params[idx]
|
|
|
|
|
|
_mark_dirty(self)
|
|
|
|
|
|
_refresh_tree(self)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _move_selected(self: "PQAutomationApp", delta: int):
|
|
|
|
|
|
idx = _selected_index(self)
|
|
|
|
|
|
if idx is None:
|
|
|
|
|
|
return
|
|
|
|
|
|
new_idx = idx + delta
|
|
|
|
|
|
if new_idx < 0 or new_idx >= len(self._gamma_pattern_params):
|
|
|
|
|
|
return
|
|
|
|
|
|
if not _check_locked_or_prompt(self):
|
|
|
|
|
|
return
|
|
|
|
|
|
params = self._gamma_pattern_params
|
|
|
|
|
|
params[idx], params[new_idx] = params[new_idx], params[idx]
|
|
|
|
|
|
_mark_dirty(self)
|
|
|
|
|
|
_refresh_tree(self)
|
|
|
|
|
|
try:
|
|
|
|
|
|
self.gamma_pattern_tree.selection_set(str(new_idx))
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _generate_by_curve(self: "PQAutomationApp"):
|
|
|
|
|
|
try:
|
|
|
|
|
|
n = int(self._gamma_n_points_var.get().strip())
|
|
|
|
|
|
except ValueError:
|
|
|
|
|
|
messagebox.showerror("输入错误", "点数 N 必须为整数")
|
|
|
|
|
|
return
|
|
|
|
|
|
if n < 2 or n > 256:
|
|
|
|
|
|
messagebox.showerror("输入错误", "点数 N 必须在 2~256 之间")
|
|
|
|
|
|
return
|
|
|
|
|
|
gen_name = self._gamma_gen_type_var.get()
|
|
|
|
|
|
func = _GENERATORS.get(gen_name)
|
|
|
|
|
|
if func is None:
|
|
|
|
|
|
return
|
|
|
|
|
|
if not _check_locked_or_prompt(self):
|
|
|
|
|
|
return
|
|
|
|
|
|
if not messagebox.askyesno("确认", f"将用 [{gen_name}] N={n} 替换当前列表,继续?"):
|
|
|
|
|
|
return
|
|
|
|
|
|
self._gamma_pattern_params = func(n)
|
|
|
|
|
|
_mark_dirty(self)
|
|
|
|
|
|
_refresh_tree(self)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _paste_from_clipboard(self: "PQAutomationApp"):
|
|
|
|
|
|
try:
|
|
|
|
|
|
raw = self.gamma_pattern_frame.clipboard_get()
|
|
|
|
|
|
except tk.TclError:
|
|
|
|
|
|
messagebox.showinfo("提示", "剪贴板为空")
|
|
|
|
|
|
return
|
|
|
|
|
|
rows: list[list[int]] = []
|
|
|
|
|
|
errors: list[str] = []
|
|
|
|
|
|
for ln_no, line in enumerate(raw.splitlines(), 1):
|
|
|
|
|
|
s = line.strip()
|
|
|
|
|
|
if not s:
|
|
|
|
|
|
continue
|
|
|
|
|
|
if s.endswith("%"):
|
|
|
|
|
|
try:
|
|
|
|
|
|
pct = float(s.rstrip("%"))
|
|
|
|
|
|
v = int(round(max(0, min(100, pct)) / 100.0 * 255))
|
|
|
|
|
|
rows.append([v, v, v])
|
|
|
|
|
|
continue
|
|
|
|
|
|
except ValueError:
|
|
|
|
|
|
errors.append(f"第 {ln_no} 行无法解析:{s}")
|
|
|
|
|
|
continue
|
|
|
|
|
|
parts = [p for p in s.replace(",", " ").replace(";", " ").split() if p]
|
|
|
|
|
|
if len(parts) == 1:
|
|
|
|
|
|
try:
|
|
|
|
|
|
v = int(parts[0])
|
|
|
|
|
|
if 0 <= v <= 255:
|
|
|
|
|
|
rows.append([v, v, v])
|
|
|
|
|
|
continue
|
|
|
|
|
|
except ValueError:
|
|
|
|
|
|
pass
|
|
|
|
|
|
errors.append(f"第 {ln_no} 行无法解析:{s}")
|
|
|
|
|
|
continue
|
|
|
|
|
|
if len(parts) < 3:
|
|
|
|
|
|
errors.append(f"第 {ln_no} 行字段不足:{s}")
|
|
|
|
|
|
continue
|
|
|
|
|
|
try:
|
|
|
|
|
|
r, g, b = int(parts[0]), int(parts[1]), int(parts[2])
|
|
|
|
|
|
except ValueError:
|
|
|
|
|
|
errors.append(f"第 {ln_no} 行非整数:{s}")
|
|
|
|
|
|
continue
|
|
|
|
|
|
if not all(0 <= v <= 255 for v in (r, g, b)):
|
|
|
|
|
|
errors.append(f"第 {ln_no} 行越界:{s}")
|
|
|
|
|
|
continue
|
|
|
|
|
|
rows.append([r, g, b])
|
|
|
|
|
|
if not rows:
|
|
|
|
|
|
messagebox.showerror("粘贴失败", "未识别到任何有效行\n" + "\n".join(errors[:5]))
|
|
|
|
|
|
return
|
|
|
|
|
|
if not _check_locked_or_prompt(self):
|
|
|
|
|
|
return
|
|
|
|
|
|
msg = f"识别到 {len(rows)} 行有效数据。"
|
|
|
|
|
|
if errors:
|
|
|
|
|
|
msg += f"\n忽略 {len(errors)} 行错误(示例):\n" + "\n".join(errors[:3])
|
|
|
|
|
|
msg += "\n\n是 = 替换当前列表 / 否 = 追加到末尾 / 取消"
|
|
|
|
|
|
choice = messagebox.askyesnocancel("确认", msg)
|
|
|
|
|
|
if choice is None:
|
|
|
|
|
|
return
|
|
|
|
|
|
if choice:
|
|
|
|
|
|
self._gamma_pattern_params = rows
|
|
|
|
|
|
else:
|
|
|
|
|
|
self._gamma_pattern_params.extend(rows)
|
|
|
|
|
|
_mark_dirty(self)
|
|
|
|
|
|
_refresh_tree(self)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ============================================================
|
|
|
|
|
|
# 元信息显示 & 校验
|
|
|
|
|
|
# ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
def _update_meta_label(self: "PQAutomationApp", data: dict):
|
|
|
|
|
|
meta = data.get("_meta") or {}
|
|
|
|
|
|
parts = []
|
|
|
|
|
|
if meta.get("locked"):
|
|
|
|
|
|
parts.append("🔒 内置(只读,需另存为修改)")
|
|
|
|
|
|
if meta.get("description"):
|
|
|
|
|
|
parts.append(meta["description"])
|
|
|
|
|
|
if meta.get("generator"):
|
|
|
|
|
|
parts.append(f"生成器:{meta['generator']}")
|
|
|
|
|
|
if meta.get("created"):
|
|
|
|
|
|
parts.append(f"创建:{meta['created']}")
|
|
|
|
|
|
self._gamma_meta_label.config(text=" | ".join(parts) if parts else "")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _run_validation(self: "PQAutomationApp"):
|
|
|
|
|
|
params = self._gamma_pattern_params
|
|
|
|
|
|
msgs: list[str] = []
|
|
|
|
|
|
|
|
|
|
|
|
if len(params) < 2:
|
|
|
|
|
|
msgs.append("⚠ 至少需要 2 个灰阶点")
|
|
|
|
|
|
|
|
|
|
|
|
bad = [
|
|
|
|
|
|
i for i, rgb in enumerate(params)
|
|
|
|
|
|
if not all(0 <= int(v) <= 255 for v in rgb)
|
|
|
|
|
|
]
|
|
|
|
|
|
if bad:
|
|
|
|
|
|
msgs.append(f"⚠ 越界点 (索引): {bad}")
|
|
|
|
|
|
|
|
|
|
|
|
non_gray = [
|
|
|
|
|
|
i for i, (r, g, b) in enumerate(params)
|
|
|
|
|
|
if not (int(r) == int(g) == int(b))
|
|
|
|
|
|
]
|
|
|
|
|
|
if non_gray:
|
|
|
|
|
|
msgs.append(f"ⓘ 非纯灰点 (R≠G≠B): {non_gray}(gamma 计算按 R 通道)")
|
|
|
|
|
|
|
|
|
|
|
|
seen: dict[tuple, int] = {}
|
|
|
|
|
|
dups: list[int] = []
|
|
|
|
|
|
for i, rgb in enumerate(params):
|
|
|
|
|
|
key = tuple(rgb)
|
|
|
|
|
|
if key in seen:
|
|
|
|
|
|
dups.append(i)
|
|
|
|
|
|
else:
|
|
|
|
|
|
seen[key] = i
|
|
|
|
|
|
if dups:
|
|
|
|
|
|
msgs.append(f"⚠ 重复点 (索引): {dups}")
|
|
|
|
|
|
|
|
|
|
|
|
r_seq = [int(rgb[0]) for rgb in params]
|
|
|
|
|
|
if len(r_seq) >= 2:
|
|
|
|
|
|
is_desc = all(a >= b for a, b in zip(r_seq, r_seq[1:]))
|
|
|
|
|
|
is_asc = all(a <= b for a, b in zip(r_seq, r_seq[1:]))
|
|
|
|
|
|
if not (is_desc or is_asc):
|
|
|
|
|
|
msgs.append("ⓘ R 通道非单调递增/递减")
|
|
|
|
|
|
|
|
|
|
|
|
if not msgs:
|
|
|
|
|
|
text = f"✔ 校验通过(共 {len(params)} 点)"
|
2026-06-04 10:36:15 +08:00
|
|
|
|
style_name = "SuccessState.TLabel"
|
2026-05-27 11:26:28 +08:00
|
|
|
|
else:
|
|
|
|
|
|
text = f"共 {len(params)} 点 | " + " ".join(msgs)
|
2026-06-04 10:36:15 +08:00
|
|
|
|
style_name = "WarningState.TLabel" if any(m.startswith("⚠") for m in msgs) else "Muted.TLabel"
|
|
|
|
|
|
self._gamma_validate_label.config(text=text, style=style_name)
|
2026-05-27 11:26:28 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ============================================================
|
|
|
|
|
|
# 初始加载
|
|
|
|
|
|
# ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
def _load_initial(self: "PQAutomationApp"):
|
|
|
|
|
|
"""启动时优先加载激活预设;如无则加载当前 gray.json。"""
|
|
|
|
|
|
active = pq_config.get_active_preset_name(_TEST_KIND)
|
|
|
|
|
|
if active:
|
|
|
|
|
|
try:
|
|
|
|
|
|
data = pq_config.load_preset(_TEST_KIND, active)
|
|
|
|
|
|
self._gamma_current_preset = active
|
|
|
|
|
|
self._gamma_preset_locked = bool((data.get("_meta") or {}).get("locked"))
|
|
|
|
|
|
self._gamma_pattern_params = [
|
|
|
|
|
|
list(map(int, rgb)) for rgb in (data.get("pattern_params") or [])
|
|
|
|
|
|
]
|
|
|
|
|
|
_refresh_tree(self)
|
|
|
|
|
|
_refresh_preset_combo(self, select=active)
|
|
|
|
|
|
_update_meta_label(self, data)
|
|
|
|
|
|
_update_active_label(self)
|
|
|
|
|
|
return
|
|
|
|
|
|
except Exception:
|
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
|
|
pattern = pq_config.get_pattern("gray")
|
|
|
|
|
|
self._gamma_pattern_params = [
|
|
|
|
|
|
list(map(int, rgb)) for rgb in (pattern.get("pattern_params") or [])
|
|
|
|
|
|
]
|
|
|
|
|
|
self._gamma_current_preset = None
|
|
|
|
|
|
self._gamma_preset_locked = False
|
|
|
|
|
|
_refresh_tree(self)
|
|
|
|
|
|
_update_meta_label(self, {})
|
|
|
|
|
|
_update_active_label(self)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ============================================================
|
|
|
|
|
|
# Mixin
|
|
|
|
|
|
# ============================================================
|
|
|
|
|
|
|
|
|
|
|
|
class GammaPatternPanelMixin:
|
|
|
|
|
|
"""挂到 PQAutomationApp 上的方法集合。"""
|
|
|
|
|
|
|
|
|
|
|
|
create_gamma_pattern_panel = create_gamma_pattern_panel
|
|
|
|
|
|
toggle_gamma_pattern_panel = toggle_gamma_pattern_panel
|