mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-29 09:28:12 +08:00
Improve 'add node' menu (#1312)
* improved node adding list with dropdowns and scrolling * changed arrow icon from default details arrow * made 'add node' menu appear above mouse when clicking at bottom of nodegraph. Searching automatically opens the dropdowns you need. Set fixed 'add node' menu size * updated code style to be more clear * undo mistake changes --------- Co-authored-by: 0HyperCube <78500760+0HyperCube@users.noreply.github.com>
This commit is contained in:
co-authored by
0HyperCube
parent
7f2347f842
commit
30fc9d065b
@@ -18,6 +18,8 @@
|
|||||||
const WHEEL_RATE = (1 / 600) * 3;
|
const WHEEL_RATE = (1 / 600) * 3;
|
||||||
const GRID_COLLAPSE_SPACING = 10;
|
const GRID_COLLAPSE_SPACING = 10;
|
||||||
const GRID_SIZE = 24;
|
const GRID_SIZE = 24;
|
||||||
|
const ADD_NODE_MENU_WIDTH = 180;
|
||||||
|
const ADD_NODE_MENU_HEIGHT = 200;
|
||||||
|
|
||||||
const editor = getContext<Editor>("editor");
|
const editor = getContext<Editor>("editor");
|
||||||
const nodeGraph = getContext<NodeGraphState>("nodeGraph");
|
const nodeGraph = getContext<NodeGraphState>("nodeGraph");
|
||||||
@@ -46,6 +48,19 @@
|
|||||||
$: nodeCategories = buildNodeCategories($nodeGraph.nodeTypes, searchTerm);
|
$: nodeCategories = buildNodeCategories($nodeGraph.nodeTypes, searchTerm);
|
||||||
$: nodeListX = ((nodeListLocation?.x || 0) * GRID_SIZE + transform.x) * transform.scale;
|
$: nodeListX = ((nodeListLocation?.x || 0) * GRID_SIZE + transform.x) * transform.scale;
|
||||||
$: nodeListY = ((nodeListLocation?.y || 0) * GRID_SIZE + transform.y) * transform.scale;
|
$: nodeListY = ((nodeListLocation?.y || 0) * GRID_SIZE + transform.y) * transform.scale;
|
||||||
|
|
||||||
|
let appearAboveMouse = false;
|
||||||
|
let appearRightOfMouse = false;
|
||||||
|
|
||||||
|
$: (() => {
|
||||||
|
const bounds = graph?.div()?.getBoundingClientRect();
|
||||||
|
if (!bounds) return;
|
||||||
|
const { width, height } = bounds;
|
||||||
|
|
||||||
|
appearRightOfMouse = nodeListX > width - ADD_NODE_MENU_WIDTH / 2;
|
||||||
|
appearAboveMouse = nodeListY > height - ADD_NODE_MENU_HEIGHT / 2;
|
||||||
|
})();
|
||||||
|
|
||||||
$: linkPathInProgress = createLinkPathInProgress(linkInProgressFromConnector, linkInProgressToConnector);
|
$: linkPathInProgress = createLinkPathInProgress(linkInProgressFromConnector, linkInProgressToConnector);
|
||||||
$: linkPaths = createLinkPaths(linkPathInProgress, nodeLinkPaths);
|
$: linkPaths = createLinkPaths(linkPathInProgress, nodeLinkPaths);
|
||||||
|
|
||||||
@@ -60,16 +75,35 @@
|
|||||||
return sparse;
|
return sparse;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildNodeCategories(nodeTypes: FrontendNodeType[], searchTerm: string): [string, FrontendNodeType[]][] {
|
type NodeCategoryDetails = {
|
||||||
const categories = new Map();
|
nodes: FrontendNodeType[];
|
||||||
|
open: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
function buildNodeCategories(nodeTypes: FrontendNodeType[], searchTerm: string): [string, NodeCategoryDetails][] {
|
||||||
|
const categories = new Map<string, NodeCategoryDetails>();
|
||||||
|
|
||||||
nodeTypes.forEach((node) => {
|
nodeTypes.forEach((node) => {
|
||||||
if (searchTerm.length > 0 && !node.name.toLowerCase().includes(searchTerm.toLowerCase()) && !node.category.toLowerCase().includes(searchTerm.toLowerCase())) {
|
const nameIncludesSearchTerm = node.name.toLowerCase().includes(searchTerm.toLowerCase());
|
||||||
|
|
||||||
|
if (searchTerm.length > 0 && !nameIncludesSearchTerm && !node.category.toLowerCase().includes(searchTerm.toLowerCase())) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const category = categories.get(node.category);
|
const category = categories.get(node.category);
|
||||||
if (category) category.push(node);
|
let open = nameIncludesSearchTerm;
|
||||||
else categories.set(node.category, [node]);
|
if (searchTerm.length === 0) {
|
||||||
|
open = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (category) {
|
||||||
|
category.open = open;
|
||||||
|
category.nodes.push(node);
|
||||||
|
} else
|
||||||
|
categories.set(node.category, {
|
||||||
|
open: open,
|
||||||
|
nodes: [node],
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
return Array.from(categories);
|
return Array.from(categories);
|
||||||
@@ -497,18 +531,32 @@
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{#if nodeListLocation}
|
{#if nodeListLocation}
|
||||||
<LayoutCol class="node-list" data-node-list styles={{ "margin-left": `${nodeListX}px`, "margin-top": `${nodeListY}px` }}>
|
<LayoutCol
|
||||||
|
class="node-list"
|
||||||
|
data-node-list
|
||||||
|
styles={{
|
||||||
|
left: `${nodeListX}px`,
|
||||||
|
top: `${nodeListY}px`,
|
||||||
|
transform: `translate(${appearRightOfMouse ? -100 : 0}%, ${appearAboveMouse ? -100 : 0}%)`,
|
||||||
|
width: `${ADD_NODE_MENU_WIDTH}px`,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<TextInput placeholder="Search Nodes..." value={searchTerm} on:value={({ detail }) => (searchTerm = detail)} bind:this={nodeSearchInput} />
|
<TextInput placeholder="Search Nodes..." value={searchTerm} on:value={({ detail }) => (searchTerm = detail)} bind:this={nodeSearchInput} />
|
||||||
{#each nodeCategories as nodeCategory}
|
<div class="list-nodes" style={`height: ${ADD_NODE_MENU_HEIGHT}px;`} on:wheel|stopPropagation>
|
||||||
<LayoutCol>
|
{#each nodeCategories as nodeCategory}
|
||||||
<TextLabel>{nodeCategory[0]}</TextLabel>
|
<details style="display: flex; flex-direction: column;" open={nodeCategory[1].open}>
|
||||||
{#each nodeCategory[1] as nodeType}
|
<summary>
|
||||||
<TextButton label={nodeType.name} action={() => createNode(nodeType.name)} />
|
<IconLabel icon="DropdownArrow" />
|
||||||
{/each}
|
<TextLabel>{nodeCategory[0]}</TextLabel>
|
||||||
</LayoutCol>
|
</summary>
|
||||||
{:else}
|
{#each nodeCategory[1].nodes as nodeType}
|
||||||
<TextLabel>No search results</TextLabel>
|
<TextButton label={nodeType.name} action={() => createNode(nodeType.name)} />
|
||||||
{/each}
|
{/each}
|
||||||
|
</details>
|
||||||
|
{:else}
|
||||||
|
<div style="margin-right: 4px;"><TextLabel>No search results</TextLabel></div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
</LayoutCol>
|
</LayoutCol>
|
||||||
{/if}
|
{/if}
|
||||||
<div class="nodes" style:transform={`scale(${transform.scale}) translate(${transform.x}px, ${transform.y}px)`} style:transform-origin={`0 0`} bind:this={nodesContainer}>
|
<div class="nodes" style:transform={`scale(${transform.scale}) translate(${transform.x}px, ${transform.y}px)`} style:transform-origin={`0 0`} bind:this={nodesContainer}>
|
||||||
@@ -607,12 +655,45 @@
|
|||||||
|
|
||||||
.node-list {
|
.node-list {
|
||||||
width: max-content;
|
width: max-content;
|
||||||
position: fixed;
|
position: absolute;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
z-index: 3;
|
z-index: 3;
|
||||||
background-color: var(--color-3-darkgray);
|
background-color: var(--color-3-darkgray);
|
||||||
|
|
||||||
|
.text-button {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-nodes {
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
details {
|
||||||
|
margin-right: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
summary {
|
||||||
|
list-style-type: none;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 2px;
|
||||||
|
|
||||||
|
span {
|
||||||
|
white-space: break-spaces;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
details summary svg {
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
details[open] summary svg {
|
||||||
|
transform: rotate(0deg);
|
||||||
|
}
|
||||||
|
|
||||||
.text-button + .text-button {
|
.text-button + .text-button {
|
||||||
|
display: block;
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
margin-top: 4px;
|
margin-top: 4px;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,7 +42,6 @@ export class UpdateNodeGraphSelection extends JsMessage {
|
|||||||
readonly selected!: bigint[];
|
readonly selected!: bigint[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export class UpdateOpenDocumentsList extends JsMessage {
|
export class UpdateOpenDocumentsList extends JsMessage {
|
||||||
@Type(() => FrontendDocumentDetails)
|
@Type(() => FrontendDocumentDetails)
|
||||||
readonly openDocuments!: FrontendDocumentDetails[];
|
readonly openDocuments!: FrontendDocumentDetails[];
|
||||||
@@ -493,22 +492,22 @@ const mouseCursorIconCSSNames = {
|
|||||||
Rotate: "custom-rotate",
|
Rotate: "custom-rotate",
|
||||||
} as const;
|
} as const;
|
||||||
export type MouseCursor = keyof typeof mouseCursorIconCSSNames;
|
export type MouseCursor = keyof typeof mouseCursorIconCSSNames;
|
||||||
export type MouseCursorIcon = typeof mouseCursorIconCSSNames[MouseCursor];
|
export type MouseCursorIcon = (typeof mouseCursorIconCSSNames)[MouseCursor];
|
||||||
|
|
||||||
export class UpdateMouseCursor extends JsMessage {
|
export class UpdateMouseCursor extends JsMessage {
|
||||||
@Transform(({ value }: { value: MouseCursor }) => mouseCursorIconCSSNames[value] || "alias")
|
@Transform(({ value }: { value: MouseCursor }) => mouseCursorIconCSSNames[value] || "alias")
|
||||||
readonly cursor!: MouseCursorIcon;
|
readonly cursor!: MouseCursorIcon;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class TriggerLoadAutoSaveDocuments extends JsMessage { }
|
export class TriggerLoadAutoSaveDocuments extends JsMessage {}
|
||||||
|
|
||||||
export class TriggerLoadPreferences extends JsMessage { }
|
export class TriggerLoadPreferences extends JsMessage {}
|
||||||
|
|
||||||
export class TriggerOpenDocument extends JsMessage { }
|
export class TriggerOpenDocument extends JsMessage {}
|
||||||
|
|
||||||
export class TriggerImport extends JsMessage { }
|
export class TriggerImport extends JsMessage {}
|
||||||
|
|
||||||
export class TriggerPaste extends JsMessage { }
|
export class TriggerPaste extends JsMessage {}
|
||||||
|
|
||||||
export class TriggerCopyToClipboardBlobUrl extends JsMessage {
|
export class TriggerCopyToClipboardBlobUrl extends JsMessage {
|
||||||
readonly blobUrl!: string;
|
readonly blobUrl!: string;
|
||||||
@@ -547,7 +546,7 @@ export class TriggerRasterizeRegionBelowLayer extends JsMessage {
|
|||||||
readonly size!: [number, number];
|
readonly size!: [number, number];
|
||||||
}
|
}
|
||||||
|
|
||||||
export class TriggerRefreshBoundsOfViewports extends JsMessage { }
|
export class TriggerRefreshBoundsOfViewports extends JsMessage {}
|
||||||
|
|
||||||
export class TriggerRevokeBlobUrl extends JsMessage {
|
export class TriggerRevokeBlobUrl extends JsMessage {
|
||||||
readonly url!: string;
|
readonly url!: string;
|
||||||
@@ -557,7 +556,7 @@ export class TriggerSavePreferences extends JsMessage {
|
|||||||
readonly preferences!: Record<string, unknown>;
|
readonly preferences!: Record<string, unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DocumentChanged extends JsMessage { }
|
export class DocumentChanged extends JsMessage {}
|
||||||
|
|
||||||
export class UpdateDocumentLayerTreeStructureJs extends JsMessage {
|
export class UpdateDocumentLayerTreeStructureJs extends JsMessage {
|
||||||
constructor(readonly layerId: bigint, readonly children: UpdateDocumentLayerTreeStructureJs[]) {
|
constructor(readonly layerId: bigint, readonly children: UpdateDocumentLayerTreeStructureJs[]) {
|
||||||
@@ -650,7 +649,7 @@ export class UpdateImageData extends JsMessage {
|
|||||||
readonly imageData!: ImaginateImageData[];
|
readonly imageData!: ImaginateImageData[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DisplayRemoveEditableTextbox extends JsMessage { }
|
export class DisplayRemoveEditableTextbox extends JsMessage {}
|
||||||
|
|
||||||
export class UpdateDocumentLayerDetails extends JsMessage {
|
export class UpdateDocumentLayerDetails extends JsMessage {
|
||||||
@Type(() => LayerPanelEntry)
|
@Type(() => LayerPanelEntry)
|
||||||
@@ -701,7 +700,7 @@ export class ImaginateImageData {
|
|||||||
readonly transform!: Float64Array;
|
readonly transform!: Float64Array;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class DisplayDialogDismiss extends JsMessage { }
|
export class DisplayDialogDismiss extends JsMessage {}
|
||||||
|
|
||||||
export class Font {
|
export class Font {
|
||||||
fontFamily!: string;
|
fontFamily!: string;
|
||||||
@@ -720,7 +719,7 @@ export class TriggerVisitLink extends JsMessage {
|
|||||||
url!: string;
|
url!: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class TriggerTextCommit extends JsMessage { }
|
export class TriggerTextCommit extends JsMessage {}
|
||||||
|
|
||||||
export class TriggerTextCopy extends JsMessage {
|
export class TriggerTextCopy extends JsMessage {
|
||||||
readonly copyText!: string;
|
readonly copyText!: string;
|
||||||
@@ -730,7 +729,7 @@ export class TriggerAboutGraphiteLocalizedCommitDate extends JsMessage {
|
|||||||
readonly commitDate!: string;
|
readonly commitDate!: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class TriggerViewportResize extends JsMessage { }
|
export class TriggerViewportResize extends JsMessage {}
|
||||||
|
|
||||||
// WIDGET PROPS
|
// WIDGET PROPS
|
||||||
|
|
||||||
@@ -1098,13 +1097,13 @@ const widgetSubTypes = [
|
|||||||
{ value: TextLabel, name: "TextLabel" },
|
{ value: TextLabel, name: "TextLabel" },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
type WidgetSubTypes = typeof widgetSubTypes[number];
|
type WidgetSubTypes = (typeof widgetSubTypes)[number];
|
||||||
type WidgetKindMap = { [T in WidgetSubTypes as T["name"]]: InstanceType<T["value"]> };
|
type WidgetKindMap = { [T in WidgetSubTypes as T["name"]]: InstanceType<T["value"]> };
|
||||||
export type WidgetPropsNames = keyof WidgetKindMap;
|
export type WidgetPropsNames = keyof WidgetKindMap;
|
||||||
export type WidgetPropsSet = WidgetKindMap[WidgetPropsNames];
|
export type WidgetPropsSet = WidgetKindMap[WidgetPropsNames];
|
||||||
|
|
||||||
export function narrowWidgetProps<K extends WidgetPropsNames>(props: WidgetPropsSet, kind: K) {
|
export function narrowWidgetProps<K extends WidgetPropsNames>(props: WidgetPropsSet, kind: K) {
|
||||||
if (props.kind === kind) return props as WidgetKindMap[K]
|
if (props.kind === kind) return props as WidgetKindMap[K];
|
||||||
else return undefined;
|
else return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1258,25 +1257,25 @@ function createLayoutGroup(layoutGroup: any): LayoutGroup {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// WIDGET LAYOUTS
|
// WIDGET LAYOUTS
|
||||||
export class UpdateDialogDetails extends WidgetDiffUpdate { }
|
export class UpdateDialogDetails extends WidgetDiffUpdate {}
|
||||||
|
|
||||||
export class UpdateDocumentModeLayout extends WidgetDiffUpdate { }
|
export class UpdateDocumentModeLayout extends WidgetDiffUpdate {}
|
||||||
|
|
||||||
export class UpdateToolOptionsLayout extends WidgetDiffUpdate { }
|
export class UpdateToolOptionsLayout extends WidgetDiffUpdate {}
|
||||||
|
|
||||||
export class UpdateDocumentBarLayout extends WidgetDiffUpdate { }
|
export class UpdateDocumentBarLayout extends WidgetDiffUpdate {}
|
||||||
|
|
||||||
export class UpdateToolShelfLayout extends WidgetDiffUpdate { }
|
export class UpdateToolShelfLayout extends WidgetDiffUpdate {}
|
||||||
|
|
||||||
export class UpdateWorkingColorsLayout extends WidgetDiffUpdate { }
|
export class UpdateWorkingColorsLayout extends WidgetDiffUpdate {}
|
||||||
|
|
||||||
export class UpdatePropertyPanelOptionsLayout extends WidgetDiffUpdate { }
|
export class UpdatePropertyPanelOptionsLayout extends WidgetDiffUpdate {}
|
||||||
|
|
||||||
export class UpdatePropertyPanelSectionsLayout extends WidgetDiffUpdate { }
|
export class UpdatePropertyPanelSectionsLayout extends WidgetDiffUpdate {}
|
||||||
|
|
||||||
export class UpdateLayerTreeOptionsLayout extends WidgetDiffUpdate { }
|
export class UpdateLayerTreeOptionsLayout extends WidgetDiffUpdate {}
|
||||||
|
|
||||||
export class UpdateNodeGraphBarLayout extends WidgetDiffUpdate { }
|
export class UpdateNodeGraphBarLayout extends WidgetDiffUpdate {}
|
||||||
|
|
||||||
export class UpdateMenuBarLayout extends JsMessage {
|
export class UpdateMenuBarLayout extends JsMessage {
|
||||||
layoutTarget!: unknown;
|
layoutTarget!: unknown;
|
||||||
|
|||||||
Reference in New Issue
Block a user