Right-click menu settings: placement as two drawn cards, a preview line

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
DemonGatanjieu
2026-10-19 17:02:51 +08:00
co-authored by Claude Opus 5.5
parent 2836cceb7c
commit 2f7cda709c
7 changed files with 217 additions and 28 deletions
+1 -1
View File
@@ -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
+1 -1
View File
@@ -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.
+18 -8
View File
@@ -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",
+87 -16
View File
@@ -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,
];
}
+1 -1
View File
@@ -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)]],
+1 -1
View File
@@ -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");
+108
View File
@@ -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);