From 2f7cda709c1795febf5baa86c97f8ae1f5ce6110 Mon Sep 17 00:00:00 2001 From: DemonGatanjieu <207458273+DemonGatanjieu@users.noreply.github.com> Date: Mon, 19 Oct 2026 17:02:51 +0800 Subject: [PATCH] Right-click menu settings: placement as two drawn cards, a preview line Co-Authored-By: Claude Opus 5.5 --- ARCHITECTURE.md | 2 +- CHANGELOG.md | 2 +- web/modules/locales.js | 26 ++++-- web/modules/ui_canvas_menu_settings.js | 103 +++++++++++++++++++---- web/modules/ui_settings_page.js | 2 +- web/styles.css | 2 +- web/styles/32-canvas-tools.css | 108 +++++++++++++++++++++++++ 7 files changed, 217 insertions(+), 28 deletions(-) diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 6389eb5..af3d3c1 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -453,7 +453,7 @@ covered by the `styles.css` manifest. (`/anomalous/find_model`, `openModelDetail`), save its values or the picked nodes as a combo, each only where it applies; for the empty canvas, AMB's pages. `canvas_menu_settings.js` keeps which entries are on, their order and whether they sit in one "AMB" submenu or right in the menu - (localStorage, no DOM); `ui_canvas_menu_settings.js` is its Settings -> Canvas group. + (localStorage, no DOM); `ui_canvas_menu_settings.js` is its Settings -> Canvas group (two placement cards drawing a small menu each, a preview line per list). `ui_quick_search.js` is the canvas's quick search (Alt+B, the command `Anomalous.ModelBrowser.QuickSearch`, or the empty canvas's menu): a bar that finds combos, models and saved prompts as you type and puts the chosen one where the pointer was (a combo diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a2bd18..a8af02e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ ### 🖱️ AMB in the canvas's right-click menu (画布右键菜单) - **Right-click a node → AMB** offers what Current node does for it, without opening the browser first: **Open in AMB** (a prompt node opens Prompts on it, any other node Current node), **Change model by picture**, **Insert a LoRA before / after**, **Apply saved values**, **Pictures made with this model**, **Model details**, **Save this node's values** and **Save as combo**. Each entry shows only where it applies: a checkpoint loader has no "before", a prompt node no model entries. - **Right-click the empty canvas → AMB** opens any of AMB's pages. -- **Make it yours: Settings → Canvas → Right-click menu**. Switch each entry on or off, move it up or down, and choose whether AMB's entries sit in one **AMB** submenu (the default, ComfyUI's menu stays tidy) or right in the menu. *Adjust this menu…* at the end of the submenu leads there. **Save as combo** moved into the AMB submenu with the rest. +- **Make it yours: Settings → Canvas → Right-click menu**. Switch each entry on or off and move it up or down; a line above each list shows what the menu will offer. Two cards, each with a small drawing of the menu, choose whether AMB's entries sit **together under "AMB"** (the default, ComfyUI's menu gets one line more) or are **listed in the menu**. *Adjust this menu…* at the end of the submenu leads there. **Save as combo** moved into the AMB submenu with the rest. - In the model picker, Esc now closes the picker wherever the focus is, and only the picker: it used to close the browser behind it when the search box had lost the focus. - Fixed: when a model folder is a link (a junction to another drive, say), a combo kept from a picture and other lookups of saved model paths got a wrong place for the model, or failed. diff --git a/web/modules/locales.js b/web/modules/locales.js index 6c4bc6e..1450bbe 100644 --- a/web/modules/locales.js +++ b/web/modules/locales.js @@ -655,10 +655,15 @@ export const i18n = { canvasMenuAdjust: "调整这个菜单…", canvasMenuModelUnknown: "AMB 没找到这个模型,可能还没扫描,或者文件不在模型文件夹里。", canvasMenuGroup: "右键菜单", - canvasMenuPlacement: "放在哪里", - canvasMenuPlacementHelp: "收进一个“AMB”子菜单,ComfyUI 的菜单保持清爽;或者直接放在菜单里,少点一下。", - canvasMenuPlacementSubmenu: "收进 AMB 子菜单", - canvasMenuPlacementTop: "直接放外层", + canvasMenuPlacement: "AMB 的选项出现在哪里", + canvasMenuPlace_submenu: "归到一个“AMB”里", + canvasMenuPlaceHelp_submenu: "ComfyUI 的菜单只多一行;要用时把鼠标移到 AMB 上", + canvasMenuPlace_top: "直接列在菜单里", + canvasMenuPlaceHelp_top: "少移一下鼠标,但 ComfyUI 的菜单会变长", + canvasMenuSketchComfy: "属性|克隆|删除", + canvasMenuPreview: "会看到:", + canvasMenuPreviewNone: "什么也没有:AMB 不会出现在这个菜单里", + canvasMenuPreviewNote: "(只列这个节点用得上的)", canvasMenuList_node: "右键一个节点", canvasMenuList_canvas: "右键画布空白处", canvasMenuUp: "上移", @@ -3462,10 +3467,15 @@ export const i18n = { canvasMenuAdjust: "Adjust this menu…", canvasMenuModelUnknown: "AMB does not know this model: maybe it is not scanned yet, or the file is outside the model folders.", canvasMenuGroup: "Right-click menu", - canvasMenuPlacement: "Where", - canvasMenuPlacementHelp: "In one “AMB” submenu, keeping ComfyUI's menu tidy, or right in the menu, one click less.", - canvasMenuPlacementSubmenu: "In an AMB submenu", - canvasMenuPlacementTop: "Right in the menu", + canvasMenuPlacement: "Where AMB's entries show", + canvasMenuPlace_submenu: "Together under “AMB”", + canvasMenuPlaceHelp_submenu: "ComfyUI's menu gets one line more; point at AMB to see them", + canvasMenuPlace_top: "Listed in the menu", + canvasMenuPlaceHelp_top: "One move less, but ComfyUI's menu gets longer", + canvasMenuSketchComfy: "Properties|Clone|Remove", + canvasMenuPreview: "You will see:", + canvasMenuPreviewNone: "Nothing: AMB stays out of this menu", + canvasMenuPreviewNote: "(only those that fit the node)", canvasMenuList_node: "Right-click on a node", canvasMenuList_canvas: "Right-click on the empty canvas", canvasMenuUp: "Move up", diff --git a/web/modules/ui_canvas_menu_settings.js b/web/modules/ui_canvas_menu_settings.js index 7a31526..9182f35 100644 --- a/web/modules/ui_canvas_menu_settings.js +++ b/web/modules/ui_canvas_menu_settings.js @@ -1,8 +1,9 @@ /** - * Settings -> Canvas -> Right-click menu: where AMB's entries sit (one "AMB" submenu or right - * in ComfyUI's menu) and, for a node's menu and the empty canvas's, each entry on or off and - * moved up or down, with Reset. Saved at once (canvas_menu_settings.js); the menu reads it each - * time it opens. + * Settings -> Canvas -> Right-click menu: where AMB's entries sit, as two cards each drawing a + * small menu (one "AMB" line opening them, or them listed in ComfyUI's menu), and, for a node's + * menu and the empty canvas's, each entry on or off and moved up or down, with a line showing + * what the menu will offer, and Reset. Saved at once (canvas_menu_settings.js); the menu reads + * it each time it opens. */ import { translate as t } from './locales.js'; @@ -22,8 +23,70 @@ function button(className, label, onClick) { return node; } -/** The rows of one menu's entries (`which`: 'node' or 'canvas'). */ -function entryList(settings, which, redraw) { +const entryName = entry => t(`canvasMenuEntry_${entry.id}`); + +/** What the menu will offer, in order: the entries switched on, as chips. */ +function previewLine(settings, which) { + const line = el('div', 'anomalous-menu-preview'); + const draw = () => { + const on = settings[which].filter(entry => entry.on); + line.replaceChildren(el('span', 'anomalous-menu-preview-label', t('canvasMenuPreview'))); + if (!on.length) { + line.append(el('span', 'anomalous-menu-preview-none', t('canvasMenuPreviewNone'))); + return; + } + line.append(...on.map(entry => el('span', 'anomalous-menu-preview-chip', entryName(entry)))); + if (which === 'node') line.append(el('span', 'anomalous-menu-preview-note', t('canvasMenuPreviewNote'))); + }; + line.redraw = draw; + draw(); + return line; +} + +/** A small menu drawn in a card: ComfyUI's own lines, then AMB's (`amb`: 'submenu' or 'top'). */ +function sketch(settings, amb) { + const box = el('span', `anomalous-menu-sketch is-${amb}`); + const menu = el('span', 'anomalous-menu-sketch-menu'); + for (const word of t('canvasMenuSketchComfy').split('|')) menu.append(el('span', 'anomalous-menu-sketch-line is-comfy', word)); + const ours = settings.node.filter(entry => entry.on).slice(0, 3).map(entryName); + if (!ours.length) ours.push(t('canvasMenuEntry_open')); + if (amb === 'top') { + menu.append(el('span', 'anomalous-menu-sketch-rule'), ...ours.map(name => el('span', 'anomalous-menu-sketch-line is-ours', name))); + box.append(menu); + return box; + } + menu.append(el('span', 'anomalous-menu-sketch-line is-amb', `${t('canvasMenuTitle')} ▸`)); + const sub = el('span', 'anomalous-menu-sketch-menu is-sub'); + sub.append(...ours.map(name => el('span', 'anomalous-menu-sketch-line', name))); + box.append(menu, sub); + return box; +} + +/** The two placement cards; a click picks one. */ +function placementCards(settings, redraw) { + const cards = el('div', 'anomalous-menu-places'); + cards.setAttribute('role', 'radiogroup'); + cards.setAttribute('aria-label', t('canvasMenuPlacement')); + for (const value of ['submenu', 'top']) { + const picked = settings.placement === value; + const card = button(`anomalous-menu-place${picked ? ' is-picked' : ''}`, undefined, () => { + if (picked) return; + settings.placement = value; + saveMenuSettings(settings); + redraw(); + }); + card.setAttribute('role', 'radio'); + card.setAttribute('aria-checked', String(picked)); + const head = el('span', 'anomalous-menu-place-head'); + head.append(el('span', 'anomalous-menu-place-mark', picked ? '✓' : ''), el('span', 'anomalous-menu-place-title', t(`canvasMenuPlace_${value}`))); + card.append(head, sketch(settings, value), el('span', 'anomalous-menu-place-help', t(`canvasMenuPlaceHelp_${value}`))); + cards.append(card); + } + return cards; +} + +/** The rows of one menu's entries (`which`: 'node' or 'canvas'); `changed()` after a switch. */ +function entryList(settings, which, redraw, changed) { const list = el('div', 'anomalous-menu-entries'); list.setAttribute('aria-label', t(`canvasMenuList_${which}`)); const entries = settings[which]; @@ -53,6 +116,7 @@ function entryList(settings, which, redraw) { entry.on = toggle.checked; line.classList.toggle('is-off', !entry.on); saveMenuSettings(settings); + changed(); }; line.append(copy, up, down, toggle); list.append(line); @@ -64,24 +128,31 @@ function entryList(settings, which, redraw) { * The group's rows: `row(titleKey, helpKey, control)` and `segment(choices, current, onPick)` * are the settings page's own helpers; `redraw()` draws the page again. */ -export function canvasMenuRows({ row, segment, redraw }) { +export function canvasMenuRows({ row, redraw }) { const settings = menuSettings(); - const placement = row('canvasMenuPlacement', 'canvasMenuPlacementHelp', segment( - [['submenu', 'canvasMenuPlacementSubmenu'], ['top', 'canvasMenuPlacementTop']], settings.placement, (value) => { - settings.placement = value; - saveMenuSettings(settings); - redraw(); - })); + let places = placementCards(settings, redraw); + const nodePreview = previewLine(settings, 'node'); + const canvasPreview = previewLine(settings, 'canvas'); + // A switch changes what the cards' small menus and the preview lines show. + const nodeChanged = () => { + nodePreview.redraw(); + const next = placementCards(settings, redraw); + places.replaceWith(next); + places = next; + }; const reset = row('canvasMenuReset', 'canvasMenuResetHelp', button('anomalous-scan-secondary anomalous-scan-small-btn', t('settingsReset'), () => { resetMenuSettings(); redraw(); })); return [ - placement, + el('h4', 'anomalous-menu-entries-title is-first', t('canvasMenuPlacement')), + places, el('h4', 'anomalous-menu-entries-title', t('canvasMenuList_node')), - entryList(settings, 'node', redraw), + nodePreview, + entryList(settings, 'node', redraw, nodeChanged), el('h4', 'anomalous-menu-entries-title', t('canvasMenuList_canvas')), - entryList(settings, 'canvas', redraw), + canvasPreview, + entryList(settings, 'canvas', redraw, () => canvasPreview.redraw()), reset, ]; } diff --git a/web/modules/ui_settings_page.js b/web/modules/ui_settings_page.js index 2aed419..f2adc8c 100644 --- a/web/modules/ui_settings_page.js +++ b/web/modules/ui_settings_page.js @@ -494,7 +494,7 @@ const TABS = [ ['look', 'settingsTabLook', (owner, redraw) => [appearanceGroup(owner, redraw), cardsGroup(owner, redraw), galleryGroup(owner), openingGroup(owner, redraw)]], ['models', 'settingsTabModels', (owner, redraw) => [foldersGroup(owner), networkGroup(), downloadGroup(owner, redraw), sidecarGroup()]], ['workflows', 'settingsTabWorkflows', (owner) => [workflowGroup(owner)]], - ['canvas', 'settingsTabCanvas', (owner, redraw) => [group('canvasMenuGroup', ...canvasMenuRows({ row, segment, redraw }))]], + ['canvas', 'settingsTabCanvas', (owner, redraw) => [group('canvasMenuGroup', ...canvasMenuRows({ row, redraw }))]], ['packs', 'settingsTabPacks', (owner, redraw) => [renderPackUsage(owner, redraw)]], ['prompts', 'settingsTabPrompts', () => [translationGroup()]], ['backup', 'settingsTabBackup', (owner) => [backupGroup(owner)]], diff --git a/web/styles.css b/web/styles.css index 9daa89f..d10993f 100644 --- a/web/styles.css +++ b/web/styles.css @@ -30,5 +30,5 @@ @import url("./styles/29-workflow-detail.css?v=20261011-wfd-3"); @import url("./styles/30-scan-picker.css?v=20261016-lists"); @import url("./styles/31-model-info.css?v=20261014-desc-2"); -@import url("./styles/32-canvas-tools.css?v=20261019-search"); +@import url("./styles/32-canvas-tools.css?v=20261019-places"); @import url("./styles/33-kept-combos.css?v=20261019-hero"); diff --git a/web/styles/32-canvas-tools.css b/web/styles/32-canvas-tools.css index e1fbe99..bf7875c 100644 --- a/web/styles/32-canvas-tools.css +++ b/web/styles/32-canvas-tools.css @@ -149,6 +149,114 @@ } /* Settings -> Canvas -> Right-click menu (ui_canvas_menu_settings.js). */ +.anomalous-menu-entries-title.is-first { margin-top: 2px; } + +.anomalous-menu-places { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); + gap: 10px; +} + +.anomalous-menu-place { + display: flex; + flex-direction: column; + gap: 10px; + padding: 12px; + border: 1px solid var(--amb-border); + border-radius: 12px; + background: rgba(var(--amb-ink-rgb), 0.02); + color: var(--amb-text-main); + font: inherit; + text-align: left; + cursor: pointer; +} + +.anomalous-menu-place:hover { border-color: var(--amb-border-hover); } + +.anomalous-menu-place.is-picked { + border: 2px solid var(--amb-link); + padding: 11px; + background: color-mix(in srgb, var(--amb-link) 8%, transparent); + cursor: default; +} + +.anomalous-menu-place-head { display: flex; align-items: center; gap: 8px; } + +.anomalous-menu-place-mark { + display: flex; + flex: none; + align-items: center; + justify-content: center; + width: 18px; + height: 18px; + border: 1.5px solid var(--amb-border-strong); + border-radius: 50%; + font-size: 11px; + font-weight: 700; +} + +.is-picked .anomalous-menu-place-mark { border-color: var(--amb-link); background: var(--amb-link); color: var(--amb-bg-panel); } + +.anomalous-menu-place-title { font-size: 13px; font-weight: 700; } + +.anomalous-menu-place-help { color: var(--amb-text-muted); font-size: 11px; line-height: 1.45; } + +/* The small menus drawn in the cards. */ +.anomalous-menu-sketch { display: flex; align-items: flex-start; gap: 4px; } + +.anomalous-menu-sketch-menu { + display: flex; + flex-direction: column; + width: 118px; + padding: 3px 0; + border: 1px solid var(--amb-border); + border-radius: 6px; + background: var(--amb-bg-panel); +} + +/* The submenu opens beside the AMB line (the fourth line). */ +.anomalous-menu-sketch-menu.is-sub { margin-top: 63px; border-color: color-mix(in srgb, var(--amb-link) 50%, transparent); } + +.anomalous-menu-sketch-line { + padding: 2px 8px; + overflow: hidden; + color: var(--amb-text-main); + font-size: 10.5px; + line-height: 17px; + text-overflow: ellipsis; + white-space: nowrap; +} + +.anomalous-menu-sketch-line.is-comfy { color: var(--amb-text-muted); } + +.anomalous-menu-sketch-line.is-amb { background: color-mix(in srgb, var(--amb-link) 18%, transparent); color: var(--amb-link); font-weight: 700; } + +.anomalous-menu-sketch-line.is-ours { color: var(--amb-link); } + +.anomalous-menu-sketch-rule { margin: 2px 0; border-top: 1px solid var(--amb-border); } + +/* What the menu will offer. */ +.anomalous-menu-preview { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 5px; + margin-bottom: 6px; + font-size: 11px; +} + +.anomalous-menu-preview-label { color: var(--amb-text-muted); } + +.anomalous-menu-preview-chip { + padding: 2px 9px; + border-radius: 999px; + background: color-mix(in srgb, var(--amb-link) 14%, transparent); + color: var(--amb-link); +} + +.anomalous-menu-preview-none, +.anomalous-menu-preview-note { color: var(--amb-text-muted); } + .anomalous-menu-entries-title { margin: 14px 0 4px; color: var(--amb-text-muted);