PropertyLine.ts 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494
  1. module INSPECTOR {
  2. export class PropertyFormatter {
  3. /**
  4. * Format the value of the given property of the given object.
  5. */
  6. public static format(obj: any, prop: string): string {
  7. // Get original value;
  8. let value = obj[prop];
  9. // test if type PrimitiveAlignment is available (only included in canvas2d)
  10. return value;
  11. }
  12. }
  13. /**
  14. * A property line represents a line in the detail panel. This line is composed of :
  15. * - a name (the property name)
  16. * - a value if this property is of a type 'simple' : string, number, boolean, color, texture
  17. * - the type of the value if this property is of a complex type (Vector2, Size, ...)
  18. * - a ID if defined (otherwise an empty string is displayed)
  19. * The original object is sent to the value object who will update it at will.
  20. *
  21. * A property line can contain OTHER property line objects in the case of a complex type.
  22. * If this instance has no link to other instances, its type is ALWAYS a simple one (see above).
  23. *
  24. */
  25. export class PropertyLine {
  26. // The property can be of any type (Property internally can have any type), relative to this._obj
  27. private _property: Property;
  28. //The HTML element corresponding to this line
  29. private _div: HTMLElement;
  30. // The div containing the value to display. Used to update dynamically the property
  31. private _valueDiv: HTMLElement;
  32. // If the type is complex, this property will have child to update
  33. private _children: Array<PropertyLine> = [];
  34. // Array representing the simple type. All others are considered 'complex'
  35. private static _SIMPLE_TYPE = ['number', 'string', 'boolean'];
  36. // The number of pixel at each children step
  37. private static _MARGIN_LEFT = 15;
  38. // The margin-left used to display to row
  39. private _level: number;
  40. /** The list of viewer element displayed at the end of the line (color, texture...) */
  41. private _elements: Array<BasicElement> = [];
  42. /** The property parent of this one. Used to update the value of this property and to retrieve the correct object */
  43. private _parent: BABYLON.Nullable<PropertyLine>;
  44. /** The input element to display if this property is 'simple' in order to update it */
  45. private _input: HTMLInputElement;
  46. /** Display input handler (stored to be removed afterwards) */
  47. private _displayInputHandler: EventListener;
  48. /** Handler used to validate the input by pressing 'enter' */
  49. private _validateInputHandler: EventListener;
  50. /** Handler used to validate the input by pressing 'esc' */
  51. private _escapeInputHandler: EventListener;
  52. /** Handler used on focus out */
  53. private _focusOutInputHandler: EventListener;
  54. /** Handler used to get mouse position */
  55. private _onMouseDownHandler: EventListener;
  56. private _onMouseDragHandler: EventListener;
  57. private _onMouseUpHandler: EventListener;
  58. private _textValue: HTMLElement;
  59. /** Save previous Y mouse position */
  60. private _prevY: number;
  61. /**Save value while slider is on */
  62. private _preValue: number;
  63. constructor(prop: Property, parent: BABYLON.Nullable<PropertyLine> = null, level: number = 0) {
  64. this._property = prop;
  65. this._level = level;
  66. this._parent = parent;
  67. this._div = Helpers.CreateDiv('row');
  68. this._div.style.marginLeft = `${this._level}px`;
  69. // Property name
  70. let propName: HTMLElement = Helpers.CreateDiv('prop-name', this._div);
  71. propName.textContent = `${this.name}`;
  72. // Value
  73. this._valueDiv = Helpers.CreateDiv('prop-value', this._div);
  74. if (typeof this.value !== 'boolean' && !this._isSliderType()) {
  75. this._valueDiv.textContent = this._displayValueContent() || '-'; // Init value text node
  76. }
  77. this._createElements();
  78. for (let elem of this._elements) {
  79. this._valueDiv.appendChild(elem.toHtml());
  80. }
  81. this._updateValue();
  82. // If the property type is not simple, add click event to unfold its children
  83. if (typeof this.value === 'boolean') {
  84. this._checkboxInput();
  85. } else if (this._isSliderType()) {
  86. this._rangeInput();
  87. } else if (!this._isSimple()) {
  88. this._valueDiv.classList.add('clickable');
  89. this._valueDiv.addEventListener('click', this._addDetails.bind(this));
  90. } else {
  91. this._initInput();
  92. this._valueDiv.addEventListener('click', this._displayInputHandler);
  93. this._input.addEventListener('focusout', this._focusOutInputHandler);
  94. this._input.addEventListener('keydown', this._validateInputHandler);
  95. this._input.addEventListener('keydown', this._escapeInputHandler);
  96. }
  97. // Add this property to the scheduler
  98. Scheduler.getInstance().add(this);
  99. }
  100. /**
  101. * Init the input element and al its handler :
  102. * - a click in the window remove the input and restore the old property value
  103. * - enters updates the property
  104. */
  105. private _initInput() {
  106. // Create the input element
  107. this._input = document.createElement('input') as HTMLInputElement;
  108. this._input.setAttribute('type', 'text');
  109. // if the property is 'simple', add an event listener to create an input
  110. this._displayInputHandler = this._displayInput.bind(this);
  111. this._validateInputHandler = this._validateInput.bind(this);
  112. this._escapeInputHandler = this._escapeInput.bind(this);
  113. this._focusOutInputHandler = this.update.bind(this);
  114. this._onMouseDownHandler = this._onMouseDown.bind(this);
  115. this._onMouseDragHandler = this._onMouseDrag.bind(this);
  116. this._onMouseUpHandler = this._onMouseUp.bind(this);
  117. }
  118. /**
  119. * On enter : validates the new value and removes the input
  120. * On escape : removes the input
  121. */
  122. private _validateInput(e: KeyboardEvent) {
  123. this._input.removeEventListener('focusout', this._focusOutInputHandler);
  124. if (e.keyCode == 13) { // Enter
  125. this.validateInput(this._input.value);
  126. } else if (e.keyCode == 9) { // Tab
  127. e.preventDefault();
  128. this.validateInput(this._input.value);
  129. } else if (e.keyCode == 27) {
  130. // Esc : remove input
  131. this.update();
  132. }
  133. }
  134. public validateInput(value: any, forceupdate: boolean = true): void {
  135. this.updateObject();
  136. if (typeof this._property.value === 'number') {
  137. this._property.value = parseFloat(value);
  138. } else {
  139. this._property.value = value;
  140. }
  141. // Remove input
  142. if (forceupdate) {
  143. this.update();
  144. // resume scheduler
  145. Scheduler.getInstance().pause = false;
  146. }
  147. }
  148. /**
  149. * On escape : removes the input
  150. */
  151. private _escapeInput(e: KeyboardEvent) {
  152. // Remove focus out handler
  153. this._input.removeEventListener('focusout', this._focusOutInputHandler);
  154. if (e.keyCode == 27) {
  155. // Esc : remove input
  156. this.update();
  157. }
  158. }
  159. /** Removes the input without validating the new value */
  160. private _removeInputWithoutValidating() {
  161. Helpers.CleanDiv(this._valueDiv);
  162. if (typeof this.value !== 'boolean' && !this._isSliderType()) {
  163. this._valueDiv.textContent = "-";
  164. }
  165. // restore elements
  166. for (let elem of this._elements) {
  167. this._valueDiv.appendChild(elem.toHtml());
  168. }
  169. if (typeof this.value !== 'boolean' && !this._isSliderType()) {
  170. this._valueDiv.addEventListener('click', this._displayInputHandler);
  171. }
  172. }
  173. /** Replaces the default display with an input */
  174. private _displayInput(e: any) {
  175. // Remove the displayInput event listener
  176. this._valueDiv.removeEventListener('click', this._displayInputHandler);
  177. // Set input value
  178. let valueTxt = this._valueDiv.textContent;
  179. this._valueDiv.textContent = "";
  180. this._input.value = valueTxt || "";
  181. this._valueDiv.appendChild(this._input);
  182. this._input.focus();
  183. if (typeof this.value !== 'boolean' && !this._isSliderType()) {
  184. this._input.addEventListener('focusout', this._focusOutInputHandler);
  185. } else if (typeof this.value === 'number') {
  186. this._input.addEventListener('mousedown', this._onMouseDownHandler);
  187. }
  188. // Pause the scheduler
  189. Scheduler.getInstance().pause = true;
  190. }
  191. /** Retrieve the correct object from its parent.
  192. * If no parent exists, returns the property value.
  193. * This method is used at each update in case the property object is removed from the original object
  194. * (example : mesh.position = new BABYLON.Vector3 ; the original vector3 object is deleted from the mesh).
  195. */
  196. public updateObject() {
  197. if (this._parent) {
  198. this._property.obj = this._parent.updateObject();
  199. }
  200. return this._property.value;
  201. }
  202. // Returns the property name
  203. public get name(): string {
  204. // let arrayName = Helpers.Capitalize(this._property.name).match(/[A-Z][a-z]+|[0-9]+/g)
  205. // if (arrayName) {
  206. // return arrayName.join(" ");
  207. // }
  208. return this._property.name;
  209. }
  210. // Returns the value of the property
  211. public get value(): any {
  212. return PropertyFormatter.format(this._property.obj, this._property.name);
  213. }
  214. // Returns the type of the property
  215. public get type(): string {
  216. return this._property.type;
  217. }
  218. /**
  219. * Creates elements that wil be displayed on a property line, depending on the
  220. * type of the property.
  221. */
  222. private _createElements() {
  223. // Colors
  224. if (this.type == 'Color3' || this.type == 'Color4') {
  225. this._elements.push(new ColorPickerElement(this.value, this));
  226. //this._elements.push(new ColorElement(this.value));
  227. }
  228. // Texture
  229. if (this.type == 'Texture') {
  230. this._elements.push(new TextureElement(this.value));
  231. }
  232. // HDR Texture
  233. if (this.type == 'HDRCubeTexture') {
  234. this._elements.push(new HDRCubeTextureElement(this.value));
  235. }
  236. if (this.type == 'CubeTexture') {
  237. this._elements.push(new CubeTextureElement(this.value));
  238. }
  239. }
  240. // Returns the text displayed on the left of the property name :
  241. // - If the type is simple, display its value
  242. // - If the type is complex, but instance of Vector2, Size, display the type and its tostring
  243. // - If the type is another one, display the Type
  244. private _displayValueContent() {
  245. let value = this.value;
  246. // If the value is a number, truncate it if needed
  247. if (typeof value === 'number') {
  248. return Helpers.Trunc(value);
  249. }
  250. // If it's a string or a boolean, display its value
  251. if (typeof value === 'string' || typeof value === 'boolean') {
  252. return value;
  253. }
  254. return PROPERTIES.format(value);
  255. }
  256. /** Delete properly this property line.
  257. * Removes itself from the scheduler.
  258. * Dispose all viewer element (color, texture...)
  259. */
  260. public dispose() {
  261. // console.log('delete properties', this.name);
  262. Scheduler.getInstance().remove(this);
  263. for (let child of this._children) {
  264. // console.log('delete properties', child.name);
  265. Scheduler.getInstance().remove(child);
  266. }
  267. for (let elem of this._elements) {
  268. elem.dispose();
  269. }
  270. this._elements = [];
  271. }
  272. /** Updates the content of _valueDiv with the value of the property,
  273. * and all HTML element correpsonding to this type.
  274. * Elements are updated as well
  275. */
  276. private _updateValue() {
  277. // Update the property object first
  278. this.updateObject();
  279. // Then update its value
  280. // this._valueDiv.textContent = " "; // TOFIX this removes the elements after
  281. if (typeof this.value === 'boolean') {
  282. this._checkboxInput();
  283. } else if (this._isSliderType()) { // Add slider when parent have slider property
  284. this._rangeInput();
  285. } else {
  286. this._valueDiv.childNodes[0].nodeValue = this._displayValueContent();
  287. }
  288. for (let elem of this._elements) {
  289. elem.update(this.value);
  290. }
  291. }
  292. /**
  293. * Update the property division with the new property value.
  294. * If this property is complex, update its child, otherwise update its text content
  295. */
  296. public update() {
  297. this._removeInputWithoutValidating();
  298. this._updateValue();
  299. }
  300. /**
  301. * Returns true if the type of this property is simple, false otherwise.
  302. * Returns true if the value is null
  303. */
  304. private _isSimple(): boolean {
  305. if (this.value != null && this.type !== 'type_not_defined') {
  306. if (PropertyLine._SIMPLE_TYPE.indexOf(this.type) == -1) {
  307. // complex type : return the type name
  308. return false;
  309. } else {
  310. // simple type : return value
  311. return true;
  312. }
  313. } else {
  314. return true;
  315. }
  316. }
  317. public toHtml(): HTMLElement {
  318. return this._div;
  319. }
  320. public closeDetails() {
  321. if (this._div.classList.contains('unfolded')) {
  322. // Remove class unfolded
  323. this._div.classList.remove('unfolded');
  324. // remove html children
  325. if (this._div.parentNode) {
  326. for (let child of this._children) {
  327. this._div.parentNode.removeChild(child.toHtml());
  328. }
  329. }
  330. }
  331. }
  332. /**
  333. * Add sub properties in case of a complex type
  334. */
  335. private _addDetails() {
  336. if (this._div.classList.contains('unfolded')) {
  337. // Remove class unfolded
  338. this._div.classList.remove('unfolded');
  339. // remove html children
  340. if (this._div.parentNode) {
  341. for (let child of this._children) {
  342. this._div.parentNode.removeChild(child.toHtml());
  343. }
  344. }
  345. } else {
  346. // if children does not exists, generate it
  347. this._div.classList.toggle('unfolded');
  348. if (this._children.length == 0) {
  349. let objToDetail = this.value;
  350. // Display all properties that are not functions
  351. let propToDisplay = Helpers.GetAllLinesPropertiesAsString(objToDetail);
  352. propToDisplay.sort().reverse();
  353. for (let prop of propToDisplay) {
  354. let infos = new Property(prop, this._property.value);
  355. let child = new PropertyLine(infos, this, this._level + PropertyLine._MARGIN_LEFT);
  356. this._children.push(child);
  357. }
  358. }
  359. // otherwise display it
  360. if (this._div.parentNode) {
  361. for (let child of this._children) {
  362. this._div.parentNode.insertBefore(child.toHtml(), this._div.nextSibling);
  363. }
  364. }
  365. }
  366. }
  367. /**
  368. * Refresh mouse position on y axis
  369. * @param e
  370. */
  371. private _onMouseDrag(e: MouseEvent): void {
  372. const diff = this._prevY - e.clientY;
  373. this._input.value = (this._preValue + diff).toString();
  374. }
  375. /**
  376. * Save new value from slider
  377. * @param e
  378. */
  379. private _onMouseUp(e: MouseEvent): void {
  380. window.removeEventListener('mousemove', this._onMouseDragHandler);
  381. window.removeEventListener('mouseup', this._onMouseUpHandler);
  382. this._prevY = e.clientY;
  383. }
  384. /**
  385. * Start record mouse position
  386. * @param e
  387. */
  388. private _onMouseDown(e: MouseEvent): void {
  389. this._prevY = e.clientY;
  390. this._preValue = this.value;
  391. window.addEventListener('mousemove', this._onMouseDragHandler);
  392. window.addEventListener('mouseup', this._onMouseUpHandler);
  393. }
  394. /**
  395. * Create input entry
  396. */
  397. private _checkboxInput() {
  398. if (this._valueDiv.childElementCount < 1) { // Prevent display two checkbox
  399. this._input = Helpers.CreateInput('checkbox-element', this._valueDiv);
  400. this._input.type = 'checkbox'
  401. this._input.checked = this.value;
  402. this._input.addEventListener('change', () => {
  403. Scheduler.getInstance().pause = true;
  404. this.validateInput(!this.value)
  405. })
  406. }
  407. }
  408. private _rangeInput() {
  409. if (this._valueDiv.childElementCount < 1) { // Prevent display two input range
  410. this._input = Helpers.CreateInput('slider-element', this._valueDiv);
  411. this._input.type = 'range';
  412. this._input.style.display = 'inline-block';
  413. this._input.min = this._getSliderProperty().min;
  414. this._input.max = this._getSliderProperty().max;
  415. this._input.step = this._getSliderProperty().step;
  416. this._input.value = this.value;
  417. this._validateInputHandler = this._rangeHandler.bind(this)
  418. this._input.addEventListener('input', this._validateInputHandler)
  419. this._input.addEventListener('change', () => {
  420. Scheduler.getInstance().pause = false;
  421. })
  422. this._textValue = Helpers.CreateDiv('value-text', this._valueDiv);
  423. this._textValue.innerText = Helpers.Trunc(this.value).toString();
  424. this._textValue.style.paddingLeft = '10px';
  425. this._textValue.style.display = 'inline-block';
  426. }
  427. }
  428. private _rangeHandler() {
  429. Scheduler.getInstance().pause = true;
  430. //this._input.style.backgroundSize = ((parseFloat(this._input.value) - parseFloat(this._input.min)) * 100 / ( parseFloat(this._input.max) - parseFloat(this._input.min))) + '% 100%'
  431. this._textValue.innerText = this._input.value;
  432. this.validateInput(this._input.value, false);
  433. }
  434. private _isSliderType() { //Check if property have slider definition
  435. return this._property &&
  436. PROPERTIES.hasOwnProperty(this._property.obj.constructor.name) &&
  437. (<any>PROPERTIES)[this._property.obj.constructor.name].hasOwnProperty('slider') &&
  438. (<any>PROPERTIES)[this._property.obj.constructor.name].slider.hasOwnProperty(this.name);
  439. }
  440. private _getSliderProperty() {
  441. return (<any>PROPERTIES)[this._property.obj.constructor.name].slider[this.name]
  442. }
  443. }
  444. }