ImageDirPreviewの左クリックメニューとプレビュー状態のソースパスを修正し、プレビューセッションIDを追加
This commit is contained in:
+11
-5
@@ -22,6 +22,7 @@ from server import PromptServer
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# v9i: ImageDirPreview left-click menu, hover frame, and preview-state source path fix.
|
||||
# v9h: ImageDirPreview right-click event separation and blank-area suppression.
|
||||
# v9g: ImageDirPreview right-click capture fix for ComfyUI/LiteGraph context menu.
|
||||
# v9f: ImageDirPreview context menu foundation and Set as checkpoint thumbnail.
|
||||
@@ -838,10 +839,12 @@ def _preferred_checkpoint_thumbnail_path(safetensors_path: Path) -> Path:
|
||||
return Path(safetensors_path).resolve().with_suffix(".jpg")
|
||||
|
||||
|
||||
def _get_preview_state_item(tab_id, node_id, item_index):
|
||||
def _get_preview_state_item(tab_id, node_id, item_index, preview_session_id=None):
|
||||
key = _state_key(tab_id, node_id)
|
||||
with _STATE_LOCK:
|
||||
state = dict(_PREVIEW_STATES.get(key, {}))
|
||||
if preview_session_id and state.get("preview_session_id") != str(preview_session_id):
|
||||
return state, None, None, None
|
||||
source_paths = state.get("source_paths")
|
||||
if not isinstance(source_paths, list):
|
||||
return None, None, None, None
|
||||
@@ -1451,6 +1454,7 @@ def _load_image_dir_preview(node_id, relpath, search_directory=None, tab_id="",
|
||||
"search_directory": str(_safe_search_root(search_directory) or ""),
|
||||
"max_preview_images": max_preview_images,
|
||||
"source_paths": [str(p) for p in loaded_paths],
|
||||
"preview_session_id": str(uuid.uuid4()),
|
||||
})
|
||||
return sheet, extra
|
||||
|
||||
@@ -2020,7 +2024,8 @@ class ImageDirPreview:
|
||||
relpath = _normalize_relpath(ckpt_name_str)
|
||||
sheet, extra = _load_image_dir_preview(unique_id, relpath, search_directory, tab_id=hps_tab_id, send_progress=True, max_preview_images=max_preview_images)
|
||||
status = _get_status(relpath)
|
||||
_store_preview_state(hps_tab_id, unique_id, relpath, status, node_class="ImageDirPreview")
|
||||
state_extra = {k: v for k, v in extra.items() if k not in ("ckpt_name_str", "status")}
|
||||
_store_preview_state(hps_tab_id, unique_id, relpath, status, **state_extra)
|
||||
_send_preview(unique_id, _image_dir_title(relpath), sheet, extra, tab_id=hps_tab_id)
|
||||
return ()
|
||||
|
||||
@@ -2085,7 +2090,7 @@ async def image_dir_preview_context_menu(request):
|
||||
data = await request.json()
|
||||
node_id = str(data.get("node_id", ""))
|
||||
tab_id = _clean_tab_id(data.get("tab_id", ""))
|
||||
state, index, source_path, relpath = _get_preview_state_item(tab_id, node_id, data.get("item_index"))
|
||||
state, index, source_path, relpath = _get_preview_state_item(tab_id, node_id, data.get("item_index"), data.get("preview_session_id"))
|
||||
if not state or index is None or source_path is None or not relpath:
|
||||
return web.json_response({"ok": False, "error": "Preview item not found."}, status=404)
|
||||
exists = _preview_item_exists_for_state(state, source_path)
|
||||
@@ -2115,7 +2120,7 @@ async def image_dir_preview_set_thumbnail(request):
|
||||
data = await request.json()
|
||||
node_id = str(data.get("node_id", ""))
|
||||
tab_id = _clean_tab_id(data.get("tab_id", ""))
|
||||
state, index, source_path, relpath = _get_preview_state_item(tab_id, node_id, data.get("item_index"))
|
||||
state, index, source_path, relpath = _get_preview_state_item(tab_id, node_id, data.get("item_index"), data.get("preview_session_id"))
|
||||
if not state or index is None or source_path is None or not relpath:
|
||||
return web.json_response({"ok": False, "error": "Preview item not found."}, status=404)
|
||||
if not _preview_item_exists_for_state(state, source_path):
|
||||
@@ -2401,7 +2406,8 @@ async def review_sync_checkpoint(request):
|
||||
True,
|
||||
max_preview_images,
|
||||
)
|
||||
_store_preview_state(tab_id, node_id, relpath, status, node_class="ImageDirPreview")
|
||||
state_extra = {k: v for k, v in extra.items() if k not in ("ckpt_name_str", "status")}
|
||||
_store_preview_state(tab_id, node_id, relpath, status, **state_extra)
|
||||
_send_preview(node_id, _image_dir_title(relpath), sheet, extra, tab_id=tab_id)
|
||||
preview_count += 1
|
||||
except Exception:
|
||||
|
||||
@@ -774,9 +774,11 @@ async function restoreNodeStateFromBackend(node, nodeClass) {
|
||||
}
|
||||
|
||||
|
||||
|
||||
// ---------- Preview ----------
|
||||
let hpsContextMenuEl = null;
|
||||
let hpsContextMenuHideInstalled = false;
|
||||
let imageDirHoverCaptureInstalled = false;
|
||||
|
||||
function ensureHpsContextMenu() {
|
||||
if (hpsContextMenuEl) return hpsContextMenuEl;
|
||||
@@ -806,7 +808,10 @@ function hideHpsContextMenu() {
|
||||
function installHpsContextMenuHide() {
|
||||
if (hpsContextMenuHideInstalled) return;
|
||||
hpsContextMenuHideInstalled = true;
|
||||
document.addEventListener("mousedown", () => hideHpsContextMenu(), true);
|
||||
document.addEventListener("mousedown", (event) => {
|
||||
if (hpsContextMenuEl?.contains?.(event.target)) return;
|
||||
hideHpsContextMenu();
|
||||
}, true);
|
||||
document.addEventListener("scroll", () => hideHpsContextMenu(), true);
|
||||
window.addEventListener("blur", () => hideHpsContextMenu());
|
||||
}
|
||||
@@ -907,20 +912,91 @@ function imageDirPreviewItemAtLocal(node, local) {
|
||||
return { index, sourcePath: String(sourcePaths[index] || ""), ckptName: String(st.ckpt_name_str || "") };
|
||||
}
|
||||
|
||||
function imageDirPreviewTileRect(node, index) {
|
||||
const st = node.__hpsPreviewState || {};
|
||||
const cols = Number(st.columns || 0);
|
||||
const tileW = Number(st.tile_width || 0);
|
||||
const tileH = Number(st.tile_height || 0);
|
||||
const gap = Number(st.gap || 0);
|
||||
if (!cols || !tileW || !tileH || index == null || index < 0) return null;
|
||||
const box = previewDrawBox(node);
|
||||
if (!box) return null;
|
||||
const row = Math.floor(index / cols);
|
||||
const col = index % cols;
|
||||
const sx = col * (tileW + gap);
|
||||
const sy = row * (tileH + gap);
|
||||
const scaleX = box.w / box.imageW;
|
||||
const scaleY = box.h / box.imageH;
|
||||
return { x: box.x + sx * scaleX, y: box.y + sy * scaleY, w: tileW * scaleX, h: tileH * scaleY };
|
||||
}
|
||||
|
||||
function clearImageDirHover(node) {
|
||||
if (!node) return;
|
||||
if (node.__hpsHoveredPreviewIndex != null) {
|
||||
node.__hpsHoveredPreviewIndex = null;
|
||||
app.graph?.setDirtyCanvas?.(true, true);
|
||||
}
|
||||
}
|
||||
|
||||
function setImageDirHover(node, item) {
|
||||
const index = item?.index ?? null;
|
||||
if (node.__hpsHoveredPreviewIndex !== index) {
|
||||
node.__hpsHoveredPreviewIndex = index;
|
||||
app.graph?.setDirtyCanvas?.(true, true);
|
||||
}
|
||||
}
|
||||
|
||||
function updateImageDirHoverFromEvent(event) {
|
||||
let handled = false;
|
||||
const nodes = [...(app.graph?._nodes || [])].reverse();
|
||||
for (const node of nodes) {
|
||||
if (!node || node.flags?.collapsed || !isNodeClass(node, "ImageDirPreview")) continue;
|
||||
const local = graphEventToLocal(node, event);
|
||||
if (!hpsLocalInsideNode(node, local)) {
|
||||
clearImageDirHover(node);
|
||||
continue;
|
||||
}
|
||||
const item = imageDirPreviewItemAtLocal(node, local);
|
||||
setImageDirHover(node, item);
|
||||
handled = true;
|
||||
break;
|
||||
}
|
||||
if (!handled) {
|
||||
for (const node of app.graph?._nodes || []) {
|
||||
if (isNodeClass(node, "ImageDirPreview")) clearImageDirHover(node);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function installImageDirHoverCapture() {
|
||||
if (imageDirHoverCaptureInstalled) return;
|
||||
const canvasEl = app.canvas?.canvas;
|
||||
if (!canvasEl) return;
|
||||
imageDirHoverCaptureInstalled = true;
|
||||
canvasEl.addEventListener("mousemove", (event) => updateImageDirHoverFromEvent(event), true);
|
||||
canvasEl.addEventListener("mouseleave", () => {
|
||||
for (const node of app.graph?._nodes || []) {
|
||||
if (isNodeClass(node, "ImageDirPreview")) clearImageDirHover(node);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async function fetchImageDirContextMenu(node, itemIndex) {
|
||||
const st = node.__hpsPreviewState || {};
|
||||
const response = await api.fetchApi(`/${EXTENSION_PREFIX}/image_dir_preview/context_menu`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ node_id: node.id, item_index: itemIndex, tab_id: HPS_TAB_ID }),
|
||||
body: JSON.stringify({ node_id: node.id, item_index: itemIndex, preview_session_id: st.preview_session_id || "", tab_id: HPS_TAB_ID }),
|
||||
});
|
||||
return await response.json();
|
||||
}
|
||||
|
||||
async function setImageDirCheckpointThumbnail(node, itemIndex) {
|
||||
const st = node.__hpsPreviewState || {};
|
||||
const response = await api.fetchApi(`/${EXTENSION_PREFIX}/image_dir_preview/set_thumbnail`, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ node_id: node.id, item_index: itemIndex, tab_id: HPS_TAB_ID }),
|
||||
body: JSON.stringify({ node_id: node.id, item_index: itemIndex, preview_session_id: st.preview_session_id || "", tab_id: HPS_TAB_ID }),
|
||||
});
|
||||
const result = await response.json();
|
||||
if (result?.ok) {
|
||||
@@ -954,54 +1030,11 @@ async function openImageDirContextMenu(node, item, event) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
let imageDirContextCaptureInstalled = false;
|
||||
|
||||
function findImageDirContextTarget(event) {
|
||||
const nodes = [...(app.graph?._nodes || [])].reverse();
|
||||
for (const node of nodes) {
|
||||
if (!node || node.flags?.collapsed || !isNodeClass(node, "ImageDirPreview")) continue;
|
||||
const local = graphEventToLocal(node, event);
|
||||
if (!hpsLocalInsideNode(node, local)) continue;
|
||||
const item = imageDirPreviewItemAtLocal(node, local);
|
||||
// Return the node even when the click is on ImageDirPreview blank space.
|
||||
// Blank-space right-clicks should suppress the standard ComfyUI menu but
|
||||
// must not open an action menu.
|
||||
return { node, item };
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function installImageDirContextMenuCapture() {
|
||||
if (imageDirContextCaptureInstalled) return;
|
||||
const canvasEl = app.canvas?.canvas;
|
||||
if (!canvasEl) return;
|
||||
imageDirContextCaptureInstalled = true;
|
||||
|
||||
canvasEl.addEventListener("contextmenu", (event) => {
|
||||
const target = findImageDirContextTarget(event);
|
||||
if (!target) return;
|
||||
|
||||
// Any right-click inside ImageDirPreview is owned by HPS. This prevents
|
||||
// browser/LiteGraph menus from leaking through blank areas.
|
||||
event.preventDefault?.();
|
||||
event.stopPropagation?.();
|
||||
event.stopImmediatePropagation?.();
|
||||
|
||||
if (target.item) {
|
||||
openImageDirContextMenu(target.node, target.item, event);
|
||||
} else {
|
||||
hideHpsContextMenu();
|
||||
}
|
||||
return false;
|
||||
}, true);
|
||||
}
|
||||
|
||||
function setupPreviewNode(nodeType) {
|
||||
installMinSize(nodeType, 340, 300);
|
||||
installTabIdSupport(nodeType);
|
||||
installCursorCapture();
|
||||
installImageDirContextMenuCapture();
|
||||
installImageDirHoverCapture();
|
||||
const origCreated = nodeType.prototype.onNodeCreated;
|
||||
nodeType.prototype.onNodeCreated = function () {
|
||||
const r = origCreated ? origCreated.apply(this, arguments) : undefined;
|
||||
@@ -1054,6 +1087,19 @@ function setupPreviewNode(nodeType) {
|
||||
const drawBox = previewDrawBox(this);
|
||||
if (img && drawBox) {
|
||||
ctx.drawImage(img, drawBox.x, drawBox.y, drawBox.w, drawBox.h);
|
||||
if (g.isImageDir && this.__hpsHoveredPreviewIndex != null) {
|
||||
const rect = imageDirPreviewTileRect(this, this.__hpsHoveredPreviewIndex);
|
||||
if (rect) {
|
||||
ctx.save();
|
||||
ctx.strokeStyle = "rgba(255,255,255,0.95)";
|
||||
ctx.lineWidth = 2;
|
||||
ctx.strokeRect(rect.x + 1, rect.y + 1, Math.max(1, rect.w - 2), Math.max(1, rect.h - 2));
|
||||
ctx.strokeStyle = "rgba(0,0,0,0.75)";
|
||||
ctx.lineWidth = 1;
|
||||
ctx.strokeRect(rect.x + 3, rect.y + 3, Math.max(1, rect.w - 6), Math.max(1, rect.h - 6));
|
||||
ctx.restore();
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const st = this.__hpsPreviewState || {};
|
||||
const isLoading = st.status === "loading" || !!st.progress;
|
||||
@@ -1080,17 +1126,22 @@ function setupPreviewNode(nodeType) {
|
||||
ctx.restore();
|
||||
};
|
||||
|
||||
const origMouseDown = nodeType.prototype.onRightClick;
|
||||
nodeType.prototype.onRightClick = function (event, local_pos) {
|
||||
// LiteGraphの onRightClick は第2引数にすでにローカル座標 (local_pos) が渡ってくる
|
||||
if (isNodeClass(this, "ImageDirPreview")) {
|
||||
if (hpsLocalInsideNode(this, local_pos)) {
|
||||
// true を返すことで、LiteGraphの標準ノードメニュー展開を完全にブロックする!
|
||||
// (実際の独自メニュー表示は、既存の contextmenu イベントキャプチャ側が担当する)
|
||||
const origMouseDown = nodeType.prototype.onMouseDown;
|
||||
nodeType.prototype.onMouseDown = function (event, pos, canvas) {
|
||||
if (isNodeClass(this, "ImageDirPreview") && event?.button === 0) {
|
||||
const local = previewLocalFromEventOrPos(this, event, pos);
|
||||
const item = imageDirPreviewItemAtLocal(this, local);
|
||||
if (item) {
|
||||
event.preventDefault?.();
|
||||
event.stopPropagation?.();
|
||||
event.stopImmediatePropagation?.();
|
||||
setImageDirHover(this, item);
|
||||
openImageDirContextMenu(this, item, event);
|
||||
return true;
|
||||
}
|
||||
hideHpsContextMenu();
|
||||
}
|
||||
return origRightClick ? origRightClick.apply(this, arguments) : false;
|
||||
return origMouseDown ? origMouseDown.apply(this, arguments) : false;
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1118,6 +1169,7 @@ api.addEventListener(PREVIEW_EVENT, ({ detail }) => {
|
||||
}
|
||||
|
||||
node.__hpsPreviewState = { ...(node.__hpsPreviewState || {}), ...detail };
|
||||
if (isNodeClass(node, "ImageDirPreview")) node.__hpsHoveredPreviewIndex = null;
|
||||
const caption = detail.progress_message || detail.message || `${detail.count ?? 0} img · ${detail.columns ?? 0}×${detail.rows ?? 0} · ${detail.width ?? 0}×${detail.height ?? 0}`;
|
||||
node.__hpsPreviewCaption = caption;
|
||||
if (!detail.image) {
|
||||
|
||||
Reference in New Issue
Block a user