Selaa lähdekoodia

[shgraph] Replaced add menu with new context menu, reorganised nodes categories

Clément Espeute 10 kuukautta sitten
vanhempi
commit
bb01ec7ce8

+ 22 - 4
bin/style.css

@@ -3576,18 +3576,32 @@ hide-popover hide-content {
   right: 5px;
   margin: 0;
 }
-.context-menu2 input {
+.context-menu2 .search-bar {
   background-color: #444;
   border: 1px solid #666;
   border-radius: 2px;
+  box-sizing: border-box;
+  position: relative;
+}
+.context-menu2 .search-bar input {
   color: #aaaaaa;
   font-family: Verdana, Geneva, Tahoma, sans-serif;
   width: 100%;
   box-sizing: border-box;
+  border: none;
+  margin: none;
 }
-.context-menu2 input:focus {
+.context-menu2 .search-bar input:focus {
   outline: none;
 }
+.context-menu2 .search-bar:after {
+  color: #aaaaaa;
+  content: "\f002";
+  font-family: "FontAwesome";
+  position: absolute;
+  right: 6px;
+  top: 2px;
+}
 .context-menu2 menu {
   max-height: min(500px, 90vh);
   overflow-y: auto;
@@ -3619,13 +3633,17 @@ hide-popover hide-content {
 .context-menu2 menu li {
   text-align: left;
   --level: 0;
+  --base-padding: 0.2em;
   padding: calc(0.2em);
-  padding-left: calc(0.2em + var(--level) * 0.5em);
+  padding-left: calc(var(--base-padding) + var(--level) * 0.5em);
   display: flex;
   align-items: baseline;
   gap: 4px;
   user-select: none;
 }
+.context-menu2 menu li:not(:has(.icon)) {
+  --base-padding: 0.5em;
+}
 .context-menu2 menu li:hover,
 .context-menu2 menu li.highlight {
   background-color: #555555;
@@ -3644,7 +3662,7 @@ hide-popover hide-content {
 .context-menu2 menu li .icon {
   display: flex;
   justify-content: center;
-  width: 1.5em;
+  width: 1.4em;
 }
 .context-menu2 menu li .shortcut {
   color: #888;

+ 31 - 9
bin/style.less

@@ -4178,18 +4178,35 @@ hide-popover {
 		margin: 0;
 	}
 
-	input {
+	.search-bar {
+
 		background-color: #444;
 		border: 1px solid #666;
 		border-radius: 2px;
-
-		color: #aaaaaa;
-		font-family: Verdana, Geneva, Tahoma, sans-serif;
-		width: 100%;
 		box-sizing: border-box;
+		position: relative;
+
+		input {
+			color: #aaaaaa;
+			font-family: Verdana, Geneva, Tahoma, sans-serif;
+			width: 100%;
+			box-sizing: border-box;
 
-		&:focus {
-			outline: none;
+			border: none;
+			margin: none;
+
+			&:focus {
+				outline: none;
+			}
+		}
+
+		&:after {
+			color: #aaaaaa;
+			content: "\f002";
+			font-family: "FontAwesome";
+			position: absolute;
+			right: 6px;
+			top: 2px;
 		}
 	}
 
@@ -4228,11 +4245,16 @@ hide-popover {
 		li {
 			text-align: left;
 			--level: 0;
+			--base-padding: 0.2em;
 			padding: calc(0.2em);
-			padding-left: calc(0.2em + var(--level) * 0.5em);
+			padding-left: calc(var(--base-padding) + var(--level) * 0.5em);
 			display: flex;
 			align-items: baseline;
 
+			&:not(:has(.icon)) {
+				--base-padding: 0.5em;
+			}
+
 			gap: 4px;
 			&:hover, &.highlight {
 				background-color: #555555;
@@ -4253,7 +4275,7 @@ hide-popover {
 			.icon {
 				display: flex;
 				justify-content: center;
-				width: 1.5em;
+				width: 1.4em;
 			}
 
 			.shortcut {

+ 97 - 29
hide/comp/ContextMenu2.hx

@@ -10,12 +10,43 @@ typedef MenuItem = {
     ?icon: String,
     ?keys: String,
     ?checked: Bool,
+    ?tooltip: String
+}
+
+enum SearchMode {
+    /**
+        No search bar or search functionality
+    **/
+    None;
+
+    /**
+        Search bar is hidden, is shown when the user starts typing anything
+    **/
+    Hidden;
+
+    /**
+        Search bar is always visible
+    **/
+    Visible;
+}
+
+typedef MenuOptions = {
+    ?search: SearchMode, // default to Hidden for top level context menus
+    ?widthOverride: Int, // if set, force the width of the first menu
+
+    /**
+        Set this to true if you have no icons/checkmarks in your menu and you want to hide the padding on the left of the entries names
+    **/
+    ?noIcons: Bool,
 }
 
 class ContextMenu2 {
     var rootElement : js.html.Element;
     var menu : js.html.MenuElement;
-    var seachBar : js.html.InputElement;
+    var searchInput : js.html.InputElement;
+    var searchBar : js.html.DivElement;
+
+    var options: MenuOptions;
 
     var items: Array<MenuItem> = [];
 
@@ -33,19 +64,31 @@ class ContextMenu2 {
     var popupTimer: haxe.Timer;
     final openDelayMs = 250;
 
-    public static function fromEvent(e: js.html.MouseEvent, items: Array<MenuItem>) {
-        return new ContextMenu2(cast e.target, null, {x: e.clientX, y: e.clientY}, items, true);
+    public static function fromEvent(e: js.html.MouseEvent, items: Array<MenuItem>, options: MenuOptions = null) {
+        return new ContextMenu2(cast e.target, null, {x: e.clientX, y: e.clientY}, items, options ?? {});
     }
 
-    public static function createFromPoint(x: Float, y: Float , items: Array<MenuItem>) {
-        return new ContextMenu2(null, null, {x:x, y:y}, items, true);
+    public static function createFromPoint(x: Float, y: Float , items: Array<MenuItem>, options: MenuOptions = null) {
+        return new ContextMenu2(null, null, {x:x, y:y}, items, options ?? {});
     }
 
-    function new(parentElement: js.html.Element, parentMenu: ContextMenu2, absPos: {x: Float, y: Float}, items: Array<MenuItem>, wantSearch: Bool) {
+    public static function createDropdown(element: js.html.Element, items: Array<MenuItem>, options: MenuOptions = null) {
+        var rect = element.getBoundingClientRect();
+        options = options ?? {};
+        options.widthOverride = options.widthOverride ?? Std.int(rect.width);
+        return new ContextMenu2(element, null, {x: rect.left, y:rect.bottom}, items, options);
+    }
+
+    function new(parentElement: js.html.Element, parentMenu: ContextMenu2, absPos: {x: Float, y: Float}, items: Array<MenuItem>, options: MenuOptions) {
         this.items = items;
         this.parentMenu = parentMenu;
         originalPos = absPos;
 
+        // Default options values
+        options.search = options.search ?? Hidden;
+
+        this.options = options;
+
         var nearest : js.html.Element = if (parentMenu != null) {
                 parentElement;
             } else if (parentElement != null) {
@@ -59,27 +102,34 @@ class ContextMenu2 {
         nearest.appendChild(rootElement);
 
         rootElement.classList.add("context-menu2");
+        if (options.widthOverride != null)
+            rootElement.style.width = '${options.widthOverride}px';
         untyped rootElement.popover = parentMenu != null ? "manual" : "auto";
         rootElement.style.left = '${0}px';
         rootElement.style.top = '${0}px';
         untyped rootElement.showPopover();
 
         menu = js.Browser.document.createMenuElement();
-        if (wantSearch) {
-            seachBar = js.Browser.document.createInputElement();
-            seachBar.type = "text";
-            seachBar.onkeyup = (e:js.html.KeyboardEvent) -> {
-                if (filter != seachBar.value) {
-                    filter = seachBar.value;
+        if (options.search != None) {
+            searchBar = js.Browser.document.createDivElement();
+            rootElement.appendChild(searchBar);
+            searchBar.classList.add("search-bar");
+
+            searchInput = js.Browser.document.createInputElement();
+            searchInput.type = "text";
+            searchInput.placeholder = "Search ...";
+            searchInput.onkeyup = (e:js.html.KeyboardEvent) -> {
+                if (filter != searchInput.value) {
+                    filter = searchInput.value;
                     refreshMenu();
                 }
             }
 
-            seachBar.onblur = (e) -> {
+            searchInput.onblur = (e) -> {
                 rootElement.focus();
             }
 
-            rootElement.appendChild(seachBar);
+            searchBar.appendChild(searchInput);
         }
         rootElement.appendChild(menu);
 
@@ -94,15 +144,20 @@ class ContextMenu2 {
 
         if (parentMenu == null) {
             rootElement.addEventListener("keydown", onGlobalKeyDown);
-            rootElement.focus();
+            if (options.search == Visible) {
+                searchInput.focus();
+            }
+            else {
+                rootElement.focus();
+            }
         }
     }
 
     function onGlobalKeyDown(e:js.html.KeyboardEvent) {
         if (!handleMovementKeys(e)) {
-            if (seachBar != null) {
-                seachBar.style.display = "block";
-                seachBar.focus();
+            if (searchBar != null) {
+                searchBar.style.display = "block";
+                searchInput.focus();
             }
         }
     }
@@ -185,6 +240,10 @@ class ContextMenu2 {
         return false;
     }
 
+    public dynamic function onClose() {
+
+    }
+
     function refreshMenu() {
         if (popupTimer != null) {
             popupTimer.stop();
@@ -197,8 +256,8 @@ class ContextMenu2 {
             flatItems = [];
             selected = -1;
 
-            if (seachBar != null) {
-                seachBar.style.display = "none";
+            if (searchBar != null && options.search == Hidden) {
+                searchBar.style.display = "none";
             }
 
             filteredItems = null;
@@ -220,7 +279,7 @@ class ContextMenu2 {
             filteredItems = [];
             flatItems = [];
 
-            seachBar.style.display = "block";
+            searchBar.style.display = "block";
 
             closeSubmenu();
 
@@ -309,16 +368,23 @@ class ContextMenu2 {
     function createItem(menuItem: MenuItem, id: Int) : js.html.Element {
         var li = js.Browser.document.createLIElement();
 
-        var icon = js.Browser.document.createSpanElement();
-        li.appendChild(icon);
-        icon.classList.add("icon");
-        if (menuItem.icon != null) {
-            icon.classList.add("fa");
-            icon.classList.add('fa-${menuItem.icon}');
+        var icon = null;
+        if (options.noIcons == null || options.noIcons == false) {
+            icon = js.Browser.document.createSpanElement();
+            li.appendChild(icon);
+            icon.classList.add("icon");
+            if (menuItem.icon != null) {
+                icon.classList.add("fa");
+                icon.classList.add('fa-${menuItem.icon}');
+            }
+        }
+
+        if (menuItem.tooltip != null) {
+            li.title = menuItem.tooltip;
         }
 
         function refreshCheck() {
-            if (menuItem.checked != null) {
+            if (icon != null && menuItem.checked != null) {
                 icon.classList.add("fa");
                 icon.classList.toggle("fa-check-square", menuItem.checked);
                 icon.classList.toggle("fa-square-o", !menuItem.checked);
@@ -432,6 +498,8 @@ class ContextMenu2 {
         if (parentMenu == null) {
             rootElement.removeEventListener("keydown", onGlobalKeyDown);
         }
+
+        onClose();
     }
 
     function closeSubmenu() {
@@ -454,7 +522,7 @@ class ContextMenu2 {
             var element = menu.children[currentSubmenuItemId];
             element.classList.add("open");
             var rect = element.getBoundingClientRect();
-            currentSubmenu = new ContextMenu2(rootElement, this, {x: rect.right, y: rect.top}, items[currentSubmenuItemId].menu, false);
+            currentSubmenu = new ContextMenu2(rootElement, this, {x: rect.right, y: rect.top}, items[currentSubmenuItemId].menu, {search: None, noIcons: options.noIcons});
         }
     }
 }

+ 2 - 2
hide/comp/SceneEditor.hx

@@ -1809,7 +1809,7 @@ class SceneEditor {
 			}
 
 			menuItems.push({ isSeparator : true, label : "" });
-			hide.comp.ContextMenu2.fromEvent(cast e, menuItems.concat(actionItems));
+			hide.comp.ContextMenu2.fromEvent(cast e, cast menuItems.concat(actionItems));
 		};
 
 		tree.element.parent().contextmenu(ctxMenu.bind(tree));
@@ -2329,7 +2329,7 @@ class SceneEditor {
 				keys : view.config.get("key.sceneeditor.gatherToMouse"),
 			},
 		];
-		hide.comp.ContextMenu2.createFromPoint(ide.mouseX, ide.mouseY, menuItems);
+		hide.comp.ContextMenu2.createFromPoint(ide.mouseX, ide.mouseY, cast menuItems);
 	}
 
 	public function refreshInteractive(elt : PrefabElement) {

+ 95 - 13
hide/view/GraphEditor.hx

@@ -11,6 +11,7 @@ import hide.view.shadereditor.Box;
 import hrt.shgraph.ShaderNode;
 import hrt.shgraph.ShaderType;
 using Lambda;
+using hrt.tools.MapUtils;
 import hrt.shgraph.ShaderType.SType;
 
 import hide.view.GraphInterface.IGraphEditor;
@@ -97,6 +98,8 @@ class GraphEditor extends hide.comp.Component {
 
 	public var currentUndoBuffer : UndoBuffer = [];
 
+	var contextMenu : hide.comp.ContextMenu2 = null;
+
 
 
 	var outputsToInputs : hrt.tools.OneToMany = new hrt.tools.OneToMany();
@@ -144,6 +147,8 @@ class GraphEditor extends hide.comp.Component {
 
 
 	public function onDisplay() {
+		saveDisplayKey = "hideGraphEditor";
+
 		heapsScene = element.find(".heaps-scene");
 		editorDisplay = new SVG(heapsScene);
 		editorDisplay.element.attr("id", "graph-root");
@@ -162,7 +167,7 @@ class GraphEditor extends hide.comp.Component {
 		keys.register("shadergraph.comment", commentFromSelection);
 		keys.register("duplicateInPlace", duplicateSelection);
 		keys.register("duplicate", duplicateSelection);
-		keys.register("graph.openAddMenu", openAddMenu.bind(null,null));
+		keys.register("graph.openAddMenu", openAddMenu2);
 		keys.register("cancel", cancelAll);
 
 		var miniPreviews = new Element('<div class="mini-preview"></div>');
@@ -198,20 +203,21 @@ class GraphEditor extends hide.comp.Component {
 				return;
 			}
 
-			if (e.button == 2) {
-				if (addMenu?.is(":visible") ?? false) {
-					closeAddMenu();
-					cleanupCreateEdge();
-				}
-				else {
-					openAddMenu();
-				}
-				e.preventDefault();
-				e.stopPropagation();
-			}
+			// if (e.button == 2) {
+			// 	if (addMenu?.is(":visible") ?? false) {
+			// 		closeAddMenu();
+			// 		cleanupCreateEdge();
+			// 	}
+			// 	else {
+			// 		openAddMenu2();
+			// 	}
+			// 	e.preventDefault();
+			// 	e.stopPropagation();
+			// }
 		});
 
 		heapsScene.on("contextmenu", function(e) {
+			openAddMenu2();
 			e.preventDefault();
 		});
 
@@ -235,7 +241,7 @@ class GraphEditor extends hide.comp.Component {
 						return;
 					}
 					else {
-						openAddMenu();
+						openAddMenu2();
 						e.stopPropagation();
 						return;
 					}
@@ -509,6 +515,82 @@ class GraphEditor extends hide.comp.Component {
 	}
 
 	static var lastOpenAddMenuPoint = new Point();
+
+	function openAddMenu2() {
+		if (getDisplayState("useOldAddMenu") != null) {
+			openAddMenu();
+			return;
+		}
+
+		if (contextMenu != null)
+			return;
+		lastOpenAddMenuPoint.set(lX(ide.mouseX), lY(ide.mouseY));
+
+		var nodes = editor.getAddNodesMenu();
+
+		var groups: Map<String, Array<hide.view.GraphInterface.AddNodeMenuEntry>> = [];
+
+		for (node in nodes) {
+			groups.getOrPut(node.group, []).push(node);
+		}
+
+		function doAdd(onConstructNode : () -> IGraphNode) {
+			//var key = Std.parseInt(this.selectedNode.attr("node"));
+			var posCursor = new Point(lX(ide.mouseX - 25), lY(ide.mouseY - 10));
+
+			var instance = onConstructNode();
+
+			var createLinkInput = edgeCreationInput;
+			var createLinkOutput = edgeCreationOutput;
+			var fromInput = createLinkInput != null;
+
+
+			if (createLinkInput != null) {
+				createLinkOutput = packIO(instance.id, 0);
+			}
+			else if (createLinkOutput != null) {
+				createLinkInput = packIO(instance.id, 0);
+			}
+
+			var pos = new h2d.col.Point();
+			pos.load(lastOpenAddMenuPoint);
+			if (createLinkInput != null) {
+				pos.set(lastCurveX, lastCurveY);
+			}
+			cleanupCreateEdge();
+
+			instance.setPos(pos);
+			opBox(instance, true, currentUndoBuffer);
+			if (createLinkInput != null && createLinkOutput != null) {
+				var box = boxes[instance.id];
+				var x = (fromInput ? @:privateAccess box.width : 0) - Box.NODE_HITBOX_RADIUS;
+				var y = box.getNodeHeight(0) - Box.NODE_HITBOX_RADIUS;
+				opMove(boxes[instance.id], pos.x - x, pos.y - y, currentUndoBuffer);
+				opEdge(createLinkOutput, createLinkInput, true, currentUndoBuffer);
+			}
+
+			commitUndo();
+			closeAddMenu();
+		}
+
+		var menu : Array<hide.comp.ContextMenu2.MenuItem> = [];
+		for (group => entries in groups) {
+			var submenu: Array<hide.comp.ContextMenu2.MenuItem> = [];
+			for (entry in entries) {
+				submenu.push({label: entry.name, click: doAdd.bind(entry.onConstructNode), tooltip: entry.description});
+			}
+			menu.push({
+				label: group,
+				menu: submenu,
+			});
+		}
+
+		contextMenu = hide.comp.ContextMenu2.createFromPoint(ide.mouseX, ide.mouseY, menu, {search: Visible, noIcons: true});
+		contextMenu.onClose = () -> {
+			contextMenu = null;
+		};
+	}
+
 	function openAddMenu(x : Int = 0, y : Int = 0) {
 
 		var boundsWidth = Std.int(element.width());

+ 12 - 0
hide/view/shadereditor/ShaderEditor.hx

@@ -230,6 +230,8 @@ class ShaderEditor extends hide.view.FileView implements GraphInterface.IGraphEd
 						<select id="domainSelection"></select>
 					</div>
 					<div> Preview Alpha<input id="previewAlpha" type="checkbox" /></div>
+					<div> Use old add menu<input id="oldAddMenu" type="checkbox" /></div>
+
 					<input id="centerView" type="button" value="Center Graph" />
 					<input id="debugMenu" type="button" value="Debug Menu"/>
 				</div>
@@ -260,6 +262,16 @@ class ShaderEditor extends hide.view.FileView implements GraphInterface.IGraphEd
 		});
 		(cast previewAlpha[0]:Dynamic).checked = previewSettings.previewAlpha;
 
+		var oldAddMenu = rightPannel.find("#oldAddMenu");
+		oldAddMenu.on("change", (e) -> {
+			if (untyped oldAddMenu.get(0).checked) {
+				graphEditor.saveDisplayState("useOldAddMenu", true);
+			} else {
+				graphEditor.removeDisplayState("useOldAddMenu");
+			}
+		});
+		untyped oldAddMenu.get(0).checked = graphEditor.getDisplayState("useOldAddMenu") != null;
+
 		rightPannel.appendTo(element);
 
 		var newParamCtxMenu : Array<hide.comp.ContextMenu.ContextMenuItem> = [

+ 1 - 1
hrt/shgraph/Random.hx

@@ -1,6 +1,6 @@
 package hrt.shgraph;
 
-@name("Random")
+@name("Math")
 @description("[CURRENTLY BROKEN] Generate a random value between min and max using the given seed")
 @group("Channel")
 class Random extends ShaderNodeHxsl {

+ 1 - 1
hrt/shgraph/ShaderGlobalInput.hx

@@ -4,7 +4,7 @@ using hxsl.Ast;
 
 @name("Global")
 @description("Global Inputs")
-@group("Property")
+@group("Input")
 @color("#0e8826")
 class ShaderGlobalInput extends ShaderNode {
 

+ 1 - 1
hrt/shgraph/ShaderInput.hx

@@ -9,7 +9,7 @@ enum InputKind {
 
 @name("Inputs")
 @description("Shader inputs of Heaps, it's dynamic")
-@group("Property")
+@group("Input")
 @color("#0e8826")
 class ShaderInput extends ShaderNode {
 	@prop("Variable") public var variable : String = "pixelColor";

+ 1 - 1
hrt/shgraph/ShaderParticleInput.hx

@@ -4,7 +4,7 @@ using hxsl.Ast;
 
 @name("Particle Inputs")
 @description("Particle specific shader inputs")
-@group("Property")
+@group("Input")
 @color("#0e8826")
 class ShaderParticleInputs extends ShaderNode {
 	@prop("Variable") public var variable : String = "life";

+ 1 - 1
hrt/shgraph/nodes/Add.hx

@@ -5,7 +5,7 @@ using hxsl.Ast;
 @name("Add")
 @description("The output is the result of A + B")
 @width(80)
-@group("Operation")
+@group("Math")
 class Add extends Operation {
 
 	static var SRC = {

+ 2 - 2
hrt/shgraph/nodes/AlphaOver.hx

@@ -1,9 +1,9 @@
 package hrt.shgraph.nodes;
 
 @name("Alpha Over")
-@description("Output is A if A.")
+@description("Blends between A and B based on A alpha * opacity")
 @width(100)
-@group("Operation")
+@group("Channel")
 class AlphaOver extends Operation {
 
 	static var SRC = {

+ 1 - 1
hrt/shgraph/nodes/Divide.hx

@@ -5,7 +5,7 @@ using hxsl.Ast;
 @name("Divide")
 @description("The output is the result of A / B")
 @width(80)
-@group("Operation")
+@group("Math")
 class Divide extends ShaderNodeHxsl {
 
 	static var SRC = {

+ 1 - 1
hrt/shgraph/nodes/Multiply.hx

@@ -5,7 +5,7 @@ using hxsl.Ast;
 @name("Multiply")
 @description("The output is the result of A * B")
 @width(80)
-@group("Operation")
+@group("Math")
 class Multiply extends ShaderNodeHxsl {
 
 	static var SRC = {

+ 1 - 1
hrt/shgraph/nodes/Project.hx

@@ -4,7 +4,7 @@ package hrt.shgraph.nodes;
 @name("Project")
 @description("Project the given world space vector into view space")
 @width(100)
-@group("Operation")
+@group("Math")
 class Project extends Operation {
 
 	static var SRC = {

+ 1 - 1
hrt/shgraph/nodes/Remap.hx

@@ -3,7 +3,7 @@ package hrt.shgraph.nodes;
 @name("Remap")
 @description("Remap value in range [inMin, inMax] to range [outMin, outMax]")
 @width(100)
-@group("Operation")
+@group("Math")
 class Remap extends Operation {
 
 	static var SRC = {

+ 1 - 1
hrt/shgraph/nodes/Subtract.hx

@@ -5,7 +5,7 @@ using hxsl.Ast;
 @name("Subtract")
 @description("The output is the result of A - B")
 @width(80)
-@group("Operation")
+@group("Math")
 class Subtract extends ShaderNodeHxsl {
 
 	static var SRC = {

+ 1 - 1
hrt/texgraph/nodes/Blur.hx

@@ -3,7 +3,7 @@ package hrt.texgraph.nodes;
 @name("Blur")
 @description("Blur texture")
 @width(100)
-@group("Operation")
+@group("Math")
 class Blur extends TexNode {
 	var inputs = [
 		{ name : "input1", type: h3d.mat.Texture }

+ 1 - 1
hrt/texgraph/nodes/Multiply.hx

@@ -14,7 +14,7 @@ class MultiplyShader extends h3d.shader.ScreenShader {
 @name("Multiply")
 @description("The output is the result of the inputs multiplied")
 @width(80)
-@group("Operation")
+@group("Math")
 class Multiply extends TexNode {
 	var inputs = [
 		{ name : "input1", type: h3d.mat.Texture },