No Description
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

UnitOption.cs 20KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using UnityEditor;
  5. using UnityEngine;
  6. namespace Unity.VisualScripting
  7. {
  8. [FuzzyOption(typeof(IUnit))]
  9. public class UnitOption<TUnit> : IUnitOption where TUnit : IUnit
  10. {
  11. public UnitOption()
  12. {
  13. sourceScriptGuids = new HashSet<string>();
  14. }
  15. public UnitOption(TUnit unit) : this()
  16. {
  17. this.unit = unit;
  18. FillFromUnit();
  19. }
  20. [DoNotSerialize]
  21. protected bool filled { get; private set; }
  22. private TUnit _unit;
  23. protected UnitOptionRow source { get; private set; }
  24. public TUnit unit
  25. {
  26. get
  27. {
  28. // Load the node on demand to avoid deserialization overhead
  29. // Deserializing the entire database takes many seconds,
  30. // which is the reason why UnitOptionRow and SQLite are used
  31. // in the first place.
  32. if (_unit == null)
  33. {
  34. _unit = (TUnit)new SerializationData(source.unit).Deserialize();
  35. }
  36. return _unit;
  37. }
  38. protected set
  39. {
  40. _unit = value;
  41. }
  42. }
  43. IUnit IUnitOption.unit => unit;
  44. public Type unitType { get; private set; }
  45. protected IUnitDescriptor descriptor => unit.Descriptor<IUnitDescriptor>();
  46. // Avoid using the descriptions for each option, because we don't need all fields described until the option is hovered
  47. protected UnitDescription description => unit.Description<UnitDescription>();
  48. protected UnitPortDescription PortDescription(IUnitPort port)
  49. {
  50. return port.Description<UnitPortDescription>();
  51. }
  52. public virtual IUnit InstantiateUnit()
  53. {
  54. var instance = unit.CloneViaSerialization();
  55. instance.Define();
  56. return instance;
  57. }
  58. void IUnitOption.PreconfigureUnit(IUnit unit)
  59. {
  60. PreconfigureUnit((TUnit)unit);
  61. }
  62. public virtual void PreconfigureUnit(TUnit unit)
  63. {
  64. }
  65. protected virtual void FillFromUnit()
  66. {
  67. unit.EnsureDefined();
  68. unitType = unit.GetType();
  69. labelHuman = Label(true);
  70. haystackHuman = Haystack(true);
  71. labelProgrammer = Label(false);
  72. haystackProgrammer = Haystack(false);
  73. category = Category();
  74. order = Order();
  75. favoriteKey = FavoriteKey();
  76. UnityAPI.Async(() => icon = Icon());
  77. showControlInputsInFooter = ShowControlInputsInFooter();
  78. showControlOutputsInFooter = ShowControlOutputsInFooter();
  79. showValueInputsInFooter = ShowValueInputsInFooter();
  80. showValueOutputsInFooter = ShowValueOutputsInFooter();
  81. controlInputCount = unit.controlInputs.Count;
  82. controlOutputCount = unit.controlOutputs.Count;
  83. valueInputTypes = unit.valueInputs.Select(vi => vi.type).ToHashSet();
  84. valueOutputTypes = unit.valueOutputs.Select(vo => vo.type).ToHashSet();
  85. filled = true;
  86. }
  87. protected virtual void FillFromData()
  88. {
  89. unit.EnsureDefined();
  90. unitType = unit.GetType();
  91. UnityAPI.Async(() => icon = Icon());
  92. showControlInputsInFooter = ShowControlInputsInFooter();
  93. showControlOutputsInFooter = ShowControlOutputsInFooter();
  94. showValueInputsInFooter = ShowValueInputsInFooter();
  95. showValueOutputsInFooter = ShowValueOutputsInFooter();
  96. filled = true;
  97. }
  98. public virtual void Deserialize(UnitOptionRow row)
  99. {
  100. source = row;
  101. if (row.sourceScriptGuids != null)
  102. {
  103. sourceScriptGuids = row.sourceScriptGuids.Split(',').ToHashSet();
  104. }
  105. unitType = Codebase.DeserializeType(row.unitType);
  106. category = row.category == null ? null : new UnitCategory(row.category);
  107. labelHuman = row.labelHuman;
  108. labelProgrammer = row.labelProgrammer;
  109. order = row.order;
  110. haystackHuman = row.haystackHuman;
  111. haystackProgrammer = row.haystackProgrammer;
  112. favoriteKey = row.favoriteKey;
  113. controlInputCount = row.controlInputCount;
  114. controlOutputCount = row.controlOutputCount;
  115. }
  116. public virtual UnitOptionRow Serialize()
  117. {
  118. var row = new UnitOptionRow();
  119. if (sourceScriptGuids.Count == 0)
  120. {
  121. // Important to set to null here, because the code relies on
  122. // null checks, not empty string checks.
  123. row.sourceScriptGuids = null;
  124. }
  125. else
  126. {
  127. row.sourceScriptGuids = string.Join(",", sourceScriptGuids.ToArray());
  128. }
  129. row.optionType = Codebase.SerializeType(GetType());
  130. row.unitType = Codebase.SerializeType(unitType);
  131. row.unit = unit.Serialize().json;
  132. row.category = category?.fullName;
  133. row.labelHuman = labelHuman;
  134. row.labelProgrammer = labelProgrammer;
  135. row.order = order;
  136. row.haystackHuman = haystackHuman;
  137. row.haystackProgrammer = haystackProgrammer;
  138. row.favoriteKey = favoriteKey;
  139. row.controlInputCount = controlInputCount;
  140. row.controlOutputCount = controlOutputCount;
  141. row.valueInputTypes = valueInputTypes.Select(Codebase.SerializeType).ToSeparatedString("|").NullIfEmpty();
  142. row.valueOutputTypes = valueOutputTypes.Select(Codebase.SerializeType).ToSeparatedString("|").NullIfEmpty();
  143. return row;
  144. }
  145. public virtual void OnPopulate()
  146. {
  147. if (!filled)
  148. {
  149. FillFromData();
  150. }
  151. }
  152. public virtual void Prewarm() { }
  153. #region Configuration
  154. public object value => this;
  155. public bool parentOnly => false;
  156. public virtual string headerLabel => label;
  157. public virtual bool showHeaderIcon => false;
  158. public virtual bool favoritable => true;
  159. #endregion
  160. #region Properties
  161. public HashSet<string> sourceScriptGuids { get; protected set; }
  162. protected string labelHuman { get; set; }
  163. protected string labelProgrammer { get; set; }
  164. public string label => BoltCore.Configuration.humanNaming ? labelHuman : labelProgrammer;
  165. public UnitCategory category { get; private set; }
  166. public int order { get; private set; }
  167. public EditorTexture icon { get; private set; }
  168. protected string haystackHuman { get; set; }
  169. protected string haystackProgrammer { get; set; }
  170. public string haystack => BoltCore.Configuration.humanNaming ? haystackHuman : haystackProgrammer;
  171. public string favoriteKey { get; private set; }
  172. public virtual string formerHaystack => BoltFlowNameUtility.UnitPreviousTitle(unitType);
  173. GUIStyle IFuzzyOption.style => Style();
  174. #endregion
  175. #region Contextual Filtering
  176. public int controlInputCount { get; private set; }
  177. public int controlOutputCount { get; private set; }
  178. private HashSet<Type> _valueInputTypes;
  179. private HashSet<Type> _valueOutputTypes;
  180. // On demand loading for initialization performance (type deserialization is expensive)
  181. public HashSet<Type> valueInputTypes
  182. {
  183. get
  184. {
  185. if (_valueInputTypes == null)
  186. {
  187. if (string.IsNullOrEmpty(source.valueInputTypes))
  188. {
  189. _valueInputTypes = new HashSet<Type>();
  190. }
  191. else
  192. {
  193. _valueInputTypes = source.valueInputTypes.Split('|').Select(Codebase.DeserializeType).ToHashSet();
  194. }
  195. }
  196. return _valueInputTypes;
  197. }
  198. private set
  199. {
  200. _valueInputTypes = value;
  201. }
  202. }
  203. public HashSet<Type> valueOutputTypes
  204. {
  205. get
  206. {
  207. if (_valueOutputTypes == null)
  208. {
  209. if (string.IsNullOrEmpty(source.valueOutputTypes))
  210. {
  211. _valueOutputTypes = new HashSet<Type>();
  212. }
  213. else
  214. {
  215. _valueOutputTypes = source.valueOutputTypes.Split('|').Select(Codebase.DeserializeType).ToHashSet();
  216. }
  217. }
  218. return _valueOutputTypes;
  219. }
  220. private set
  221. {
  222. _valueOutputTypes = value;
  223. }
  224. }
  225. #endregion
  226. #region Providers
  227. protected virtual string Label(bool human)
  228. {
  229. return BoltFlowNameUtility.UnitTitle(unitType, false, true);
  230. }
  231. protected virtual UnitCategory Category()
  232. {
  233. return unitType.GetAttribute<UnitCategory>();
  234. }
  235. protected virtual int Order()
  236. {
  237. return unitType.GetAttribute<UnitOrderAttribute>()?.order ?? int.MaxValue;
  238. }
  239. protected virtual string Haystack(bool human)
  240. {
  241. return Label(human);
  242. }
  243. protected virtual EditorTexture Icon()
  244. {
  245. return descriptor.Icon();
  246. }
  247. protected virtual GUIStyle Style()
  248. {
  249. return FuzzyWindow.defaultOptionStyle;
  250. }
  251. protected virtual string FavoriteKey()
  252. {
  253. return unit.GetType().FullName;
  254. }
  255. #endregion
  256. #region Search
  257. public virtual string SearchResultLabel(string query)
  258. {
  259. var label = SearchUtility.HighlightQuery(haystack, query);
  260. if (category != null)
  261. {
  262. label += $" <color=#{ColorPalette.unityForegroundDim.ToHexString()}>(in {category.fullName})</color>";
  263. }
  264. return label;
  265. }
  266. #endregion
  267. #region Footer
  268. private string summary => description.summary;
  269. public bool hasFooter => !StringUtility.IsNullOrWhiteSpace(summary) || footerPorts.Any();
  270. protected virtual bool ShowControlInputsInFooter()
  271. {
  272. return unitType.GetAttribute<UnitFooterPortsAttribute>()?.ControlInputs ?? false;
  273. }
  274. protected virtual bool ShowControlOutputsInFooter()
  275. {
  276. return unitType.GetAttribute<UnitFooterPortsAttribute>()?.ControlOutputs ?? false;
  277. }
  278. protected virtual bool ShowValueInputsInFooter()
  279. {
  280. return unitType.GetAttribute<UnitFooterPortsAttribute>()?.ValueInputs ?? true;
  281. }
  282. protected virtual bool ShowValueOutputsInFooter()
  283. {
  284. return unitType.GetAttribute<UnitFooterPortsAttribute>()?.ValueOutputs ?? true;
  285. }
  286. [DoNotSerialize]
  287. protected bool showControlInputsInFooter { get; private set; }
  288. [DoNotSerialize]
  289. protected bool showControlOutputsInFooter { get; private set; }
  290. [DoNotSerialize]
  291. protected bool showValueInputsInFooter { get; private set; }
  292. [DoNotSerialize]
  293. protected bool showValueOutputsInFooter { get; private set; }
  294. private IEnumerable<IUnitPort> footerPorts
  295. {
  296. get
  297. {
  298. if (showControlInputsInFooter)
  299. {
  300. foreach (var controlInput in unit.controlInputs)
  301. {
  302. yield return controlInput;
  303. }
  304. }
  305. if (showControlOutputsInFooter)
  306. {
  307. foreach (var controlOutput in unit.controlOutputs)
  308. {
  309. yield return controlOutput;
  310. }
  311. }
  312. if (showValueInputsInFooter)
  313. {
  314. foreach (var valueInput in unit.valueInputs)
  315. {
  316. yield return valueInput;
  317. }
  318. }
  319. if (showValueOutputsInFooter)
  320. {
  321. foreach (var valueOutput in unit.valueOutputs)
  322. {
  323. yield return valueOutput;
  324. }
  325. }
  326. }
  327. }
  328. public float GetFooterHeight(float width)
  329. {
  330. var hasSummary = !StringUtility.IsNullOrWhiteSpace(summary);
  331. var hasIcon = icon != null;
  332. var hasPorts = footerPorts.Any();
  333. var height = 0f;
  334. width -= 2 * FooterStyles.padding;
  335. height += FooterStyles.padding;
  336. if (hasSummary)
  337. {
  338. if (hasIcon)
  339. {
  340. height += Mathf.Max(FooterStyles.unitIconSize, GetFooterSummaryHeight(width - FooterStyles.unitIconSize - FooterStyles.spaceAfterUnitIcon));
  341. }
  342. else
  343. {
  344. height += GetFooterSummaryHeight(width);
  345. }
  346. }
  347. if (hasSummary && hasPorts)
  348. {
  349. height += FooterStyles.spaceBetweenDescriptionAndPorts;
  350. }
  351. foreach (var port in footerPorts)
  352. {
  353. height += GetFooterPortHeight(width, port);
  354. height += FooterStyles.spaceBetweenPorts;
  355. }
  356. if (hasPorts)
  357. {
  358. height -= FooterStyles.spaceBetweenPorts;
  359. }
  360. height += FooterStyles.padding;
  361. return height;
  362. }
  363. public void OnFooterGUI(Rect position)
  364. {
  365. var hasSummary = !StringUtility.IsNullOrWhiteSpace(summary);
  366. var hasIcon = icon != null;
  367. var hasPorts = footerPorts.Any();
  368. var y = position.y;
  369. y += FooterStyles.padding;
  370. position.x += FooterStyles.padding;
  371. position.width -= FooterStyles.padding * 2;
  372. if (hasSummary)
  373. {
  374. if (hasIcon)
  375. {
  376. var iconPosition = new Rect
  377. (
  378. position.x,
  379. y,
  380. FooterStyles.unitIconSize,
  381. FooterStyles.unitIconSize
  382. );
  383. var summaryWidth = position.width - iconPosition.width - FooterStyles.spaceAfterUnitIcon;
  384. var summaryPosition = new Rect
  385. (
  386. iconPosition.xMax + FooterStyles.spaceAfterUnitIcon,
  387. y,
  388. summaryWidth,
  389. GetFooterSummaryHeight(summaryWidth)
  390. );
  391. GUI.DrawTexture(iconPosition, icon?[FooterStyles.unitIconSize]);
  392. OnFooterSummaryGUI(summaryPosition);
  393. y = Mathf.Max(iconPosition.yMax, summaryPosition.yMax);
  394. }
  395. else
  396. {
  397. OnFooterSummaryGUI(position.VerticalSection(ref y, GetFooterSummaryHeight(position.width)));
  398. }
  399. }
  400. if (hasSummary && hasPorts)
  401. {
  402. y += FooterStyles.spaceBetweenDescriptionAndPorts;
  403. }
  404. foreach (var port in footerPorts)
  405. {
  406. OnFooterPortGUI(position.VerticalSection(ref y, GetFooterPortHeight(position.width, port)), port);
  407. y += FooterStyles.spaceBetweenPorts;
  408. }
  409. if (hasPorts)
  410. {
  411. y -= FooterStyles.spaceBetweenPorts;
  412. }
  413. y += FooterStyles.padding;
  414. }
  415. private float GetFooterSummaryHeight(float width)
  416. {
  417. return FooterStyles.description.CalcHeight(new GUIContent(summary), width);
  418. }
  419. private void OnFooterSummaryGUI(Rect position)
  420. {
  421. EditorGUI.LabelField(position, summary, FooterStyles.description);
  422. }
  423. private string GetFooterPortLabel(IUnitPort port)
  424. {
  425. string type;
  426. if (port is ValueInput)
  427. {
  428. type = ((IUnitValuePort)port).type.DisplayName() + " Input";
  429. }
  430. else if (port is ValueOutput)
  431. {
  432. type = ((IUnitValuePort)port).type.DisplayName() + " Output";
  433. }
  434. else if (port is ControlInput)
  435. {
  436. type = "Trigger Input";
  437. }
  438. else if (port is ControlOutput)
  439. {
  440. type = "Trigger Output";
  441. }
  442. else
  443. {
  444. throw new NotSupportedException();
  445. }
  446. var portDescription = PortDescription(port);
  447. if (!StringUtility.IsNullOrWhiteSpace(portDescription.summary))
  448. {
  449. return $"<b>{portDescription.label}:</b> {portDescription.summary} {LudiqGUIUtility.DimString($"({type})")}";
  450. }
  451. else
  452. {
  453. return $"<b>{portDescription.label}:</b> {LudiqGUIUtility.DimString($"({type})")}";
  454. }
  455. }
  456. private float GetFooterPortDescriptionHeight(float width, IUnitPort port)
  457. {
  458. return FooterStyles.portDescription.CalcHeight(new GUIContent(GetFooterPortLabel(port)), width);
  459. }
  460. private void OnFooterPortDescriptionGUI(Rect position, IUnitPort port)
  461. {
  462. GUI.Label(position, GetFooterPortLabel(port), FooterStyles.portDescription);
  463. }
  464. private float GetFooterPortHeight(float width, IUnitPort port)
  465. {
  466. var descriptionWidth = width - FooterStyles.portIconSize - FooterStyles.spaceAfterPortIcon;
  467. return GetFooterPortDescriptionHeight(descriptionWidth, port);
  468. }
  469. private void OnFooterPortGUI(Rect position, IUnitPort port)
  470. {
  471. var iconPosition = new Rect
  472. (
  473. position.x,
  474. position.y,
  475. FooterStyles.portIconSize,
  476. FooterStyles.portIconSize
  477. );
  478. var descriptionWidth = position.width - FooterStyles.portIconSize - FooterStyles.spaceAfterPortIcon;
  479. var descriptionPosition = new Rect
  480. (
  481. iconPosition.xMax + FooterStyles.spaceAfterPortIcon,
  482. position.y,
  483. descriptionWidth,
  484. GetFooterPortDescriptionHeight(descriptionWidth, port)
  485. );
  486. var portDescription = PortDescription(port);
  487. var icon = portDescription.icon?[FooterStyles.portIconSize];
  488. if (icon != null)
  489. {
  490. GUI.DrawTexture(iconPosition, icon);
  491. }
  492. OnFooterPortDescriptionGUI(descriptionPosition, port);
  493. }
  494. public static class FooterStyles
  495. {
  496. static FooterStyles()
  497. {
  498. description = new GUIStyle(EditorStyles.label);
  499. description.padding = new RectOffset(0, 0, 0, 0);
  500. description.wordWrap = true;
  501. description.richText = true;
  502. portDescription = new GUIStyle(EditorStyles.label);
  503. portDescription.padding = new RectOffset(0, 0, 0, 0);
  504. portDescription.wordWrap = true;
  505. portDescription.richText = true;
  506. portDescription.imagePosition = ImagePosition.TextOnly;
  507. }
  508. public static readonly GUIStyle description;
  509. public static readonly GUIStyle portDescription;
  510. public static readonly float spaceAfterUnitIcon = 7;
  511. public static readonly int unitIconSize = IconSize.Medium;
  512. public static readonly float spaceAfterPortIcon = 6;
  513. public static readonly int portIconSize = IconSize.Small;
  514. public static readonly float spaceBetweenDescriptionAndPorts = 8;
  515. public static readonly float spaceBetweenPorts = 8;
  516. public static readonly float padding = 8;
  517. }
  518. #endregion
  519. }
  520. }