继续优化AI图片列表UI显示
This commit is contained in:
@@ -5,6 +5,8 @@ from __future__ import annotations
|
||||
import os
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
import logging
|
||||
import tkinter as tk
|
||||
from tkinter import filedialog, messagebox, simpledialog
|
||||
|
||||
@@ -20,6 +22,9 @@ if TYPE_CHECKING:
|
||||
from pqAutomationApp import PQAutomationApp
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _theme_colors():
|
||||
style = ttk.Style()
|
||||
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_request_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.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_propagate(False)
|
||||
|
||||
ttk.Label(left, text="历史图片", font=("微软雅黑", 10, "bold")).pack(
|
||||
anchor=tk.W, pady=(0, 4)
|
||||
title_row = ttk.Frame(left)
|
||||
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.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)
|
||||
self.ai_image_listbox = tk.Listbox(
|
||||
self.ai_image_tree = ttk.Treeview(
|
||||
list_wrap,
|
||||
width=34,
|
||||
height=1, # 由 pack fill/expand 撑满,height 仅为最小保底
|
||||
activestyle="none",
|
||||
font=("微软雅黑", 9),
|
||||
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"],
|
||||
columns=("time",),
|
||||
show="tree headings",
|
||||
selectmode="browse",
|
||||
style="AIImage.Treeview",
|
||||
yscrollcommand=scroll.set,
|
||||
)
|
||||
scroll.config(command=self.ai_image_listbox.yview)
|
||||
self.ai_image_listbox.pack(side=tk.LEFT, fill=tk.Y)
|
||||
scroll.pack(side=tk.RIGHT, fill=tk.Y)
|
||||
self.ai_image_listbox.bind("<<ListboxSelect>>", lambda e: _on_list_select(self))
|
||||
self.ai_image_tree.heading("#0", text="标题")
|
||||
self.ai_image_tree.heading("time", text="时间")
|
||||
self.ai_image_tree.column("#0", width=210, minwidth=140, anchor=tk.W)
|
||||
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 / 重命名 / 另存为 / 删除
|
||||
# 索引: 0=发送, 1=sep, 2=重命名, 3=另存为, 4=删除
|
||||
self.ai_image_menu = tk.Menu(self.root, tearoff=0)
|
||||
@@ -120,7 +278,7 @@ def create_ai_image_panel(self: "PQAutomationApp"):
|
||||
label="删除",
|
||||
command=lambda: _delete_current(self),
|
||||
)
|
||||
self.ai_image_listbox.bind(
|
||||
self.ai_image_tree.bind(
|
||||
"<Button-3>",
|
||||
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.ai_image_visible = False
|
||||
|
||||
# 初次加载缓存
|
||||
reload_ai_image_list(self)
|
||||
self.ai_image_meta_var.set("首次打开 AI 图片面板时加载缓存")
|
||||
|
||||
|
||||
def toggle_ai_image_panel(self: "PQAutomationApp"):
|
||||
"""切换 AI 图片面板显隐。"""
|
||||
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:
|
||||
@@ -245,58 +406,102 @@ def reload_ai_image_list(self: "PQAutomationApp", auto_select_first=True):
|
||||
其下列出该轮生成的所有图片。会话按"最近使用"倒序,组内按时间倒序。
|
||||
auto_select_first: 是否自动选中第一张图片(默认 True)。
|
||||
"""
|
||||
palette = _theme_colors()
|
||||
self.ai_image_records = _svc.list_records(base_dir=_get_app_base_dir(self))
|
||||
self.ai_image_listbox.delete(0, tk.END)
|
||||
# 维护行号 → 记录索引的映射;分隔头处为 None
|
||||
self._ai_image_row_map = []
|
||||
self._ai_image_row_session_map = []
|
||||
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)
|
||||
self.ai_image_listbox.insert(tk.END, header)
|
||||
# 头部行:禁用选中(视觉上变灰)
|
||||
last = self.ai_image_listbox.size() - 1
|
||||
self.ai_image_listbox.itemconfig(
|
||||
last, foreground=palette["muted"], selectforeground=palette["muted"],
|
||||
background=palette["bg"], selectbackground=palette["bg"],
|
||||
t0 = time.monotonic()
|
||||
if getattr(self, "_ai_image_reloading", False):
|
||||
logger.debug("[AIImagePanel] 忽略重入 reload 请求")
|
||||
return
|
||||
self._ai_image_reloading = True
|
||||
try:
|
||||
_apply_ai_image_list_style(self)
|
||||
all_records = _svc.list_records(base_dir=_get_app_base_dir(self))
|
||||
self._ai_image_all_records = list(all_records)
|
||||
self.ai_image_tree.delete(*self.ai_image_tree.get_children())
|
||||
self._ai_image_node_map = {}
|
||||
self._ai_image_session_node_map = {}
|
||||
|
||||
keyword = (self._ai_image_search_var.get() or "").strip().lower()
|
||||
records = []
|
||||
for rec in all_records:
|
||||
if not keyword:
|
||||
records.append(rec)
|
||||
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_row_session_map.append(sid)
|
||||
for rec in sess["records"]:
|
||||
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("暂无缓存图片")
|
||||
self._ai_image_list_loaded = True
|
||||
finally:
|
||||
self._ai_image_reloading = False
|
||||
|
||||
|
||||
def _format_session_header(index: int, sess: dict, is_current: bool) -> str:
|
||||
started = (sess.get("started_at") or "").replace("T", " ")[:16]
|
||||
tag = "(当前)" if is_current else ""
|
||||
count = len(sess.get("records") or [])
|
||||
if sess.get("session_id"):
|
||||
return f"── 会话 #{index} · {started} {tag}──"
|
||||
return f"── 未归类 · {started} ──"
|
||||
return f"会话 #{index} · {started} · {count} 张 {tag}".strip()
|
||||
return f"未归类 · {started} · {count} 张"
|
||||
|
||||
|
||||
def _format_list_label(rec: _svc.AIImageRecord) -> str:
|
||||
@@ -305,42 +510,105 @@ def _format_list_label(rec: _svc.AIImageRecord) -> str:
|
||||
extra = rec.extra or {}
|
||||
if isinstance(extra, dict) and extra.get("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 "(未命名)"
|
||||
# 列表宽度 width=34,需要扣除两格缩进 + size_tag
|
||||
max_name = 34 - 2 - len(size_tag) - 2
|
||||
# Treeview 标题列可变宽,给展示名预留较长空间
|
||||
max_name = 40 - len(size_tag)
|
||||
if max_name > 4 and len(name_line) > max_name:
|
||||
name_line = name_line[:max_name] + "…"
|
||||
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"):
|
||||
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:
|
||||
return
|
||||
row = sel[0]
|
||||
row_map = getattr(self, "_ai_image_row_map", None) or []
|
||||
if row >= len(row_map):
|
||||
self._ai_image_select_guard = True
|
||||
try:
|
||||
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
|
||||
ridx = row_map[row]
|
||||
if ridx is None:
|
||||
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)
|
||||
node_map = getattr(self, "_ai_image_node_map", None) or {}
|
||||
if item_id in node_map:
|
||||
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, target_record_id=rec.id)
|
||||
_select_record(self, rec)
|
||||
sid = _session_id_for_item(self, item_id)
|
||||
if sid:
|
||||
_switch_to_session(self, sid, show_message=False, refresh_list=False)
|
||||
|
||||
|
||||
def _select_record(self: "PQAutomationApp", rec: _svc.AIImageRecord):
|
||||
@@ -387,36 +655,54 @@ def _start_new_session(self: "PQAutomationApp"):
|
||||
return
|
||||
_svc.reset_session()
|
||||
self.ai_image_status_var.set("已开启新对话")
|
||||
# 新对话创建后不要自动选中历史图片,否则会立即把 session 切回旧会话。
|
||||
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:
|
||||
session_map = getattr(self, "_ai_image_row_session_map", None) or []
|
||||
if row < 0 or row >= len(session_map):
|
||||
return ""
|
||||
return session_map[row] or ""
|
||||
def _session_id_for_item(self: "PQAutomationApp", item_id: str) -> str:
|
||||
session_map = getattr(self, "_ai_image_session_node_map", None) or {}
|
||||
parent = item_id
|
||||
if parent not in session_map:
|
||||
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()
|
||||
if not sid:
|
||||
return
|
||||
if sid == _svc.get_session_id():
|
||||
return
|
||||
_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:
|
||||
for row, ridx in enumerate(getattr(self, "_ai_image_row_map", []) or []):
|
||||
if ridx is None:
|
||||
continue
|
||||
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
|
||||
item_id = _find_tree_item_by_record_id(self, target_record_id)
|
||||
if item_id:
|
||||
_set_tree_selection(self, item_id)
|
||||
self.ai_image_status_var.set("已切换到历史对话")
|
||||
if show_message:
|
||||
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_input.delete("1.0", tk.END)
|
||||
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:
|
||||
for row, ridx in enumerate(getattr(self, "_ai_image_row_map", []) or []):
|
||||
if ridx is None:
|
||||
continue
|
||||
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
|
||||
item_id = _find_tree_item_by_record_id(self, record.id)
|
||||
if item_id:
|
||||
_set_tree_selection(self, item_id)
|
||||
|
||||
|
||||
def _stop_request(self: "PQAutomationApp"):
|
||||
@@ -596,18 +876,9 @@ def _rename_current(self: "PQAutomationApp"):
|
||||
|
||||
target_id = rec.id
|
||||
reload_ai_image_list(self)
|
||||
# 重新定位
|
||||
for row, ridx in enumerate(getattr(self, "_ai_image_row_map", []) or []):
|
||||
if ridx is None:
|
||||
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
|
||||
item_id = _find_tree_item_by_record_id(self, target_id)
|
||||
if item_id:
|
||||
_set_tree_selection(self, item_id)
|
||||
|
||||
|
||||
# ---------------- 发送到 UCD ----------------
|
||||
@@ -615,17 +886,21 @@ def _rename_current(self: "PQAutomationApp"):
|
||||
|
||||
def _show_list_context_menu(self: "PQAutomationApp", event):
|
||||
"""在图片列表上显示右键菜单,并根据状态启用/禁用项。"""
|
||||
_hide_tree_tooltip(self)
|
||||
try:
|
||||
row = self.ai_image_listbox.nearest(event.y)
|
||||
item_id = self.ai_image_tree.identify_row(event.y)
|
||||
except Exception:
|
||||
row = -1
|
||||
row_map = getattr(self, "_ai_image_row_map", None) or []
|
||||
ridx = row_map[row] if 0 <= row < len(row_map) else None
|
||||
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):
|
||||
self.ai_image_listbox.selection_clear(0, tk.END)
|
||||
self.ai_image_listbox.selection_set(row)
|
||||
self.ai_image_listbox.activate(row)
|
||||
_set_tree_selection(self, item_id)
|
||||
_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
|
||||
ucd = getattr(self, "ucd", None)
|
||||
@@ -777,7 +1052,7 @@ class AIImagePanelMixin:
|
||||
_select_record = _select_record
|
||||
_redraw_preview = _redraw_preview
|
||||
_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
|
||||
_update_request_progress = _update_request_progress
|
||||
_send_prompt = _send_prompt
|
||||
|
||||
Reference in New Issue
Block a user