A game about forced loneliness, made by TACStudios
1using System.Collections.Generic; 2using System.Linq; 3using UnityEngine; 4using UnityEditor.Experimental.GraphView; 5using UnityEngine.UIElements; 6using System; 7using UnityEditor.Graphing; 8using UnityEditor.ShaderGraph.Internal; 9using GraphDataStore = UnityEditor.ShaderGraph.DataStore<UnityEditor.ShaderGraph.GraphData>; 10using BlackboardItem = UnityEditor.ShaderGraph.Internal.ShaderInput; 11 12namespace UnityEditor.ShaderGraph.Drawing 13{ 14 struct BlackboardShaderInputOrder 15 { 16 public bool isKeyword; 17 public bool isDropdown; 18 public KeywordType keywordType; 19 public ShaderKeyword builtInKeyword; 20 public string deprecatedPropertyName; 21 public int version; 22 } 23 class BlackboardShaderInputFactory 24 { 25 static public ShaderInput GetShaderInput(BlackboardShaderInputOrder order) 26 { 27 ShaderInput output; 28 if (order.isKeyword) 29 { 30 if (order.builtInKeyword == null) 31 { 32 output = new ShaderKeyword(order.keywordType); 33 } 34 else 35 { 36 output = order.builtInKeyword; 37 } 38 } 39 else if (order.isDropdown) 40 { 41 output = new ShaderDropdown(); 42 } 43 else 44 { 45 switch (order.deprecatedPropertyName) 46 { 47 case "Color": 48 output = new ColorShaderProperty(order.version); 49 break; 50 default: 51 output = null; 52 AssertHelpers.Fail("BlackboardShaderInputFactory: Unknown deprecated property type."); 53 break; 54 } 55 } 56 57 return output; 58 } 59 } 60 class AddShaderInputAction : IGraphDataAction 61 { 62 public enum AddActionSource 63 { 64 Default, 65 AddMenu 66 } 67 68 void AddShaderInput(GraphData graphData) 69 { 70 AssertHelpers.IsNotNull(graphData, "GraphData is null while carrying out AddShaderInputAction"); 71 72 // If type property is valid, create instance of that type 73 if (blackboardItemType != null && blackboardItemType.IsSubclassOf(typeof(BlackboardItem))) 74 { 75 shaderInputReference = (BlackboardItem)Activator.CreateInstance(blackboardItemType, true); 76 } 77 else if (m_ShaderInputReferenceGetter != null) 78 { 79 shaderInputReference = m_ShaderInputReferenceGetter(); 80 } 81 // If type is null a direct override object must have been provided or else we are in an error-state 82 else if (shaderInputReference == null) 83 { 84 AssertHelpers.Fail("BlackboardController: Unable to complete Add Shader Input action."); 85 return; 86 } 87 88 shaderInputReference.generatePropertyBlock = shaderInputReference.isExposable; 89 90 if (graphData.owner != null) 91 graphData.owner.RegisterCompleteObjectUndo("Add Shader Input"); 92 else 93 AssertHelpers.Fail("GraphObject is null while carrying out AddShaderInputAction"); 94 95 graphData.AddGraphInput(shaderInputReference); 96 97 // If no categoryToAddItemToGuid is provided, add the input to the default category 98 if (categoryToAddItemToGuid == String.Empty) 99 { 100 var defaultCategory = graphData.categories.FirstOrDefault(); 101 AssertHelpers.IsNotNull(defaultCategory, "Default category reference is null."); 102 if (defaultCategory != null) 103 { 104 var addItemToCategoryAction = new AddItemToCategoryAction(); 105 addItemToCategoryAction.categoryGuid = defaultCategory.categoryGuid; 106 addItemToCategoryAction.itemToAdd = shaderInputReference; 107 graphData.owner.graphDataStore.Dispatch(addItemToCategoryAction); 108 } 109 } 110 else 111 { 112 var addItemToCategoryAction = new AddItemToCategoryAction(); 113 addItemToCategoryAction.categoryGuid = categoryToAddItemToGuid; 114 addItemToCategoryAction.itemToAdd = shaderInputReference; 115 graphData.owner.graphDataStore.Dispatch(addItemToCategoryAction); 116 } 117 } 118 119 public static AddShaderInputAction AddDeprecatedPropertyAction(BlackboardShaderInputOrder order) 120 { 121 return new() { shaderInputReference = BlackboardShaderInputFactory.GetShaderInput(order), addInputActionType = AddShaderInputAction.AddActionSource.AddMenu }; 122 } 123 124 public static AddShaderInputAction AddDropdownAction(BlackboardShaderInputOrder order) 125 { 126 return new() { shaderInputReference = BlackboardShaderInputFactory.GetShaderInput(order), addInputActionType = AddShaderInputAction.AddActionSource.AddMenu }; 127 } 128 129 public static AddShaderInputAction AddKeywordAction(BlackboardShaderInputOrder order) 130 { 131 return new() { shaderInputReference = BlackboardShaderInputFactory.GetShaderInput(order), addInputActionType = AddShaderInputAction.AddActionSource.AddMenu }; 132 } 133 134 public static AddShaderInputAction AddPropertyAction(Type shaderInputType) 135 { 136 return new() { blackboardItemType = shaderInputType, addInputActionType = AddShaderInputAction.AddActionSource.AddMenu }; 137 } 138 139 public Action<GraphData> modifyGraphDataAction => AddShaderInput; 140 // If this is a subclass of ShaderInput and is not null, then an object of this type is created to add to blackboard 141 // If the type field above is null and this is provided, then it is directly used as the item to add to blackboard 142 public BlackboardItem shaderInputReference { get; set; } 143 public AddActionSource addInputActionType { get; set; } 144 public string categoryToAddItemToGuid { get; set; } = String.Empty; 145 146 Type blackboardItemType { get; set; } 147 148 Func<BlackboardItem> m_ShaderInputReferenceGetter = null; 149 } 150 151 class ChangeGraphPathAction : IGraphDataAction 152 { 153 void ChangeGraphPath(GraphData graphData) 154 { 155 AssertHelpers.IsNotNull(graphData, "GraphData is null while carrying out ChangeGraphPathAction"); 156 graphData.path = NewGraphPath; 157 } 158 159 public Action<GraphData> modifyGraphDataAction => ChangeGraphPath; 160 161 public string NewGraphPath { get; set; } 162 } 163 164 class CopyShaderInputAction : IGraphDataAction 165 { 166 void CopyShaderInput(GraphData graphData) 167 { 168 AssertHelpers.IsNotNull(graphData, "GraphData is null while carrying out CopyShaderInputAction"); 169 AssertHelpers.IsNotNull(shaderInputToCopy, "ShaderInputToCopy is null while carrying out CopyShaderInputAction"); 170 171 // Don't handle undo here as there are different contexts in which this action is used, that define the undo action 172 // TODO: Perhaps a sign that each of those need to be made their own actions instead of conflating intent into a single action 173 174 switch (shaderInputToCopy) 175 { 176 case AbstractShaderProperty property: 177 178 insertIndex = Mathf.Clamp(insertIndex, -1, graphData.properties.Count() - 1); 179 var copiedProperty = (AbstractShaderProperty)graphData.AddCopyOfShaderInput(property, insertIndex); 180 if (copiedProperty != null) // some property types cannot be duplicated (unknown types) 181 { 182 // Update the property nodes that depends on the copied node 183 foreach (var node in dependentNodeList) 184 { 185 if (node is PropertyNode propertyNode) 186 { 187 propertyNode.owner = graphData; 188 propertyNode.property = copiedProperty; 189 } 190 } 191 } 192 193 194 copiedShaderInput = copiedProperty; 195 break; 196 197 case ShaderKeyword shaderKeyword: 198 // InsertIndex gets passed in relative to the blackboard position of an item overall, 199 // and not relative to the array sizes of the properties/keywords/dropdowns 200 var keywordInsertIndex = insertIndex - graphData.properties.Count(); 201 202 keywordInsertIndex = Mathf.Clamp(keywordInsertIndex, -1, graphData.keywords.Count() - 1); 203 204 // Don't duplicate built-in keywords within the same graph 205 if (shaderKeyword.isBuiltIn && graphData.keywords.Any(p => p.referenceName == shaderInputToCopy.referenceName)) 206 return; 207 208 var copiedKeyword = (ShaderKeyword)graphData.AddCopyOfShaderInput(shaderKeyword, keywordInsertIndex); 209 210 // Update the keyword nodes that depends on the copied node 211 foreach (var node in dependentNodeList) 212 { 213 if (node is KeywordNode propertyNode) 214 { 215 propertyNode.owner = graphData; 216 propertyNode.keyword = copiedKeyword; 217 } 218 } 219 220 copiedShaderInput = copiedKeyword; 221 break; 222 223 case ShaderDropdown shaderDropdown: 224 // InsertIndex gets passed in relative to the blackboard position of an item overall, 225 // and not relative to the array sizes of the properties/keywords/dropdowns 226 var dropdownInsertIndex = insertIndex - graphData.properties.Count() - graphData.keywords.Count(); 227 228 dropdownInsertIndex = Mathf.Clamp(dropdownInsertIndex, -1, graphData.dropdowns.Count() - 1); 229 230 var copiedDropdown = (ShaderDropdown)graphData.AddCopyOfShaderInput(shaderDropdown, dropdownInsertIndex); 231 232 // Update the dropdown nodes that depends on the copied node 233 foreach (var node in dependentNodeList) 234 { 235 if (node is DropdownNode propertyNode) 236 { 237 propertyNode.owner = graphData; 238 propertyNode.dropdown = copiedDropdown; 239 } 240 } 241 242 copiedShaderInput = copiedDropdown; 243 break; 244 245 default: 246 throw new ArgumentOutOfRangeException(); 247 } 248 249 if (copiedShaderInput != null) 250 { 251 // If specific category to copy to is provided, find and use it 252 foreach (var category in graphData.categories) 253 { 254 if (category.categoryGuid == containingCategoryGuid) 255 { 256 // Ensures that the new item gets added after the item it was duplicated from 257 insertIndex += 1; 258 // If the source item was already the last item in list, just add to end of list 259 if (insertIndex >= category.childCount) 260 insertIndex = -1; 261 graphData.InsertItemIntoCategory(category.objectId, copiedShaderInput, insertIndex); 262 return; 263 } 264 } 265 266 // Else, add to default category 267 graphData.categories.First().InsertItemIntoCategory(copiedShaderInput); 268 } 269 } 270 271 public Action<GraphData> modifyGraphDataAction => CopyShaderInput; 272 273 public IEnumerable<AbstractMaterialNode> dependentNodeList { get; set; } = new List<AbstractMaterialNode>(); 274 275 public BlackboardItem shaderInputToCopy { get; set; } 276 277 public BlackboardItem copiedShaderInput { get; set; } 278 279 public string containingCategoryGuid { get; set; } 280 281 public int insertIndex { get; set; } = -1; 282 } 283 284 class AddCategoryAction : IGraphDataAction 285 { 286 void AddCategory(GraphData graphData) 287 { 288 AssertHelpers.IsNotNull(graphData, "GraphData is null while carrying out AddCategoryAction"); 289 graphData.owner.RegisterCompleteObjectUndo("Add Category"); 290 // If categoryDataReference is not null, directly add it to graphData 291 if (categoryDataReference == null) 292 categoryDataReference = new CategoryData(categoryName, childObjects); 293 graphData.AddCategory(categoryDataReference); 294 } 295 296 public Action<GraphData> modifyGraphDataAction => AddCategory; 297 298 // Direct reference to the categoryData to use if it is specified 299 public CategoryData categoryDataReference { get; set; } 300 public string categoryName { get; set; } = String.Empty; 301 public List<ShaderInput> childObjects { get; set; } 302 } 303 304 class MoveCategoryAction : IGraphDataAction 305 { 306 void MoveCategory(GraphData graphData) 307 { 308 AssertHelpers.IsNotNull(graphData, "GraphData is null while carrying out MoveCategoryAction"); 309 graphData.owner.RegisterCompleteObjectUndo("Move Category"); 310 // Handling for out of range moves is slightly different, but otherwise we need to reverse for insertion order. 311 var guids = newIndexValue >= graphData.categories.Count() ? categoryGuids : categoryGuids.Reverse<string>(); 312 foreach (var guid in categoryGuids) 313 { 314 var cat = graphData.categories.FirstOrDefault(c => c.categoryGuid == guid); 315 graphData.MoveCategory(cat, newIndexValue); 316 } 317 } 318 319 public Action<GraphData> modifyGraphDataAction => MoveCategory; 320 321 // Reference to the shader input being modified 322 internal List<string> categoryGuids { get; set; } 323 324 internal int newIndexValue { get; set; } 325 } 326 327 class AddItemToCategoryAction : IGraphDataAction 328 { 329 public enum AddActionSource 330 { 331 Default, 332 DragDrop 333 } 334 335 void AddItemsToCategory(GraphData graphData) 336 { 337 AssertHelpers.IsNotNull(graphData, "GraphData is null while carrying out AddItemToCategoryAction"); 338 graphData.owner.RegisterCompleteObjectUndo("Add Item to Category"); 339 graphData.InsertItemIntoCategory(categoryGuid, itemToAdd, indexToAddItemAt); 340 } 341 342 public Action<GraphData> modifyGraphDataAction => AddItemsToCategory; 343 344 public string categoryGuid { get; set; } 345 346 public ShaderInput itemToAdd { get; set; } 347 348 // By default an item is always added to the end of a category, if this value is set to something other than -1, will insert the item at that position within the category 349 public int indexToAddItemAt { get; set; } = -1; 350 351 public AddActionSource addActionSource { get; set; } 352 } 353 354 class CopyCategoryAction : IGraphDataAction 355 { 356 void CopyCategory(GraphData graphData) 357 { 358 AssertHelpers.IsNotNull(graphData, "GraphData is null while carrying out CopyCategoryAction"); 359 AssertHelpers.IsNotNull(categoryToCopyReference, "CategoryToCopyReference is null while carrying out CopyCategoryAction"); 360 361 // This is called by MaterialGraphView currently, no need to repeat it here, though ideally it would live here 362 //graphData.owner.RegisterCompleteObjectUndo("Copy Category"); 363 364 newCategoryDataReference = graphData.CopyCategory(categoryToCopyReference); 365 } 366 367 // Reference to the new category created as a copy 368 public CategoryData newCategoryDataReference { get; set; } 369 370 // After category has been copied, store reference to it 371 public CategoryData categoryToCopyReference { get; set; } 372 373 public Action<GraphData> modifyGraphDataAction => CopyCategory; 374 } 375 376 class ShaderVariantLimitAction : IGraphDataAction 377 { 378 public int currentVariantCount { get; set; } = 0; 379 public int maxVariantCount { get; set; } = 0; 380 381 public ShaderVariantLimitAction(int currentVariantCount, int maxVariantCount) 382 { 383 this.maxVariantCount = maxVariantCount; 384 this.currentVariantCount = currentVariantCount; 385 } 386 387 // There's no action actually performed on the graph, but we need to implement this as a valid function 388 public Action<GraphData> modifyGraphDataAction => Empty; 389 390 void Empty(GraphData graphData) 391 { 392 } 393 } 394 395 class BlackboardController : SGViewController<GraphData, BlackboardViewModel> 396 { 397 // Type changes (adds/removes of Types) only happen after a full assembly reload so its safe to make this static 398 static IList<Type> s_ShaderInputTypes; 399 400 static BlackboardController() 401 { 402 var shaderInputTypes = TypeCache.GetTypesWithAttribute<BlackboardInputInfo>().ToList(); 403 // Sort the ShaderInput by priority using the BlackboardInputInfo attribute 404 shaderInputTypes.Sort((s1, s2) => 405 { 406 var info1 = Attribute.GetCustomAttribute(s1, typeof(BlackboardInputInfo)) as BlackboardInputInfo; 407 var info2 = Attribute.GetCustomAttribute(s2, typeof(BlackboardInputInfo)) as BlackboardInputInfo; 408 409 if (info1.priority == info2.priority) 410 return (info1.name ?? s1.Name).CompareTo(info2.name ?? s2.Name); 411 else 412 return info1.priority.CompareTo(info2.priority); 413 }); 414 415 s_ShaderInputTypes = shaderInputTypes.ToList(); 416 } 417 418 BlackboardCategoryController m_DefaultCategoryController = null; 419 Dictionary<string, BlackboardCategoryController> m_BlackboardCategoryControllers = new Dictionary<string, BlackboardCategoryController>(); 420 421 protected SGBlackboard m_Blackboard; 422 423 internal SGBlackboard blackboard 424 { 425 get => m_Blackboard; 426 private set => m_Blackboard = value; 427 } 428 public string GetFirstSelectedCategoryGuid() 429 { 430 if (m_Blackboard == null) 431 { 432 return string.Empty; 433 } 434 var copiedSelectionList = new List<ISelectable>(m_Blackboard.selection); 435 var selectedCategories = new List<SGBlackboardCategory>(); 436 var selectedCategoryGuid = String.Empty; 437 for (int i = 0; i < copiedSelectionList.Count; i++) 438 { 439 var selectable = copiedSelectionList[i]; 440 if (selectable is SGBlackboardCategory category) 441 { 442 selectedCategories.Add(selectable as SGBlackboardCategory); 443 } 444 } 445 if (selectedCategories.Any()) 446 { 447 selectedCategoryGuid = selectedCategories[0].viewModel.associatedCategoryGuid; 448 } 449 return selectedCategoryGuid; 450 } 451 452 void InitializeViewModel(bool useDropdowns) 453 { 454 // Clear the view model 455 ViewModel.ResetViewModelData(); 456 ViewModel.subtitle = BlackboardUtils.FormatPath(Model.path); 457 BlackboardShaderInputOrder propertyTypesOrder = new BlackboardShaderInputOrder(); 458 459 // Property data first 460 foreach (var shaderInputType in s_ShaderInputTypes) 461 { 462 if (shaderInputType.IsAbstract) 463 continue; 464 465 var info = Attribute.GetCustomAttribute(shaderInputType, typeof(BlackboardInputInfo)) as BlackboardInputInfo; 466 string name = info?.name ?? ObjectNames.NicifyVariableName(shaderInputType.Name.Replace("ShaderProperty", "")); 467 468 // QUICK FIX TO DEAL WITH DEPRECATED COLOR PROPERTY 469 if (name.Equals("Color", StringComparison.InvariantCultureIgnoreCase) && ShaderGraphPreferences.allowDeprecatedBehaviors) 470 { 471 propertyTypesOrder.isKeyword = false; 472 propertyTypesOrder.deprecatedPropertyName = name; 473 propertyTypesOrder.version = ColorShaderProperty.deprecatedVersion; 474 ViewModel.propertyNameToAddActionMap.Add($"Color (Legacy v0)", AddShaderInputAction.AddDeprecatedPropertyAction(propertyTypesOrder)); 475 ViewModel.propertyNameToAddActionMap.Add(name, AddShaderInputAction.AddPropertyAction(shaderInputType)); 476 } 477 else 478 ViewModel.propertyNameToAddActionMap.Add(name, AddShaderInputAction.AddPropertyAction(shaderInputType)); 479 } 480 481 // Default Keywords next 482 BlackboardShaderInputOrder keywordTypesOrder = new BlackboardShaderInputOrder(); 483 keywordTypesOrder.isKeyword = true; 484 keywordTypesOrder.keywordType = KeywordType.Boolean; 485 ViewModel.defaultKeywordNameToAddActionMap.Add("Boolean", AddShaderInputAction.AddKeywordAction(keywordTypesOrder)); 486 keywordTypesOrder.keywordType = KeywordType.Enum; 487 ViewModel.defaultKeywordNameToAddActionMap.Add("Enum", AddShaderInputAction.AddKeywordAction(keywordTypesOrder)); 488 489 // Built-In Keywords after that 490 foreach (var builtinKeywordDescriptor in KeywordUtil.GetBuiltinKeywordDescriptors()) 491 { 492 var keyword = ShaderKeyword.CreateBuiltInKeyword(builtinKeywordDescriptor); 493 // Do not allow user to add built-in keywords that conflict with user-made keywords that have the same reference name or display name 494 if (Model.keywords.Any(x => x.referenceName == keyword.referenceName || x.displayName == keyword.displayName)) 495 { 496 ViewModel.disabledKeywordNameList.Add(keyword.displayName); 497 } 498 else 499 { 500 keywordTypesOrder.builtInKeyword = (ShaderKeyword)keyword.Copy(); 501 ViewModel.builtInKeywordNameToAddActionMap.Add(keyword.displayName, AddShaderInputAction.AddKeywordAction(keywordTypesOrder)); 502 } 503 } 504 505 if (useDropdowns) 506 { 507 BlackboardShaderInputOrder dropdownsOrder = new BlackboardShaderInputOrder(); 508 dropdownsOrder.isDropdown = true; 509 ViewModel.defaultDropdownNameToAdd = new Tuple<string, IGraphDataAction>("Dropdown", AddShaderInputAction.AddDropdownAction(dropdownsOrder)); 510 } 511 512 // Category data last 513 var defaultNewCategoryReference = new CategoryData("Category"); 514 ViewModel.addCategoryAction = new AddCategoryAction() { categoryDataReference = defaultNewCategoryReference }; 515 516 ViewModel.requestModelChangeAction = this.RequestModelChange; 517 ViewModel.categoryInfoList.AddRange(DataStore.State.categories.ToList()); 518 } 519 520 internal BlackboardController(GraphData model, BlackboardViewModel inViewModel, GraphDataStore graphDataStore) 521 : base(model, inViewModel, graphDataStore) 522 { 523 // TODO: hide this more generically for category types. 524 bool useDropdowns = model.isSubGraph; 525 InitializeViewModel(useDropdowns); 526 527 blackboard = new SGBlackboard(ViewModel, this); 528 529 // Add default category at the top of the blackboard (create it if it doesn't exist already) 530 var existingDefaultCategory = DataStore.State.categories.FirstOrDefault(); 531 if (existingDefaultCategory != null && existingDefaultCategory.IsNamedCategory() == false) 532 { 533 AddBlackboardCategory(graphDataStore, existingDefaultCategory); 534 } 535 else 536 { 537 // Any properties that don't already have a category (for example, if this graph is being loaded from an older version that doesn't have category data) 538 var uncategorizedBlackboardItems = new List<ShaderInput>(); 539 foreach (var shaderProperty in DataStore.State.properties) 540 if (IsInputUncategorized(shaderProperty)) 541 uncategorizedBlackboardItems.Add(shaderProperty); 542 543 foreach (var shaderKeyword in DataStore.State.keywords) 544 if (IsInputUncategorized(shaderKeyword)) 545 uncategorizedBlackboardItems.Add(shaderKeyword); 546 547 if (useDropdowns) 548 { 549 foreach (var shaderDropdown in DataStore.State.dropdowns) 550 if (IsInputUncategorized(shaderDropdown)) 551 uncategorizedBlackboardItems.Add(shaderDropdown); 552 } 553 554 var addCategoryAction = new AddCategoryAction(); 555 addCategoryAction.categoryDataReference = CategoryData.DefaultCategory(uncategorizedBlackboardItems); 556 graphDataStore.Dispatch(addCategoryAction); 557 } 558 559 // Get the reference to default category controller after its been added 560 m_DefaultCategoryController = m_BlackboardCategoryControllers.Values.FirstOrDefault(); 561 AssertHelpers.IsNotNull(m_DefaultCategoryController, "Failed to instantiate default category."); 562 563 // Handle loaded-in categories from graph first, skipping the first/default category 564 foreach (var categoryData in ViewModel.categoryInfoList.Skip(1)) 565 { 566 AddBlackboardCategory(graphDataStore, categoryData); 567 } 568 } 569 570 internal string editorPrefsBaseKey => "unity.shadergraph." + DataStore.State.objectId; 571 572 BlackboardCategoryController AddBlackboardCategory(GraphDataStore graphDataStore, CategoryData categoryInfo) 573 { 574 var blackboardCategoryViewModel = new BlackboardCategoryViewModel(); 575 blackboardCategoryViewModel.parentView = blackboard; 576 blackboardCategoryViewModel.requestModelChangeAction = ViewModel.requestModelChangeAction; 577 blackboardCategoryViewModel.name = categoryInfo.name; 578 blackboardCategoryViewModel.associatedCategoryGuid = categoryInfo.categoryGuid; 579 blackboardCategoryViewModel.isExpanded = EditorPrefs.GetBool($"{editorPrefsBaseKey}.{categoryInfo.categoryGuid}.{ChangeCategoryIsExpandedAction.kEditorPrefKey}", true); 580 581 var blackboardCategoryController = new BlackboardCategoryController(categoryInfo, blackboardCategoryViewModel, graphDataStore); 582 if (m_BlackboardCategoryControllers.ContainsKey(categoryInfo.categoryGuid) == false) 583 { 584 m_BlackboardCategoryControllers.Add(categoryInfo.categoryGuid, blackboardCategoryController); 585 m_DefaultCategoryController = m_BlackboardCategoryControllers.Values.FirstOrDefault(); 586 } 587 else 588 { 589 AssertHelpers.Fail("Failed to add category controller due to category with same GUID already having been added."); 590 return null; 591 } 592 return blackboardCategoryController; 593 } 594 595 // Creates controller, view and view model for a blackboard item and adds the view to the specified index in the category 596 SGBlackboardRow InsertBlackboardRow(BlackboardItem shaderInput, int insertionIndex = -1) 597 { 598 return m_DefaultCategoryController.InsertBlackboardRow(shaderInput, insertionIndex); 599 } 600 601 public void UpdateBlackboardTitle(string newTitle) 602 { 603 ViewModel.title = newTitle; 604 blackboard.title = ViewModel.title; 605 } 606 607 protected override void RequestModelChange(IGraphDataAction changeAction) 608 { 609 DataStore.Dispatch(changeAction); 610 } 611 612 // Called by GraphDataStore.Subscribe after the model has been changed 613 protected override void ModelChanged(GraphData graphData, IGraphDataAction changeAction) 614 { 615 // Reconstruct view-model first 616 // TODO: hide this more generically for category types. 617 bool useDropdowns = graphData.isSubGraph; 618 InitializeViewModel(useDropdowns); 619 620 var graphView = ViewModel.parentView as MaterialGraphView; 621 622 switch (changeAction) 623 { 624 // If newly added input doesn't belong to any of the user-made categories, add it to the default category at top of blackboard 625 case AddShaderInputAction addBlackboardItemAction: 626 if (IsInputUncategorized(addBlackboardItemAction.shaderInputReference)) 627 { 628 var blackboardRow = InsertBlackboardRow(addBlackboardItemAction.shaderInputReference); 629 if (blackboardRow != null) 630 { 631 var propertyView = blackboardRow.Q<SGBlackboardField>(); 632 if (addBlackboardItemAction.addInputActionType == AddShaderInputAction.AddActionSource.AddMenu) 633 propertyView.OpenTextEditor(); 634 } 635 } 636 break; 637 // Need to handle deletion of shader inputs here as opposed to BlackboardCategoryController, as currently, 638 // once removed from the categories there is no way to associate an input with the category that owns it 639 case DeleteShaderInputAction deleteShaderInputAction: 640 foreach (var shaderInput in deleteShaderInputAction.shaderInputsToDelete) 641 RemoveInputFromBlackboard(shaderInput); 642 break; 643 644 case HandleUndoRedoAction handleUndoRedoAction: 645 ClearBlackboardCategories(); 646 647 foreach (var categoryData in graphData.addedCategories) 648 AddBlackboardCategory(DataStore, categoryData); 649 650 m_DefaultCategoryController = m_BlackboardCategoryControllers.Values.FirstOrDefault(); 651 652 break; 653 case CopyShaderInputAction copyShaderInputAction: 654 // In the specific case of only-one keywords like Material Quality and Raytracing, they can get copied, but because only one can exist, the output copied value is null 655 if (copyShaderInputAction.copiedShaderInput != null && IsInputUncategorized(copyShaderInputAction.copiedShaderInput)) 656 { 657 var blackboardRow = InsertBlackboardRow(copyShaderInputAction.copiedShaderInput, copyShaderInputAction.insertIndex); 658 var propertyView = blackboardRow.Q<SGBlackboardField>(); 659 graphView?.AddToSelectionNoUndoRecord(propertyView); 660 } 661 662 break; 663 664 case AddCategoryAction addCategoryAction: 665 AddBlackboardCategory(DataStore, addCategoryAction.categoryDataReference); 666 // Iterate through anything that is selected currently 667 foreach (var selectedElement in blackboard.selection.ToList()) 668 { 669 if (selectedElement is SGBlackboardField { userData: ShaderInput shaderInput }) 670 { 671 // If a blackboard item is selected, first remove it from the blackboard 672 RemoveInputFromBlackboard(shaderInput); 673 674 // Then add input to the new category 675 var addItemToCategoryAction = new AddItemToCategoryAction(); 676 addItemToCategoryAction.categoryGuid = addCategoryAction.categoryDataReference.categoryGuid; 677 addItemToCategoryAction.itemToAdd = shaderInput; 678 DataStore.Dispatch(addItemToCategoryAction); 679 } 680 } 681 break; 682 683 case DeleteCategoryAction deleteCategoryAction: 684 // Clean up deleted categories 685 foreach (var categoryGUID in deleteCategoryAction.categoriesToRemoveGuids) 686 { 687 RemoveBlackboardCategory(categoryGUID); 688 } 689 break; 690 691 case MoveCategoryAction moveCategoryAction: 692 ClearBlackboardCategories(); 693 foreach (var categoryData in ViewModel.categoryInfoList) 694 AddBlackboardCategory(graphData.owner.graphDataStore, categoryData); 695 break; 696 697 case CopyCategoryAction copyCategoryAction: 698 var blackboardCategory = AddBlackboardCategory(graphData.owner.graphDataStore, copyCategoryAction.newCategoryDataReference); 699 if (blackboardCategory != null) 700 graphView?.AddToSelectionNoUndoRecord(blackboardCategory.blackboardCategoryView); 701 break; 702 case ShaderVariantLimitAction shaderVariantLimitAction: 703 blackboard.SetCurrentVariantUsage(shaderVariantLimitAction.currentVariantCount, shaderVariantLimitAction.maxVariantCount); 704 break; 705 } 706 707 // Lets all event handlers this controller owns/manages know that the model has changed 708 // Usually this is to update views and make them reconstruct themself from updated view-model 709 //NotifyChange(changeAction); 710 711 // Let child controllers know about changes to this controller so they may update themselves in turn 712 //ApplyChanges(); 713 } 714 715 void RemoveInputFromBlackboard(ShaderInput shaderInput) 716 { 717 // Check if input is in one of the categories 718 foreach (var controller in m_BlackboardCategoryControllers.Values) 719 { 720 var blackboardRow = controller.FindBlackboardRow(shaderInput); 721 if (blackboardRow != null) 722 { 723 controller.RemoveBlackboardRow(shaderInput); 724 return; 725 } 726 } 727 } 728 729 bool IsInputUncategorized(ShaderInput shaderInput) 730 { 731 // Skip the first category controller as that is guaranteed to be the default category 732 foreach (var categoryController in m_BlackboardCategoryControllers.Values.Skip(1)) 733 { 734 if (categoryController.IsInputInCategory(shaderInput)) 735 return false; 736 } 737 738 return true; 739 } 740 741 public SGBlackboardCategory GetBlackboardCategory(string inputGuid) 742 { 743 foreach (var categoryController in m_BlackboardCategoryControllers.Values) 744 { 745 if (categoryController.Model.categoryGuid == inputGuid) 746 return categoryController.blackboardCategoryView; 747 } 748 749 return null; 750 } 751 752 public SGBlackboardRow GetBlackboardRow(ShaderInput blackboardItem) 753 { 754 foreach (var categoryController in m_BlackboardCategoryControllers.Values) 755 { 756 var blackboardRow = categoryController.FindBlackboardRow(blackboardItem); 757 if (blackboardRow != null) 758 return blackboardRow; 759 } 760 761 return null; 762 } 763 764 int numberOfCategories => m_BlackboardCategoryControllers.Count; 765 766 // Gets the index after the currently selected shader input for pasting properties into this graph 767 internal int GetInsertionIndexForPaste() 768 { 769 if (blackboard?.selection == null || blackboard.selection.Count == 0) 770 { 771 return 0; 772 } 773 774 foreach (ISelectable selection in blackboard.selection) 775 { 776 if (selection is SGBlackboardField blackboardPropertyView) 777 { 778 SGBlackboardRow row = blackboardPropertyView.GetFirstAncestorOfType<SGBlackboardRow>(); 779 SGBlackboardCategory category = blackboardPropertyView.GetFirstAncestorOfType<SGBlackboardCategory>(); 780 if (row == null || category == null) 781 continue; 782 int blackboardFieldIndex = category.IndexOf(row); 783 784 return blackboardFieldIndex; 785 } 786 } 787 788 return 0; 789 } 790 791 void RemoveBlackboardCategory(string categoryGUID) 792 { 793 m_BlackboardCategoryControllers.TryGetValue(categoryGUID, out var blackboardCategoryController); 794 if (blackboardCategoryController != null) 795 { 796 blackboardCategoryController.Dispose(); 797 m_BlackboardCategoryControllers.Remove(categoryGUID); 798 } 799 else 800 AssertHelpers.Fail("Tried to remove a category that doesn't exist. "); 801 } 802 803 public override void Dispose() 804 { 805 if (m_Blackboard == null) 806 return; 807 808 base.Dispose(); 809 m_DefaultCategoryController = null; 810 ClearBlackboardCategories(); 811 812 m_Blackboard?.Dispose(); 813 m_Blackboard = null; 814 } 815 816 void ClearBlackboardCategories() 817 { 818 foreach (var categoryController in m_BlackboardCategoryControllers.Values) 819 { 820 categoryController.Dispose(); 821 } 822 m_BlackboardCategoryControllers.Clear(); 823 } 824 825 // Meant to be used by UI testing in order to clear blackboard state 826 internal void ResetBlackboardState() 827 { 828 ClearBlackboardCategories(); 829 var addCategoryAction = new AddCategoryAction(); 830 addCategoryAction.categoryDataReference = CategoryData.DefaultCategory(); 831 DataStore.Dispatch(addCategoryAction); 832 } 833 } 834}