Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ To add one, click **Add reference image** and pick images from the Gallery or up

Hovering over a reference's thumbnail offers **Crop**, to use only part of the image, and **Use size**, to set the generation's size to match the reference. The switch on each card turns a reference off without removing it.

In the crop dialog, drag the crop box or its handles, or pick an **Aspect ratio** (16:9, 3:2, 4:3, 1:1, 3:4, 2:3 or 9:16) to keep the box in proportion while you resize it. Scroll to zoom in on the image, hold **Space** (or use the middle mouse button) and drag to move the view, and use the **Fit image**, **Actual size**, **Zoom in** and **Zoom out** buttons. **Apply** uses the crop for the reference; **Save to Uploads** also stores the cropped image as an upload in the Gallery without changing the reference.

What a reference does, and what options it has, depends on the model family:

| Model family | How references work | Options |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ The gear button in the Gallery header opens its settings:
* **Show in-progress section** — shows generations that are still running at the top of the grid.
* **Pagination** — **Infinite** scrolling or numbered **Pages**.

**All Gallery settings…** at the bottom opens the full settings. They also include **Send new results to the selected board** (under **Destination**), which is on by default; turn it off to keep sending results to the current destination board, which then becomes the **Auto** board. The full settings also include the sort order and the board visibility and sorting options from the [Filter & sort boards](/users-guide/gallery/introduction/#finding-filtering-and-sorting-boards) menu. All Gallery settings are saved with the project.
**All Gallery settings…** at the bottom opens the full settings. They also include **Middle-click opens media in a new tab** (under **Thumbnails**), off by default, which makes a middle click on an image or video tile open it in a new browser tab, and **Send new results to the selected board** (under **Destination**), which is on by default; turn it off to keep sending results to the current destination board, which then becomes the **Auto** board. The full settings also include the sort order and the board visibility and sorting options from the [Filter & sort boards](/users-guide/gallery/introduction/#finding-filtering-and-sorting-boards) menu. All Gallery settings are saved with the project.

## Keyboard shortcuts

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ The Shape and Select buttons hold several tools. Click to use the current one, o
### Tool options

* **Brush** — **Size**, **Opacity** and **Hardness**; the **Foreground** color; and, under **Dynamics**, whether pen pressure affects the width or the opacity. **[** and **]** make the brush smaller or larger, and so does **Ctrl**+scroll.
* **Eraser** — **Size**, **Opacity** and **Hardness**.
* **Eraser** — **Size**, **Opacity** and **Hardness**; and, under **Dynamics**, whether pen pressure affects the width. Pressure never changes how strongly the eraser erases.
* **Shape** — the **Kind** of shape; **Place on**, which draws on the **Selected layer** or on a **New layer** of its own; **Fill** and **Stroke**, each with a color; and the stroke **Width**. **Shift** constrains the proportions. For polygons, click to place points and double-click, press **Enter** or click the first point to close the shape. With an inpaint mask or regional guidance layer selected, the shape is added to the mask or region, which makes shapes a quick way to mark rectangular or geometric areas; the colors are ignored.
* **Gradient** — **Kind** (Linear or Radial), **Angle**, and the color **Stops**, which can be added and removed or follow the foreground and background colors. Dragging creates a gradient layer the size of the generation frame, or repositions the selected gradient layer.
* **Text** — the font **Family**, **Style** or **Weight**, **Size**, **Line height**, **Align** and **Fill** color. Click the canvas to start typing; **Ctrl+Enter** or clicking elsewhere finishes the text, and **Escape** cancels it. The text stays editable as a text layer.
Expand Down Expand Up @@ -212,7 +212,8 @@ These shortcuts work while the canvas has focus. On macOS, use **Cmd** in place
| **Ctrl+J** / **Ctrl+E** | Duplicate layer / merge layer down |
| **Ctrl+G** / **Ctrl+Shift+G** | Group / ungroup layers |
| **Ctrl+]** / **Ctrl+[** (add **Shift** for front / back) | Move layer forward / backward |
| **Alt+[** / **Alt+]** | Previous / next layer, or staged result |
| **Alt+[** / **Alt+]** | Previous / next layer (wrapping, in the order of the Layers panel), or staged result while results are staged |
| **Q** | Switch to the layer bookmarked with **Bookmark for quick switch** in its menu, or back to the previously selected layer |
| **Shift+H** | Hide control, guidance and mask layers |
| **Shift+N** / **Shift+B** | Fit the generation frame to the layers / to the masks |
| **Enter** / **Escape** | Apply / cancel a transform, text or filter |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ The icons at the top **Arrange** the layer (move it to the front, forward, backw
* **Merge down**.
* **Copy to** — including **Copy layer to clipboard**, and copies to other layer types.
* **Crop layer to bbox** and **Save layer to uploads** (save it to the Gallery's Uploads).
* **Disable layer**, **Lock layer** and **Color label**.
* **Disable layer**, **Lock layer**, **Bookmark for quick switch** and **Color label**. A bookmarked layer is where **Q** switches on the canvas; press **Q** again to return. The bookmark lasts for the session.
* **Delete layer**.
* **Raster layers:**
* **Transform**.
Expand Down
13 changes: 12 additions & 1 deletion invokeai/frontend/webv2/public/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -2313,8 +2313,15 @@
"copyValue": "Copy value",
"couldNotExpandPrompt": "Could not expand the prompt.",
"couldNotGeneratePromptFromImage": "Could not generate a prompt from the selected image.",
"cropActualSize": "Actual size",
"cropAspectRatioFree": "Free",
"cropFitView": "Fit image",
"cropReferenceImage": "Crop reference image",
"cropReferenceImageHelp": "Drag the crop box to move it, or drag its handles to resize. Apply to create a cropped reference image.",
"cropReferenceImageHelp": "Drag the crop box to move it, or drag its handles to resize. Scroll to zoom and hold Space to drag the view. Apply to create a cropped reference image.",
"cropSaveToAssets": "Save to Uploads",
"cropSavedToAssets": "The crop was saved to Uploads.",
"cropZoomIn": "Zoom in",
"cropZoomOut": "Zoom out",
"deletePresetNamed": "Delete preset {{name}}",
"deletePromptHistoryItem": "Delete prompt history item",
"denoisingStrength": "Denoising strength",
Expand Down Expand Up @@ -2957,6 +2964,7 @@
"nudgeUpLarge": "Nudge layer up 10px",
"pasteImage": "Paste image",
"previousEntity": "Previous canvas entity",
"quickSwitch": "Quick switch to bookmarked or previous layer",
"redo": "Redo canvas edit",
"reorderLayer": "Reorder layer",
"resetSelected": "Clear selected mask",
Expand Down Expand Up @@ -3390,6 +3398,7 @@
"addRegionalReferenceImage": "Regional reference image",
"actionFailed": "Layer action failed",
"blendMode": "Blend mode",
"bookmarkForQuickSwitch": "Bookmark for quick switch",
"busy": "The canvas is busy. Try again when the current action finishes.",
"copyFailed": "The layer could not be copied.",
"copyLayerToClipboard": "Copy layer to clipboard",
Expand Down Expand Up @@ -3448,6 +3457,7 @@
"overBudget": "There is not enough canvas memory to make this change undoable, so nothing was changed.",
"stale": "The layer changed before this action finished, so nothing was changed. Try again.",
"rasterize": "Rasterize layer",
"removeQuickSwitchBookmark": "Remove quick switch bookmark",
"rename": "Rename layer",
"reorder": "Reorder layer",
"runWorkflow": "Run workflow",
Expand Down Expand Up @@ -4098,6 +4108,7 @@
"imageSize": "Image size",
"imageSort": "Image Sort",
"infinite": "Infinite",
"middleClickOpensInNewTab": "Middle-click opens media in a new tab",
"itemActions": {
"loadWorkflow": {
"editorUnavailable": "The workflow editor is not available in this layout.",
Expand Down
8 changes: 6 additions & 2 deletions invokeai/frontend/webv2/src/app/GenerationUiAdapter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { QueueItem, QueueItemReadModel } from '@features/queue/contracts';
import type { ReadableExternalStore } from '@platform/state/projectedExternalStore';
import type { ReactNode } from 'react';

import { getSelectedGalleryImageFromValues } from '@features/gallery/contracts';
import { getGalleryAutoAddBoardId, getSelectedGalleryImageFromValues } from '@features/gallery/contracts';
import { invalidateGallery } from '@features/gallery/queries';
import { galleryImageUrls } from '@features/gallery/utility';
import { GenerationUiProvider } from '@features/generation/react';
Expand Down Expand Up @@ -232,13 +232,17 @@ export const GenerationUiAdapterProvider = ({ children }: { children: ReactNode
(imageName) => findGalleryItem({ kind: 'image', name: imageName }),
[findGalleryItem]
);
const uploadBoardId = useActiveProjectSelector(
(activeProject) => getGalleryAutoAddBoardId(getProjectWidgetValues(activeProject, 'gallery')) ?? 'none'
);
const galleryGroup = useMemo<GenerationUiAdapter['gallery']>(
() => ({
findImage,
selectedImage: selectedGalleryImage,
touchImages: () => void invalidateGallery(queryClient),
uploadBoardId,
}),
[findImage, queryClient, selectedGalleryImage]
[findImage, queryClient, selectedGalleryImage, uploadBoardId]
);
const openInModelManager = useOpenModelInManager();
const modelsGroup = useMemo<GenerationUiAdapter['models']>(
Expand Down
4 changes: 4 additions & 0 deletions invokeai/frontend/webv2/src/features/gallery/core/settings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@ export interface GallerySettings {
collapsedBoardSections: GalleryBoardSectionId[];
imageDensityPercent: number;
imageOrderDir: GalleryOrderDir;
/** Opt-in: a middle click on a tile opens the media in a new browser tab instead of the browser default. */
middleClickOpensInNewTab: boolean;
paginationMode: GalleryPaginationMode;
showArchivedBoards: boolean;
showDateBoards: boolean;
Expand All @@ -58,6 +60,7 @@ export const DEFAULT_GALLERY_SETTINGS: GallerySettings = {
collapsedBoardSections: [],
imageDensityPercent: 50,
imageOrderDir: 'DESC',
middleClickOpensInNewTab: false,
paginationMode: 'infinite',
showArchivedBoards: false,
showDateBoards: false,
Expand Down Expand Up @@ -122,6 +125,7 @@ export const getGallerySettings = (values: Record<string, unknown>): GallerySett
? Math.min(100, Math.max(0, values.imageDensityPercent))
: DEFAULT_GALLERY_SETTINGS.imageDensityPercent,
imageOrderDir: isOrderDir(values.imageOrderDir) ? values.imageOrderDir : DEFAULT_GALLERY_SETTINGS.imageOrderDir,
middleClickOpensInNewTab: values.middleClickOpensInNewTab === true,
paginationMode,
showArchivedBoards:
typeof values.showArchivedBoards === 'boolean'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,14 @@ export const gallerySettingsContribution: SettingsContribution = {
keywords: 'queue progress placeholders',
scope: 'instance',
},
{
id: 'middleClickOpensInNewTab',
kind: 'boolean',
label: (t) => t('widgets.gallery.middleClickOpensInNewTab'),
group: (t) => t('widgets.gallery.settingsThumbnails'),
keywords: 'mouse middle click wheel button open new tab',
scope: 'instance',
},
{
id: 'paginationMode',
kind: 'select',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1227,6 +1227,28 @@ describe('GalleryImageGrid mixed item cells', () => {
expect(imageActionMocks.openItemInPreview).not.toHaveBeenCalled();
});

it('opens a middle-clicked tile in a new tab only when the Gallery setting opts in', async () => {
const auxclick = (button: HTMLButtonElement) =>
interact(() => {
button.dispatchEvent(new MouseEvent('mousedown', { bubbles: true, button: 1, cancelable: true }));
button.dispatchEvent(new MouseEvent('auxclick', { bubbles: true, button: 1, cancelable: true }));
});

await renderGallery();
await auxclick(getButton('Select last.png for preview'));
expect(imageActionMocks.openItemInNewTab).not.toHaveBeenCalled();

const gallery = createGallery();
await renderGallery({ ...gallery, settings: { ...gallery.settings, middleClickOpensInNewTab: true } });
await auxclick(getButton('Select last.png for preview'));
expect(imageActionMocks.openItemInNewTab).toHaveBeenCalledExactlyOnceWith(createItem('image', 'last.png'));
await auxclick(getButton('Select video shared, duration 1:06, for preview'));
expect(imageActionMocks.openItemInNewTab).toHaveBeenLastCalledWith(createItem('video', 'shared'));
// The middle button never selects or opens in Preview.
expect(actionMocks.selectItem).not.toHaveBeenCalled();
expect(imageActionMocks.openItemInPreview).not.toHaveBeenCalled();
});

it('toggles the star on each click of a double-click without selecting or opening the item', async () => {
await renderGallery();
await act(() => userEvent.dblClick(getButton('Star first.png')));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -431,6 +431,7 @@ export const GalleryImageGrid = () => {
const viewportRef = useRef<HTMLDivElement | null>(null);
const {
imageDensityPercent,
middleClickOpensInNewTab,
paginationMode,
progressSectionCollapsed,
showImageDimensions,
Expand Down Expand Up @@ -1654,6 +1655,7 @@ export const GalleryImageGrid = () => {
[itemActions]
);
const handleOpenItem = useCallback((item: GalleryItem) => itemActions.openItemInPreview(item), [itemActions]);
const handleOpenItemInNewTab = useCallback((item: GalleryItem) => itemActions.openItemInNewTab(item), [itemActions]);

// Releasing the anchor puts the window back over the top of the listing.
const handleReturnToBoardTop = useCallback(() => galleryCommands.setPage(0), [galleryCommands]);
Expand Down Expand Up @@ -1690,6 +1692,7 @@ export const GalleryImageGrid = () => {
onContextMenu={handleThumbnailContextMenu}
onFocusLost={restoreTileFocus}
onOpen={handleOpenItem}
onOpenInNewTab={middleClickOpensInNewTab ? handleOpenItemInNewTab : null}
onToggleStarred={handleToggleStarred}
selectionPage={selectionPage}
/>
Expand All @@ -1701,11 +1704,13 @@ export const GalleryImageGrid = () => {
getDragItems,
getReadyItemLabel,
handleOpenItem,
handleOpenItemInNewTab,
handleThumbnailClick,
handleThumbnailContextMenu,
handleToggleStarred,
isComparisonActive,
isFollowingLive,
middleClickOpensInNewTab,
region,
restoreTileFocus,
selectedItemKeys,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ let host: HTMLDivElement | null = null;
let root: Root | null = null;
const STALE_TARGET = { projectId: 'old-project' };
const PICKED_BOARD_VALUES = { selectedBoardId: 'picked' };
const NO_VALUES = {};
(globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT: boolean }).IS_REACT_ACT_ENVIRONMENT = true;

const Harness = ({
Expand Down Expand Up @@ -105,6 +106,18 @@ describe('Gallery settings contribution bindings', () => {
expect(host?.querySelector('output')?.textContent).toContain('"showPendingItems":false');
});

it('binds the opt-in middle-click setting, off by default, through the settings command', async () => {
await act(() => root?.render(<Harness id="middleClickOpensInNewTab" initialValues={NO_VALUES} />));
const quick = host?.querySelector<HTMLInputElement>('[data-surface="quick"] input');
const dialog = host?.querySelector<HTMLInputElement>('[data-surface="dialog"] input');

expect(quick?.checked).toBe(false);
await act(() => dialog?.click());

expect(quick?.checked).toBe(true);
expect(host?.querySelector('output')?.textContent).toContain('"middleClickOpensInNewTab":true');
});

it('pins the current destination when results stop following the selected board', async () => {
await act(() => root?.render(<Harness id="followSelectedBoard" initialValues={PICKED_BOARD_VALUES} />));
const toggle = host?.querySelector<HTMLInputElement>('[data-surface="quick"] input');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ export const GallerySettingField = ({ field, surface, target }: SettingFieldProp
} else if (
(field.id === 'showImageDimensions' ||
field.id === 'showPendingItems' ||
field.id === 'middleClickOpensInNewTab' ||
field.id === 'showDateBoards' ||
field.id === 'showArchivedBoards' ||
field.id === 'showOtherProjectBoards') &&
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ const GalleryThumbnail = ({
onContextMenu,
onFocusLost,
onOpen,
onOpenInNewTab,
onToggleStarred,
}: {
alwaysShowDimensions: boolean;
Expand All @@ -108,6 +109,8 @@ const GalleryThumbnail = ({
onFocusLost: (itemKey: GalleryItemKey) => void;
/** Double-click or Enter: show the item in the centre Preview. */
onOpen: (item: GalleryItem) => void;
/** Middle click, when the Gallery setting opts in; null leaves the browser's middle-click behaviour alone. */
onOpenInNewTab: ((item: GalleryItem) => void) | null;
onToggleStarred: (item: GalleryItem) => void;
}) => {
const { t } = useTranslation();
Expand Down Expand Up @@ -232,6 +235,27 @@ const GalleryThumbnail = ({
[item, onOpen]
);

// Middle button only; the primary button's click/double-click stay selection and open. Its mousedown default
// (autoscroll on some platforms) is suppressed so the release reliably delivers the auxclick.
const handleMiddleMouseDown = useCallback(
(event: MouseEvent<HTMLButtonElement>) => {
if (onOpenInNewTab && event.button === 1) {
event.preventDefault();
}
},
[onOpenInNewTab]
);
const handleAuxClick = useCallback(
(event: MouseEvent<HTMLButtonElement>) => {
if (onOpenInNewTab && event.button === 1) {
event.preventDefault();
event.stopPropagation();
onOpenInNewTab(item);
}
},
[item, onOpenInNewTab]
);

// Enter opens and Space selects; neither starts a keyboard drag. Enter's default would click (select) first.
const handleActivationKeyDown = useCallback(
(event: KeyboardEvent<HTMLButtonElement>) => {
Expand Down Expand Up @@ -301,9 +325,11 @@ const GalleryThumbnail = ({
style={THUMBNAIL_BUTTON_STYLE}
tabIndex={isTabStop ? 0 : -1}
type="button"
onAuxClick={handleAuxClick}
onClick={handleClick}
onDoubleClick={handleDoubleClick}
onKeyDown={handleActivationKeyDown}
onMouseDown={handleMiddleMouseDown}
>
<img
alt={item.name}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@ describe('gallery state view', () => {
collapsedBoardSections: [],
imageDensityPercent: 50,
imageOrderDir: 'DESC',
middleClickOpensInNewTab: false,
paginationMode: 'infinite',
showArchivedBoards: false,
showDateBoards: false,
Expand Down
Loading
Loading