继续优化AI图片列表UI显示
This commit is contained in:
@@ -5,6 +5,8 @@ from __future__ import annotations
|
|||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
|
import time
|
||||||
|
import logging
|
||||||
import tkinter as tk
|
import tkinter as tk
|
||||||
from tkinter import filedialog, messagebox, simpledialog
|
from tkinter import filedialog, messagebox, simpledialog
|
||||||
|
|
||||||
@@ -20,6 +22,9 @@ if TYPE_CHECKING:
|
|||||||
from pqAutomationApp import PQAutomationApp
|
from pqAutomationApp import PQAutomationApp
|
||||||
|
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
def _theme_colors():
|
def _theme_colors():
|
||||||
style = ttk.Style()
|
style = ttk.Style()
|
||||||
colors = style.colors
|
colors = style.colors
|
||||||
@@ -35,6 +40,118 @@ def _theme_colors():
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _apply_ai_image_list_style(self: "PQAutomationApp"):
|
||||||
|
"""刷新 AI 图片列表控件样式,尽量贴合当前主题色板。"""
|
||||||
|
palette = _theme_colors()
|
||||||
|
style = ttk.Style()
|
||||||
|
style.configure(
|
||||||
|
"AIImage.Treeview",
|
||||||
|
background=palette["input_bg"],
|
||||||
|
fieldbackground=palette["input_bg"],
|
||||||
|
foreground=palette["input_fg"],
|
||||||
|
bordercolor=palette["border"],
|
||||||
|
lightcolor=palette["border"],
|
||||||
|
darkcolor=palette["border"],
|
||||||
|
rowheight=24,
|
||||||
|
font=("微软雅黑", 9),
|
||||||
|
)
|
||||||
|
style.configure(
|
||||||
|
"AIImage.Treeview.Heading",
|
||||||
|
background=palette["bg"],
|
||||||
|
foreground=palette["muted"],
|
||||||
|
bordercolor=palette["border"],
|
||||||
|
font=("微软雅黑", 9, "bold"),
|
||||||
|
)
|
||||||
|
style.map(
|
||||||
|
"AIImage.Treeview",
|
||||||
|
background=[("selected", palette["select_bg"])],
|
||||||
|
foreground=[("selected", palette["select_fg"])],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _hide_tree_tooltip(self: "PQAutomationApp"):
|
||||||
|
tip = getattr(self, "_ai_image_tooltip", None)
|
||||||
|
if tip is None:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
tip.withdraw()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self._ai_image_tooltip_item = ""
|
||||||
|
|
||||||
|
|
||||||
|
def _show_tree_tooltip(self: "PQAutomationApp", text: str, x_root: int, y_root: int, item_id: str):
|
||||||
|
if not text:
|
||||||
|
_hide_tree_tooltip(self)
|
||||||
|
return
|
||||||
|
tip = getattr(self, "_ai_image_tooltip", None)
|
||||||
|
if tip is None:
|
||||||
|
tip = tk.Toplevel(self.root)
|
||||||
|
tip.withdraw()
|
||||||
|
tip.overrideredirect(True)
|
||||||
|
tip.transient(self.root)
|
||||||
|
label = tk.Label(
|
||||||
|
tip,
|
||||||
|
text="",
|
||||||
|
justify=tk.LEFT,
|
||||||
|
anchor=tk.W,
|
||||||
|
bg="#ffffff",
|
||||||
|
fg="#1f2937",
|
||||||
|
relief=tk.SOLID,
|
||||||
|
bd=1,
|
||||||
|
padx=8,
|
||||||
|
pady=6,
|
||||||
|
font=("微软雅黑", 9),
|
||||||
|
wraplength=520,
|
||||||
|
)
|
||||||
|
label.pack(fill=tk.BOTH, expand=True)
|
||||||
|
self._ai_image_tooltip = tip
|
||||||
|
self._ai_image_tooltip_label = label
|
||||||
|
else:
|
||||||
|
label = getattr(self, "_ai_image_tooltip_label", None)
|
||||||
|
if label is None:
|
||||||
|
return
|
||||||
|
|
||||||
|
self._ai_image_tooltip_item = item_id
|
||||||
|
label.configure(text=text)
|
||||||
|
tip.geometry(f"+{x_root + 14}+{y_root + 18}")
|
||||||
|
tip.deiconify()
|
||||||
|
tip.lift()
|
||||||
|
|
||||||
|
|
||||||
|
def _on_tree_motion(self: "PQAutomationApp", event):
|
||||||
|
tree = self.ai_image_tree
|
||||||
|
item_id = tree.identify_row(event.y)
|
||||||
|
col = tree.identify_column(event.x)
|
||||||
|
region = tree.identify("region", event.x, event.y)
|
||||||
|
if not item_id or col != "#0" or region not in {"tree", "cell"}:
|
||||||
|
_hide_tree_tooltip(self)
|
||||||
|
return
|
||||||
|
|
||||||
|
node_map = getattr(self, "_ai_image_node_map", None) or {}
|
||||||
|
ridx = node_map.get(item_id)
|
||||||
|
if ridx is None or ridx < 0 or ridx >= len(self.ai_image_records):
|
||||||
|
_hide_tree_tooltip(self)
|
||||||
|
return
|
||||||
|
|
||||||
|
rec = self.ai_image_records[ridx]
|
||||||
|
full_text = (rec.display_name or "").strip()
|
||||||
|
if not full_text:
|
||||||
|
_hide_tree_tooltip(self)
|
||||||
|
return
|
||||||
|
|
||||||
|
# 悬浮到任意图片标题项时都显示完整描述,避免因截断判定误差导致无响应。
|
||||||
|
if getattr(self, "_ai_image_tooltip_item", "") == item_id:
|
||||||
|
tip = getattr(self, "_ai_image_tooltip", None)
|
||||||
|
if tip is not None:
|
||||||
|
try:
|
||||||
|
tip.geometry(f"+{event.x_root + 14}+{event.y_root + 18}")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return
|
||||||
|
_show_tree_tooltip(self, full_text, event.x_root, event.y_root, item_id)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -56,6 +173,17 @@ def create_ai_image_panel(self: "PQAutomationApp"):
|
|||||||
self._ai_image_cancel_event = None
|
self._ai_image_cancel_event = None
|
||||||
self._ai_image_request_seq = 0
|
self._ai_image_request_seq = 0
|
||||||
self._ai_image_active_seq = 0
|
self._ai_image_active_seq = 0
|
||||||
|
self._ai_image_node_map = {}
|
||||||
|
self._ai_image_session_node_map = {}
|
||||||
|
self._ai_image_all_records = []
|
||||||
|
self._ai_image_search_var = tk.StringVar(value="")
|
||||||
|
self._ai_image_count_var = tk.StringVar(value="0 张图片")
|
||||||
|
self._ai_image_reloading = False
|
||||||
|
self._ai_image_select_guard = False
|
||||||
|
self._ai_image_list_loaded = False
|
||||||
|
self._ai_image_tooltip = None
|
||||||
|
self._ai_image_tooltip_label = None
|
||||||
|
self._ai_image_tooltip_item = ""
|
||||||
|
|
||||||
container = ttk.Frame(frame, padding=10)
|
container = ttk.Frame(frame, padding=10)
|
||||||
container.pack(fill=tk.BOTH, expand=True)
|
container.pack(fill=tk.BOTH, expand=True)
|
||||||
@@ -71,35 +199,65 @@ def create_ai_image_panel(self: "PQAutomationApp"):
|
|||||||
left.grid(row=0, column=0, sticky=tk.NS, padx=(0, 10))
|
left.grid(row=0, column=0, sticky=tk.NS, padx=(0, 10))
|
||||||
left.grid_propagate(False)
|
left.grid_propagate(False)
|
||||||
|
|
||||||
ttk.Label(left, text="历史图片", font=("微软雅黑", 10, "bold")).pack(
|
title_row = ttk.Frame(left)
|
||||||
anchor=tk.W, pady=(0, 4)
|
title_row.pack(fill=tk.X, pady=(0, 4))
|
||||||
|
ttk.Label(title_row, text="历史图片", font=("微软雅黑", 10, "bold")).pack(side=tk.LEFT)
|
||||||
|
ttk.Label(
|
||||||
|
title_row,
|
||||||
|
textvariable=self._ai_image_count_var,
|
||||||
|
foreground=palette["muted"],
|
||||||
|
font=("微软雅黑", 9),
|
||||||
|
).pack(side=tk.RIGHT)
|
||||||
|
|
||||||
|
search_row = ttk.Frame(left)
|
||||||
|
search_row.pack(fill=tk.X, pady=(0, 6))
|
||||||
|
ttk.Label(
|
||||||
|
search_row,
|
||||||
|
text="搜索",
|
||||||
|
foreground=palette["muted"],
|
||||||
|
font=("微软雅黑", 9),
|
||||||
|
).pack(side=tk.LEFT, padx=(0, 6))
|
||||||
|
self.ai_image_search_entry = ttk.Entry(
|
||||||
|
search_row,
|
||||||
|
textvariable=self._ai_image_search_var,
|
||||||
|
bootstyle="secondary",
|
||||||
)
|
)
|
||||||
|
self.ai_image_search_entry.pack(side=tk.LEFT, fill=tk.X, expand=True)
|
||||||
|
self.ai_image_search_entry.bind("<KeyRelease>", lambda e: reload_ai_image_list(self, auto_select_first=False))
|
||||||
|
ttk.Button(
|
||||||
|
search_row,
|
||||||
|
text="清空",
|
||||||
|
width=6,
|
||||||
|
bootstyle="secondary-outline",
|
||||||
|
command=lambda: _clear_list_search(self),
|
||||||
|
).pack(side=tk.LEFT, padx=(6, 0))
|
||||||
|
|
||||||
list_wrap = ttk.Frame(left, padding=2)
|
list_wrap = ttk.Frame(left, padding=2)
|
||||||
list_wrap.pack(fill=tk.BOTH, expand=True)
|
list_wrap.pack(fill=tk.BOTH, expand=True)
|
||||||
|
list_wrap.columnconfigure(0, weight=1)
|
||||||
|
list_wrap.rowconfigure(0, weight=1)
|
||||||
|
|
||||||
|
_apply_ai_image_list_style(self)
|
||||||
scroll = ttk.Scrollbar(list_wrap, orient=tk.VERTICAL)
|
scroll = ttk.Scrollbar(list_wrap, orient=tk.VERTICAL)
|
||||||
self.ai_image_listbox = tk.Listbox(
|
self.ai_image_tree = ttk.Treeview(
|
||||||
list_wrap,
|
list_wrap,
|
||||||
width=34,
|
columns=("time",),
|
||||||
height=1, # 由 pack fill/expand 撑满,height 仅为最小保底
|
show="tree headings",
|
||||||
activestyle="none",
|
selectmode="browse",
|
||||||
font=("微软雅黑", 9),
|
style="AIImage.Treeview",
|
||||||
bd=1,
|
|
||||||
relief=tk.FLAT,
|
|
||||||
highlightthickness=1,
|
|
||||||
bg=palette["input_bg"],
|
|
||||||
fg=palette["input_fg"],
|
|
||||||
highlightbackground=palette["border"],
|
|
||||||
highlightcolor=palette["select_bg"],
|
|
||||||
selectbackground=palette["select_bg"],
|
|
||||||
selectforeground=palette["select_fg"],
|
|
||||||
yscrollcommand=scroll.set,
|
yscrollcommand=scroll.set,
|
||||||
)
|
)
|
||||||
scroll.config(command=self.ai_image_listbox.yview)
|
self.ai_image_tree.heading("#0", text="标题")
|
||||||
self.ai_image_listbox.pack(side=tk.LEFT, fill=tk.Y)
|
self.ai_image_tree.heading("time", text="时间")
|
||||||
scroll.pack(side=tk.RIGHT, fill=tk.Y)
|
self.ai_image_tree.column("#0", width=210, minwidth=140, anchor=tk.W)
|
||||||
self.ai_image_listbox.bind("<<ListboxSelect>>", lambda e: _on_list_select(self))
|
self.ai_image_tree.column("time", width=105, minwidth=90, stretch=False, anchor=tk.W)
|
||||||
|
scroll.config(command=self.ai_image_tree.yview)
|
||||||
|
self.ai_image_tree.grid(row=0, column=0, sticky=tk.NSEW)
|
||||||
|
scroll.grid(row=0, column=1, sticky=tk.NS)
|
||||||
|
self.ai_image_tree.bind("<<TreeviewSelect>>", lambda e: _on_list_select(self))
|
||||||
|
self.ai_image_tree.bind("<Double-1>", lambda e: _on_tree_double_click(self, e))
|
||||||
|
self.ai_image_tree.bind("<Motion>", lambda e: _on_tree_motion(self, e))
|
||||||
|
self.ai_image_tree.bind("<Leave>", lambda e: _hide_tree_tooltip(self))
|
||||||
# 右键菜单:发送到 UCD / 重命名 / 另存为 / 删除
|
# 右键菜单:发送到 UCD / 重命名 / 另存为 / 删除
|
||||||
# 索引: 0=发送, 1=sep, 2=重命名, 3=另存为, 4=删除
|
# 索引: 0=发送, 1=sep, 2=重命名, 3=另存为, 4=删除
|
||||||
self.ai_image_menu = tk.Menu(self.root, tearoff=0)
|
self.ai_image_menu = tk.Menu(self.root, tearoff=0)
|
||||||
@@ -120,7 +278,7 @@ def create_ai_image_panel(self: "PQAutomationApp"):
|
|||||||
label="删除",
|
label="删除",
|
||||||
command=lambda: _delete_current(self),
|
command=lambda: _delete_current(self),
|
||||||
)
|
)
|
||||||
self.ai_image_listbox.bind(
|
self.ai_image_tree.bind(
|
||||||
"<Button-3>",
|
"<Button-3>",
|
||||||
lambda e: _show_list_context_menu(self, e),
|
lambda e: _show_list_context_menu(self, e),
|
||||||
)
|
)
|
||||||
@@ -216,14 +374,17 @@ def create_ai_image_panel(self: "PQAutomationApp"):
|
|||||||
# 注册面板
|
# 注册面板
|
||||||
self.register_panel("ai_image", frame, None, "ai_image_visible")
|
self.register_panel("ai_image", frame, None, "ai_image_visible")
|
||||||
self.ai_image_visible = False
|
self.ai_image_visible = False
|
||||||
|
self.ai_image_meta_var.set("首次打开 AI 图片面板时加载缓存")
|
||||||
# 初次加载缓存
|
|
||||||
reload_ai_image_list(self)
|
|
||||||
|
|
||||||
|
|
||||||
def toggle_ai_image_panel(self: "PQAutomationApp"):
|
def toggle_ai_image_panel(self: "PQAutomationApp"):
|
||||||
"""切换 AI 图片面板显隐。"""
|
"""切换 AI 图片面板显隐。"""
|
||||||
self.show_panel("ai_image")
|
self.show_panel("ai_image")
|
||||||
|
_apply_ai_image_list_style(self)
|
||||||
|
if not getattr(self, "_ai_image_list_loaded", False):
|
||||||
|
logger.info("[AIImagePanel] 首次显示面板,开始加载列表")
|
||||||
|
reload_ai_image_list(self)
|
||||||
|
self._ai_image_list_loaded = True
|
||||||
|
|
||||||
|
|
||||||
def _get_app_base_dir(self: "PQAutomationApp") -> str:
|
def _get_app_base_dir(self: "PQAutomationApp") -> str:
|
||||||
@@ -245,58 +406,102 @@ def reload_ai_image_list(self: "PQAutomationApp", auto_select_first=True):
|
|||||||
其下列出该轮生成的所有图片。会话按"最近使用"倒序,组内按时间倒序。
|
其下列出该轮生成的所有图片。会话按"最近使用"倒序,组内按时间倒序。
|
||||||
auto_select_first: 是否自动选中第一张图片(默认 True)。
|
auto_select_first: 是否自动选中第一张图片(默认 True)。
|
||||||
"""
|
"""
|
||||||
palette = _theme_colors()
|
t0 = time.monotonic()
|
||||||
self.ai_image_records = _svc.list_records(base_dir=_get_app_base_dir(self))
|
if getattr(self, "_ai_image_reloading", False):
|
||||||
self.ai_image_listbox.delete(0, tk.END)
|
logger.debug("[AIImagePanel] 忽略重入 reload 请求")
|
||||||
# 维护行号 → 记录索引的映射;分隔头处为 None
|
return
|
||||||
self._ai_image_row_map = []
|
self._ai_image_reloading = True
|
||||||
self._ai_image_row_session_map = []
|
try:
|
||||||
sessions = _svc.group_records_by_session(self.ai_image_records)
|
_apply_ai_image_list_style(self)
|
||||||
flat = []
|
all_records = _svc.list_records(base_dir=_get_app_base_dir(self))
|
||||||
current_sid = _svc.get_session_id()
|
self._ai_image_all_records = list(all_records)
|
||||||
for idx, sess in enumerate(sessions, start=1):
|
self.ai_image_tree.delete(*self.ai_image_tree.get_children())
|
||||||
sid = sess["session_id"]
|
self._ai_image_node_map = {}
|
||||||
is_current = sid and sid == current_sid
|
self._ai_image_session_node_map = {}
|
||||||
header = _format_session_header(idx, sess, is_current=is_current)
|
|
||||||
self.ai_image_listbox.insert(tk.END, header)
|
keyword = (self._ai_image_search_var.get() or "").strip().lower()
|
||||||
# 头部行:禁用选中(视觉上变灰)
|
records = []
|
||||||
last = self.ai_image_listbox.size() - 1
|
for rec in all_records:
|
||||||
self.ai_image_listbox.itemconfig(
|
if not keyword:
|
||||||
last, foreground=palette["muted"], selectforeground=palette["muted"],
|
records.append(rec)
|
||||||
background=palette["bg"], selectbackground=palette["bg"],
|
continue
|
||||||
|
hay = "\n".join([
|
||||||
|
rec.display_name or "",
|
||||||
|
rec.prompt or "",
|
||||||
|
os.path.basename(rec.image_path),
|
||||||
|
]).lower()
|
||||||
|
if keyword in hay:
|
||||||
|
records.append(rec)
|
||||||
|
|
||||||
|
self.ai_image_records = records
|
||||||
|
sessions = _svc.group_records_by_session(self.ai_image_records)
|
||||||
|
flat = []
|
||||||
|
current_sid = _svc.get_session_id()
|
||||||
|
for idx, sess in enumerate(sessions, start=1):
|
||||||
|
sid = sess["session_id"]
|
||||||
|
is_current = sid and sid == current_sid
|
||||||
|
header = _format_session_header(idx, sess, is_current=is_current)
|
||||||
|
parent_id = self.ai_image_tree.insert(
|
||||||
|
"",
|
||||||
|
tk.END,
|
||||||
|
text=header,
|
||||||
|
values=("",),
|
||||||
|
open=True,
|
||||||
|
)
|
||||||
|
self._ai_image_session_node_map[parent_id] = sid
|
||||||
|
for rec in sess["records"]:
|
||||||
|
label = _format_list_label(rec)
|
||||||
|
created = (rec.created_at or "").replace("T", " ")[:16]
|
||||||
|
node_id = self.ai_image_tree.insert(
|
||||||
|
parent_id,
|
||||||
|
tk.END,
|
||||||
|
text=label,
|
||||||
|
values=(created,),
|
||||||
|
)
|
||||||
|
self._ai_image_node_map[node_id] = len(flat)
|
||||||
|
flat.append(rec)
|
||||||
|
# 替换为按显示顺序展平后的列表,便于其它逻辑(前一张/后一张等)
|
||||||
|
self.ai_image_records = flat
|
||||||
|
total_count = len(all_records)
|
||||||
|
visible_count = len(self.ai_image_records)
|
||||||
|
if keyword:
|
||||||
|
self._ai_image_count_var.set(f"{visible_count}/{total_count} 张")
|
||||||
|
else:
|
||||||
|
self._ai_image_count_var.set(f"{visible_count} 张图片")
|
||||||
|
if self.ai_image_records and auto_select_first:
|
||||||
|
first_id = ""
|
||||||
|
for parent in self.ai_image_tree.get_children(""):
|
||||||
|
children = self.ai_image_tree.get_children(parent)
|
||||||
|
if children:
|
||||||
|
first_id = children[0]
|
||||||
|
break
|
||||||
|
if first_id:
|
||||||
|
_set_tree_selection(self, first_id)
|
||||||
|
else:
|
||||||
|
self.ai_image_current = None
|
||||||
|
self.ai_image_photo = None
|
||||||
|
self.ai_image_canvas.delete("all")
|
||||||
|
self.ai_image_meta_var.set("暂无缓存图片" if not keyword else "当前筛选无结果")
|
||||||
|
logger.info(
|
||||||
|
"[AIImagePanel] reload 完成 total=%d visible=%d sessions=%d keyword=%r elapsed=%.3fs",
|
||||||
|
total_count,
|
||||||
|
visible_count,
|
||||||
|
len(sessions),
|
||||||
|
keyword,
|
||||||
|
time.monotonic() - t0,
|
||||||
)
|
)
|
||||||
self._ai_image_row_map.append(None)
|
self._ai_image_list_loaded = True
|
||||||
self._ai_image_row_session_map.append(sid)
|
finally:
|
||||||
for rec in sess["records"]:
|
self._ai_image_reloading = False
|
||||||
label = " " + _format_list_label(rec)
|
|
||||||
self.ai_image_listbox.insert(tk.END, label)
|
|
||||||
self._ai_image_row_map.append(len(flat))
|
|
||||||
self._ai_image_row_session_map.append(rec.session_id)
|
|
||||||
flat.append(rec)
|
|
||||||
# 替换为按显示顺序展平后的列表,便于其它逻辑(前一张/后一张等)
|
|
||||||
self.ai_image_records = flat
|
|
||||||
if self.ai_image_records and auto_select_first:
|
|
||||||
# 选中第一张实际记录
|
|
||||||
for row, ridx in enumerate(self._ai_image_row_map):
|
|
||||||
if ridx is not None:
|
|
||||||
self.ai_image_listbox.selection_clear(0, tk.END)
|
|
||||||
self.ai_image_listbox.selection_set(row)
|
|
||||||
self.ai_image_listbox.activate(row)
|
|
||||||
_select_record(self, self.ai_image_records[ridx])
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
self.ai_image_current = None
|
|
||||||
self.ai_image_photo = None
|
|
||||||
self.ai_image_canvas.delete("all")
|
|
||||||
self.ai_image_meta_var.set("暂无缓存图片")
|
|
||||||
|
|
||||||
|
|
||||||
def _format_session_header(index: int, sess: dict, is_current: bool) -> str:
|
def _format_session_header(index: int, sess: dict, is_current: bool) -> str:
|
||||||
started = (sess.get("started_at") or "").replace("T", " ")[:16]
|
started = (sess.get("started_at") or "").replace("T", " ")[:16]
|
||||||
tag = "(当前)" if is_current else ""
|
tag = "(当前)" if is_current else ""
|
||||||
|
count = len(sess.get("records") or [])
|
||||||
if sess.get("session_id"):
|
if sess.get("session_id"):
|
||||||
return f"── 会话 #{index} · {started} {tag}──"
|
return f"会话 #{index} · {started} · {count} 张 {tag}".strip()
|
||||||
return f"── 未归类 · {started} ──"
|
return f"未归类 · {started} · {count} 张"
|
||||||
|
|
||||||
|
|
||||||
def _format_list_label(rec: _svc.AIImageRecord) -> str:
|
def _format_list_label(rec: _svc.AIImageRecord) -> str:
|
||||||
@@ -305,42 +510,105 @@ def _format_list_label(rec: _svc.AIImageRecord) -> str:
|
|||||||
extra = rec.extra or {}
|
extra = rec.extra or {}
|
||||||
if isinstance(extra, dict) and extra.get("size"):
|
if isinstance(extra, dict) and extra.get("size"):
|
||||||
size_tag = f"[{extra['size']}] "
|
size_tag = f"[{extra['size']}] "
|
||||||
else:
|
|
||||||
try:
|
|
||||||
from PIL import Image as _Im
|
|
||||||
with _Im.open(rec.image_path) as _im:
|
|
||||||
size_tag = f"[{_im.width}×{_im.height}] "
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
name_line = rec.display_name.splitlines()[0] if rec.display_name else "(未命名)"
|
name_line = rec.display_name.splitlines()[0] if rec.display_name else "(未命名)"
|
||||||
# 列表宽度 width=34,需要扣除两格缩进 + size_tag
|
# Treeview 标题列可变宽,给展示名预留较长空间
|
||||||
max_name = 34 - 2 - len(size_tag) - 2
|
max_name = 40 - len(size_tag)
|
||||||
if max_name > 4 and len(name_line) > max_name:
|
if max_name > 4 and len(name_line) > max_name:
|
||||||
name_line = name_line[:max_name] + "…"
|
name_line = name_line[:max_name] + "…"
|
||||||
return f"{size_tag}{name_line}"
|
return f"{size_tag}{name_line}"
|
||||||
|
|
||||||
|
|
||||||
|
def _clear_list_search(self: "PQAutomationApp"):
|
||||||
|
if (self._ai_image_search_var.get() or "").strip() == "":
|
||||||
|
return
|
||||||
|
self._ai_image_search_var.set("")
|
||||||
|
reload_ai_image_list(self, auto_select_first=False)
|
||||||
|
_hide_tree_tooltip(self)
|
||||||
|
|
||||||
|
|
||||||
|
def _set_tree_selection(self: "PQAutomationApp", item_id: str):
|
||||||
|
if not item_id:
|
||||||
|
return
|
||||||
|
_hide_tree_tooltip(self)
|
||||||
|
try:
|
||||||
|
current = self.ai_image_tree.selection()
|
||||||
|
if current and current[0] == item_id:
|
||||||
|
node_map = getattr(self, "_ai_image_node_map", None) or {}
|
||||||
|
ridx = node_map.get(item_id)
|
||||||
|
if ridx is not None and 0 <= ridx < len(self.ai_image_records):
|
||||||
|
_select_record(self, self.ai_image_records[ridx])
|
||||||
|
return
|
||||||
|
self.ai_image_tree.selection_set(item_id)
|
||||||
|
self.ai_image_tree.focus(item_id)
|
||||||
|
self.ai_image_tree.see(item_id)
|
||||||
|
except Exception:
|
||||||
|
return
|
||||||
|
node_map = getattr(self, "_ai_image_node_map", None) or {}
|
||||||
|
ridx = node_map.get(item_id)
|
||||||
|
if ridx is not None and 0 <= ridx < len(self.ai_image_records):
|
||||||
|
_select_record(self, self.ai_image_records[ridx])
|
||||||
|
|
||||||
|
|
||||||
|
def _find_tree_item_by_record_id(self: "PQAutomationApp", record_id: str) -> str:
|
||||||
|
if not record_id:
|
||||||
|
return ""
|
||||||
|
node_map = getattr(self, "_ai_image_node_map", None) or {}
|
||||||
|
for item_id, ridx in node_map.items():
|
||||||
|
if ridx is None or ridx >= len(self.ai_image_records):
|
||||||
|
continue
|
||||||
|
rec = self.ai_image_records[ridx]
|
||||||
|
if rec.id == record_id:
|
||||||
|
return item_id
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
def _on_list_select(self: "PQAutomationApp"):
|
def _on_list_select(self: "PQAutomationApp"):
|
||||||
sel = self.ai_image_listbox.curselection()
|
if getattr(self, "_ai_image_reloading", False):
|
||||||
|
return
|
||||||
|
if getattr(self, "_ai_image_select_guard", False):
|
||||||
|
logger.debug("[AIImagePanel] 忽略重入选择事件")
|
||||||
|
return
|
||||||
|
sel = self.ai_image_tree.selection()
|
||||||
if not sel:
|
if not sel:
|
||||||
return
|
return
|
||||||
row = sel[0]
|
self._ai_image_select_guard = True
|
||||||
row_map = getattr(self, "_ai_image_row_map", None) or []
|
try:
|
||||||
if row >= len(row_map):
|
item_id = sel[0]
|
||||||
|
node_map = getattr(self, "_ai_image_node_map", None) or {}
|
||||||
|
ridx = node_map.get(item_id)
|
||||||
|
if ridx is None:
|
||||||
|
session_id = _session_id_for_item(self, item_id)
|
||||||
|
if session_id:
|
||||||
|
logger.info("[AIImagePanel] 选中会话头 sid=%s", session_id[:8])
|
||||||
|
_switch_to_session(self, session_id, show_message=False, refresh_list=False)
|
||||||
|
return
|
||||||
|
if 0 <= ridx < len(self.ai_image_records):
|
||||||
|
rec = self.ai_image_records[ridx]
|
||||||
|
if rec.session_id:
|
||||||
|
_switch_to_session(
|
||||||
|
self,
|
||||||
|
rec.session_id,
|
||||||
|
show_message=False,
|
||||||
|
refresh_list=False,
|
||||||
|
)
|
||||||
|
_select_record(self, rec)
|
||||||
|
logger.debug("[AIImagePanel] 选中图片 id=%s sid=%s", rec.id, (rec.session_id or "")[:8])
|
||||||
|
finally:
|
||||||
|
self._ai_image_select_guard = False
|
||||||
|
|
||||||
|
|
||||||
|
def _on_tree_double_click(self: "PQAutomationApp", event):
|
||||||
|
"""双击会话头时只切换会话并展开;双击记录保持默认行为。"""
|
||||||
|
item_id = self.ai_image_tree.identify_row(event.y)
|
||||||
|
if not item_id:
|
||||||
return
|
return
|
||||||
ridx = row_map[row]
|
node_map = getattr(self, "_ai_image_node_map", None) or {}
|
||||||
if ridx is None:
|
if item_id in node_map:
|
||||||
session_id = _session_id_for_row(self, row)
|
|
||||||
if session_id:
|
|
||||||
_switch_to_session(self, session_id, show_message=False)
|
|
||||||
self.ai_image_listbox.selection_clear(row)
|
|
||||||
return
|
return
|
||||||
if 0 <= ridx < len(self.ai_image_records):
|
sid = _session_id_for_item(self, item_id)
|
||||||
rec = self.ai_image_records[ridx]
|
if sid:
|
||||||
if rec.session_id:
|
_switch_to_session(self, sid, show_message=False, refresh_list=False)
|
||||||
_switch_to_session(self, rec.session_id, show_message=False, target_record_id=rec.id)
|
|
||||||
_select_record(self, rec)
|
|
||||||
|
|
||||||
|
|
||||||
def _select_record(self: "PQAutomationApp", rec: _svc.AIImageRecord):
|
def _select_record(self: "PQAutomationApp", rec: _svc.AIImageRecord):
|
||||||
@@ -387,36 +655,54 @@ def _start_new_session(self: "PQAutomationApp"):
|
|||||||
return
|
return
|
||||||
_svc.reset_session()
|
_svc.reset_session()
|
||||||
self.ai_image_status_var.set("已开启新对话")
|
self.ai_image_status_var.set("已开启新对话")
|
||||||
|
# 新对话创建后不要自动选中历史图片,否则会立即把 session 切回旧会话。
|
||||||
reload_ai_image_list(self, auto_select_first=False)
|
reload_ai_image_list(self, auto_select_first=False)
|
||||||
|
try:
|
||||||
|
self.ai_image_tree.selection_remove(self.ai_image_tree.selection())
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self.ai_image_current = None
|
||||||
|
self.ai_image_photo = None
|
||||||
|
self.ai_image_canvas.delete("all")
|
||||||
|
self.ai_image_meta_var.set("新对话已开启,等待生成图片")
|
||||||
|
|
||||||
|
|
||||||
def _session_id_for_row(self: "PQAutomationApp", row: int) -> str:
|
def _session_id_for_item(self: "PQAutomationApp", item_id: str) -> str:
|
||||||
session_map = getattr(self, "_ai_image_row_session_map", None) or []
|
session_map = getattr(self, "_ai_image_session_node_map", None) or {}
|
||||||
if row < 0 or row >= len(session_map):
|
parent = item_id
|
||||||
return ""
|
if parent not in session_map:
|
||||||
return session_map[row] or ""
|
try:
|
||||||
|
parent = self.ai_image_tree.parent(item_id)
|
||||||
|
except Exception:
|
||||||
|
parent = ""
|
||||||
|
return (session_map.get(parent) or "") if parent else ""
|
||||||
|
|
||||||
|
|
||||||
def _switch_to_session(self: "PQAutomationApp", session_id: str, show_message: bool = True, target_record_id: str = ""):
|
def _switch_to_session(
|
||||||
|
self: "PQAutomationApp",
|
||||||
|
session_id: str,
|
||||||
|
show_message: bool = True,
|
||||||
|
target_record_id: str = "",
|
||||||
|
refresh_list: bool = True,
|
||||||
|
):
|
||||||
sid = (session_id or "").strip()
|
sid = (session_id or "").strip()
|
||||||
if not sid:
|
if not sid:
|
||||||
return
|
return
|
||||||
if sid == _svc.get_session_id():
|
if sid == _svc.get_session_id():
|
||||||
return
|
return
|
||||||
_svc.set_session_id(sid)
|
_svc.set_session_id(sid)
|
||||||
reload_ai_image_list(self)
|
logger.info(
|
||||||
|
"[AIImagePanel] 切换会话 sid=%s refresh=%s target=%s",
|
||||||
|
sid[:8],
|
||||||
|
refresh_list,
|
||||||
|
target_record_id[:8] if target_record_id else "",
|
||||||
|
)
|
||||||
|
if refresh_list:
|
||||||
|
reload_ai_image_list(self)
|
||||||
if target_record_id:
|
if target_record_id:
|
||||||
for row, ridx in enumerate(getattr(self, "_ai_image_row_map", []) or []):
|
item_id = _find_tree_item_by_record_id(self, target_record_id)
|
||||||
if ridx is None:
|
if item_id:
|
||||||
continue
|
_set_tree_selection(self, item_id)
|
||||||
rec = self.ai_image_records[ridx]
|
|
||||||
if rec.id == target_record_id:
|
|
||||||
self.ai_image_listbox.selection_clear(0, tk.END)
|
|
||||||
self.ai_image_listbox.selection_set(row)
|
|
||||||
self.ai_image_listbox.activate(row)
|
|
||||||
self.ai_image_listbox.see(row)
|
|
||||||
_select_record(self, rec)
|
|
||||||
break
|
|
||||||
self.ai_image_status_var.set("已切换到历史对话")
|
self.ai_image_status_var.set("已切换到历史对话")
|
||||||
if show_message:
|
if show_message:
|
||||||
messagebox.showinfo("提示", "已切换到所选历史对话")
|
messagebox.showinfo("提示", "已切换到所选历史对话")
|
||||||
@@ -512,18 +798,12 @@ def _on_request_done(self: "PQAutomationApp", record, exc, req_seq):
|
|||||||
self.ai_image_status_var.set("完成")
|
self.ai_image_status_var.set("完成")
|
||||||
self.ai_image_input.delete("1.0", tk.END)
|
self.ai_image_input.delete("1.0", tk.END)
|
||||||
reload_ai_image_list(self)
|
reload_ai_image_list(self)
|
||||||
|
if record is not None:
|
||||||
|
logger.info("[AIImagePanel] 生成完成并入列 id=%s sid=%s", record.id, (record.session_id or "")[:8])
|
||||||
if record is not None and self.ai_image_records:
|
if record is not None and self.ai_image_records:
|
||||||
for row, ridx in enumerate(getattr(self, "_ai_image_row_map", []) or []):
|
item_id = _find_tree_item_by_record_id(self, record.id)
|
||||||
if ridx is None:
|
if item_id:
|
||||||
continue
|
_set_tree_selection(self, item_id)
|
||||||
r = self.ai_image_records[ridx]
|
|
||||||
if r.id == record.id:
|
|
||||||
self.ai_image_listbox.selection_clear(0, tk.END)
|
|
||||||
self.ai_image_listbox.selection_set(row)
|
|
||||||
self.ai_image_listbox.activate(row)
|
|
||||||
self.ai_image_listbox.see(row)
|
|
||||||
_select_record(self, r)
|
|
||||||
break
|
|
||||||
|
|
||||||
|
|
||||||
def _stop_request(self: "PQAutomationApp"):
|
def _stop_request(self: "PQAutomationApp"):
|
||||||
@@ -596,18 +876,9 @@ def _rename_current(self: "PQAutomationApp"):
|
|||||||
|
|
||||||
target_id = rec.id
|
target_id = rec.id
|
||||||
reload_ai_image_list(self)
|
reload_ai_image_list(self)
|
||||||
# 重新定位
|
item_id = _find_tree_item_by_record_id(self, target_id)
|
||||||
for row, ridx in enumerate(getattr(self, "_ai_image_row_map", []) or []):
|
if item_id:
|
||||||
if ridx is None:
|
_set_tree_selection(self, item_id)
|
||||||
continue
|
|
||||||
r = self.ai_image_records[ridx]
|
|
||||||
if r.id == target_id:
|
|
||||||
self.ai_image_listbox.selection_clear(0, tk.END)
|
|
||||||
self.ai_image_listbox.selection_set(row)
|
|
||||||
self.ai_image_listbox.activate(row)
|
|
||||||
self.ai_image_listbox.see(row)
|
|
||||||
_select_record(self, r)
|
|
||||||
break
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------- 发送到 UCD ----------------
|
# ---------------- 发送到 UCD ----------------
|
||||||
@@ -615,17 +886,21 @@ def _rename_current(self: "PQAutomationApp"):
|
|||||||
|
|
||||||
def _show_list_context_menu(self: "PQAutomationApp", event):
|
def _show_list_context_menu(self: "PQAutomationApp", event):
|
||||||
"""在图片列表上显示右键菜单,并根据状态启用/禁用项。"""
|
"""在图片列表上显示右键菜单,并根据状态启用/禁用项。"""
|
||||||
|
_hide_tree_tooltip(self)
|
||||||
try:
|
try:
|
||||||
row = self.ai_image_listbox.nearest(event.y)
|
item_id = self.ai_image_tree.identify_row(event.y)
|
||||||
except Exception:
|
except Exception:
|
||||||
row = -1
|
item_id = ""
|
||||||
row_map = getattr(self, "_ai_image_row_map", None) or []
|
|
||||||
ridx = row_map[row] if 0 <= row < len(row_map) else None
|
node_map = getattr(self, "_ai_image_node_map", None) or {}
|
||||||
|
ridx = node_map.get(item_id)
|
||||||
if ridx is not None and 0 <= ridx < len(self.ai_image_records):
|
if ridx is not None and 0 <= ridx < len(self.ai_image_records):
|
||||||
self.ai_image_listbox.selection_clear(0, tk.END)
|
_set_tree_selection(self, item_id)
|
||||||
self.ai_image_listbox.selection_set(row)
|
|
||||||
self.ai_image_listbox.activate(row)
|
|
||||||
_select_record(self, self.ai_image_records[ridx])
|
_select_record(self, self.ai_image_records[ridx])
|
||||||
|
elif item_id:
|
||||||
|
sid = _session_id_for_item(self, item_id)
|
||||||
|
if sid:
|
||||||
|
_switch_to_session(self, sid, show_message=False, refresh_list=False)
|
||||||
|
|
||||||
has_selection = self.ai_image_current is not None
|
has_selection = self.ai_image_current is not None
|
||||||
ucd = getattr(self, "ucd", None)
|
ucd = getattr(self, "ucd", None)
|
||||||
@@ -777,7 +1052,7 @@ class AIImagePanelMixin:
|
|||||||
_select_record = _select_record
|
_select_record = _select_record
|
||||||
_redraw_preview = _redraw_preview
|
_redraw_preview = _redraw_preview
|
||||||
_start_new_session = _start_new_session
|
_start_new_session = _start_new_session
|
||||||
_session_id_for_row = _session_id_for_row
|
_session_id_for_item = _session_id_for_item
|
||||||
_switch_to_session = _switch_to_session
|
_switch_to_session = _switch_to_session
|
||||||
_update_request_progress = _update_request_progress
|
_update_request_progress = _update_request_progress
|
||||||
_send_prompt = _send_prompt
|
_send_prompt = _send_prompt
|
||||||
|
|||||||
Reference in New Issue
Block a user