PropsEditor.hx 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537
  1. package hide.comp;
  2. import hrt.prefab.Props;
  3. class PropsEditor extends Component {
  4. public var undo : hide.ui.UndoHistory;
  5. public var lastChange : Float = 0.;
  6. public var fields(default, null) : Array<PropsField>;
  7. public var isTempChange = false;
  8. public function new(?undo,?parent,?el) {
  9. super(parent,el);
  10. element.addClass("hide-properties");
  11. this.undo = undo == null ? new hide.ui.UndoHistory() : undo;
  12. fields = [];
  13. }
  14. public function clear() {
  15. element.empty();
  16. fields = [];
  17. }
  18. public function onDragDrop( items : Array<String>, isDrop : Bool ) : Bool {
  19. if( items.length == 0 )
  20. return false;
  21. var pickedEl = js.Browser.document.elementFromPoint(ide.mouseX, ide.mouseY);
  22. var rootEl = element[0];
  23. while( pickedEl != null ) {
  24. if( pickedEl == rootEl )
  25. return false;
  26. for( field in fields ) {
  27. if( field.tselect != null && field.tselect.element[0] == pickedEl )
  28. return field.tselect.onDragDrop(items, isDrop);
  29. if( field.fselect != null && field.fselect.element[0] == pickedEl )
  30. return field.fselect.onDragDrop(items, isDrop);
  31. }
  32. pickedEl = pickedEl.parentElement;
  33. }
  34. return false;
  35. }
  36. public function addMaterial( m : h3d.mat.Material, ?parent : Element, ?onChange ) {
  37. var def = m.editProps();
  38. def = add(def, m.props, function(name) {
  39. m.refreshProps();
  40. if( !isTempChange ) {
  41. def.remove();
  42. addMaterial(m, parent, onChange);
  43. if( onChange != null ) onChange(name);
  44. }
  45. });
  46. if( parent != null && parent.length != 0 )
  47. def.appendTo(parent);
  48. }
  49. public static function makePropEl(p: PropDef, parent: Element) {
  50. switch( p.t ) {
  51. case PInt(min, max):
  52. var e = new Element('<input type="range" field="${p.name}" step="1">').appendTo(parent);
  53. if( min != null ) e.attr("min", "" + min);
  54. if(p.def != null) e.attr("value", "" + p.def);
  55. e.attr("max", "" + (max == null ? 100 : max));
  56. case PFloat(min, max):
  57. var e = new Element('<input type="range" field="${p.name}">').appendTo(parent);
  58. if(p.def != null) e.attr("value", "" + p.def);
  59. if( min != null ) e.attr("min", "" + min);
  60. if( max != null ) e.attr("max", "" + max);
  61. case PBool:
  62. new Element('<input type="checkbox" field="${p.name}">').appendTo(parent);
  63. case PTexture:
  64. new Element('<input type="texturepath" field="${p.name}">').appendTo(parent);
  65. case PUnsupported(text):
  66. new Element('<font color="red">' + StringTools.htmlEscape(text) + '</font>').appendTo(parent);
  67. case PVec(n, min, max):
  68. var isColor = p.name.toLowerCase().indexOf("color") >= 0;
  69. if(isColor && (n == 3 || n == 4)) {
  70. new Element('<input type="color" field="${p.name}">').appendTo(parent);
  71. }
  72. else {
  73. var row = new Element('<div class="flex"/>').appendTo(parent);
  74. for( i in 0...n ) {
  75. var e = new Element('<input type="number" field="${p.name}.$i">').appendTo(row);
  76. if(min == null) min = isColor ? 0.0 : -1.0;
  77. if(max == null) max = 1.0;
  78. e.attr("min", "" + min);
  79. e.attr("max", "" + max);
  80. }
  81. }
  82. case PChoice(choices):
  83. var e = new Element('<select field="${p.name}" type="number"></select>').appendTo(parent);
  84. for(c in choices)
  85. new hide.Element('<option>').attr("value", choices.indexOf(c)).text(upperCase(c)).appendTo(e);
  86. case PEnum(en):
  87. var e = new Element('<select field="${p.name}"></select>').appendTo(parent);
  88. case PFile(exts):
  89. new Element('<input type="texturepath" extensions="${exts.join(" ")}" field="${p.name}">').appendTo(parent);
  90. case PString(len):
  91. var e = new Element('<input type="text" field="${p.name}">').appendTo(parent);
  92. if ( len != null ) e.attr("maxlength", "" + len);
  93. if ( p.def != null ) e.attr("value", "" + p.def);
  94. }
  95. }
  96. public static function makeGroupEl(name: String, content: Element) {
  97. var el = new Element('<div class="group" name="${name}"></div>');
  98. content.appendTo(el);
  99. return el;
  100. }
  101. public static function makeSectionEl(name: String, content: Element, ?headerContent: Element) {
  102. var el = new Element('<div class="section"><h1><span>${name}</span></h1><div class="content"></div></div>');
  103. if (headerContent != null) headerContent.appendTo(el.find("h1"));
  104. content.appendTo(el.find(".content"));
  105. return el;
  106. }
  107. public static function makeLabelEl(name: String, content: Element) {
  108. var el = new Element('<span><dt>${name}</dt><dd></dd></span>');
  109. content.appendTo(el.find("dd"));
  110. return el;
  111. }
  112. public static function makeListEl(content:Array<Element>) {
  113. var el = new Element("<dl>");
  114. for ( e in content ) e.appendTo(el);
  115. return el;
  116. }
  117. static function upperCase(prop: String) {
  118. return prop.charAt(0).toUpperCase() + prop.substr(1);
  119. }
  120. public static function makePropsList(props : Array<PropDef>) : Element {
  121. var e = new Element('<dl>');
  122. for( p in props ) {
  123. new Element('<dt>${p.disp != null ? p.disp : upperCase(p.name)}</dt>').appendTo(e);
  124. var def = new Element('<dd>').appendTo(e);
  125. makePropEl(p, def);
  126. }
  127. return e;
  128. }
  129. public function addProps( props : Array<PropDef>, context : Dynamic, ?onChange : String -> Void) {
  130. var e = makePropsList(props);
  131. return add(e, context, onChange);
  132. }
  133. public function add( e : Element, ?context : Dynamic, ?onChange : String -> Void ) {
  134. e.appendTo(element);
  135. return build(e,context,onChange);
  136. }
  137. public function build( e : Element, ?context : Dynamic, ?onChange : String -> Void ) {
  138. e = e.wrap("<div></div>").parent(); // necessary to have find working on top level element
  139. e.find("input[type=checkbox]").wrap("<div class='checkbox-wrapper'></div>");
  140. e.find("input[type=range]").not("[step]").attr({step: "any", tabindex:"-1"});
  141. // Wrap dt+dd for nw versions of 0.4x+
  142. for ( el in e.find("dt").wrap("<div></div>").parent().elements() ) {
  143. var n = el.next();
  144. if (n.length != 0 && n[0].tagName == "DD") n.appendTo(el);
  145. }
  146. // -- reload states ---
  147. for( h in e.find(".section > h1").elements() )
  148. if( getDisplayState("section:" + StringTools.trim(h.text())) != false )
  149. h.parent().addClass("open");
  150. // init section
  151. e.find(".section").not(".open").children(".content").hide();
  152. e.find(".section > h1").mousedown(function(e) {
  153. if( e.button != 0 ) return;
  154. var section = e.getThis().parent();
  155. section.toggleClass("open");
  156. section.children(".content").slideToggle(100);
  157. saveDisplayState("section:" + StringTools.trim(e.getThis().text()), section.hasClass("open"));
  158. }).find("input").mousedown(function(e) e.stopPropagation());
  159. e.find("input[type=section_name]").change(function(e) {
  160. e.getThis().closest(".section").find(">h1 span").text(e.getThis().val());
  161. });
  162. // init groups
  163. var gindex = 0;
  164. for( g in e.find(".group").elements() ) {
  165. var name = g.attr("name");
  166. g.wrapInner("<div class='content'></div>");
  167. if( name != null )
  168. new Element("<div class='title'>" + g.attr("name") + '</div>').prependTo(g);
  169. else {
  170. name = "_g"+(gindex++);
  171. g.attr("name",name);
  172. g.children().children(".title").prependTo(g);
  173. }
  174. var s = g.closest(".section");
  175. var key = (s.length == 0 ? "" : StringTools.trim(s.children("h1").text()) + "/") + name;
  176. if( getDisplayState("group:" + key) != false && !g.hasClass("closed") )
  177. g.addClass("open");
  178. }
  179. e.find(".group").not(".open").children(".content").hide();
  180. e.find(".group > .title").mousedown(function(e) {
  181. if( e.button != 0 ) return;
  182. var group = e.getThis().parent();
  183. group.toggleClass("open");
  184. group.children(".content").slideToggle(100);
  185. var s = group.closest(".section");
  186. var key = (s.length == 0 ? "" : StringTools.trim(s.children("h1").text()) + "/") + group.attr("name");
  187. saveDisplayState("group:" + key, group.hasClass("open"));
  188. }).find("input").mousedown(function(e) e.stopPropagation());
  189. e.find("input[type=group_name]").change(function(e) {
  190. e.getThis().closest(".group").find(">.title").val(e.getThis().val());
  191. });
  192. // init input reflection
  193. for( f in e.find("[field]").elements() ) {
  194. var f = new PropsField(this, f, context);
  195. f.onChange = function(undo) {
  196. isTempChange = f.isTempChange;
  197. lastChange = haxe.Timer.stamp();
  198. if( onChange != null ) onChange(@:privateAccess f.fname);
  199. isTempChange = false;
  200. };
  201. fields.push(f);
  202. // Init reset buttons
  203. var def = f.element.attr("value");
  204. if(def != null) {
  205. var dd = f.element.parent().parent("dd");
  206. var dt = dd.prev("dt");
  207. var tooltip = 'Click to reset ($def)\nCtrl+Click to round';
  208. var button = dt.wrapInner('<input type="button" tabindex="-1" value="${upperCase(dt.text())}" title="$tooltip"/>');
  209. button.click(function(e) {
  210. var range = @:privateAccess f.range;
  211. if(range != null) {
  212. if(e.ctrlKey) {
  213. range.value = Math.round(range.value);
  214. range.onChange(false);
  215. }
  216. else
  217. range.reset();
  218. }
  219. });
  220. }
  221. }
  222. return e;
  223. }
  224. }
  225. @:allow(hide.comp.PropsEditor)
  226. class PropsField extends Component {
  227. public var fname : String;
  228. var isTempChange : Bool;
  229. var props : PropsEditor;
  230. var context : Dynamic;
  231. var current : Dynamic;
  232. var enumValue : Enum<Dynamic>;
  233. var tempChange : Bool;
  234. var beforeTempChange : { value : Dynamic };
  235. var tselect : hide.comp.TextureSelect;
  236. var fselect : hide.comp.FileSelect;
  237. var viewRoot : Element;
  238. var range : hide.comp.Range;
  239. public function new(props, el, context) {
  240. super(null,el);
  241. viewRoot = element.closest(".lm_content");
  242. this.props = props;
  243. this.context = context;
  244. var f = element;
  245. Reflect.setField(f[0],"propsField", this);
  246. fname = f.attr("field");
  247. current = getFieldValue();
  248. switch( f.attr("type") ) {
  249. case "checkbox":
  250. f.prop("checked", current);
  251. f.mousedown(function(e) e.stopPropagation());
  252. f.change(function(_) {
  253. undo(function() {
  254. var f = resolveField();
  255. f.current = getFieldValue();
  256. f.element.prop("checked", f.current);
  257. f.onChange(true);
  258. });
  259. current = f.prop("checked");
  260. setFieldValue(current);
  261. onChange(false);
  262. });
  263. return;
  264. case "texture":
  265. tselect = new hide.comp.TextureSelect(null,f);
  266. tselect.value = current;
  267. tselect.onChange = function() {
  268. undo(function() {
  269. var f = resolveField();
  270. f.current = getFieldValue();
  271. f.tselect.value = f.current;
  272. f.onChange(true);
  273. });
  274. current = tselect.value;
  275. setFieldValue(current);
  276. onChange(false);
  277. }
  278. return;
  279. case "texturepath":
  280. tselect = new hide.comp.TextureSelect(null,f);
  281. tselect.path = current;
  282. tselect.onChange = function() {
  283. undo(function() {
  284. var f = resolveField();
  285. f.current = getFieldValue();
  286. f.tselect.path = f.current;
  287. f.onChange(true);
  288. });
  289. current = tselect.path;
  290. setFieldValue(current);
  291. onChange(false);
  292. }
  293. return;
  294. case "model":
  295. fselect = new hide.comp.FileSelect(["hmd", "fbx"], null, f);
  296. fselect.path = current;
  297. fselect.onChange = function() {
  298. undo(function() {
  299. var f = resolveField();
  300. f.current = getFieldValue();
  301. f.fselect.path = f.current;
  302. f.onChange(true);
  303. });
  304. current = fselect.path;
  305. setFieldValue(current);
  306. onChange(false);
  307. };
  308. return;
  309. case "fileselect":
  310. var exts = f.attr("extensions");
  311. if( exts == null ) exts = "*";
  312. fselect = new hide.comp.FileSelect(exts.split(" "), null, f);
  313. fselect.path = current;
  314. fselect.onChange = function() {
  315. undo(function() {
  316. var f = resolveField();
  317. f.current = getFieldValue();
  318. f.fselect.path = f.current;
  319. f.onChange(true);
  320. });
  321. current = fselect.path;
  322. setFieldValue(current);
  323. onChange(false);
  324. };
  325. return;
  326. case "range":
  327. range = new hide.comp.Range(null,f);
  328. if(!Math.isNaN(current))
  329. range.value = current;
  330. range.onChange = function(temp) {
  331. tempChange = temp;
  332. setVal(range.value);
  333. };
  334. return;
  335. case "color":
  336. var arr = Std.downcast(current, Array);
  337. var alpha = arr != null && arr.length == 4 || f.attr("alpha") == "true";
  338. var picker = new hide.comp.ColorPicker(alpha, null, f);
  339. function updatePicker(val: Dynamic) {
  340. if(arr != null) {
  341. var v = h3d.Vector.fromArray(val);
  342. picker.value = v.toColor();
  343. }
  344. else if(!Math.isNaN(val))
  345. picker.value = val;
  346. }
  347. updatePicker(current);
  348. picker.onChange = function(move) {
  349. if(!move) {
  350. undo(function() {
  351. var f = resolveField();
  352. f.current = getFieldValue();
  353. updatePicker(f.current);
  354. f.onChange(true);
  355. });
  356. }
  357. var newVal : Dynamic =
  358. if(arr != null) {
  359. var vec = h3d.Vector.fromColor(picker.value);
  360. if(alpha)
  361. [vec.x, vec.y, vec.z, vec.w];
  362. else
  363. [vec.x, vec.y, vec.z];
  364. }
  365. else picker.value;
  366. if(!move)
  367. current = newVal;
  368. setFieldValue(newVal);
  369. onChange(false);
  370. };
  371. return;
  372. default:
  373. if( f.is("select") ) {
  374. enumValue = Type.getEnum(current);
  375. if( enumValue != null && f.find("option").length == 0 ) {
  376. for( c in enumValue.getConstructors() )
  377. new Element('<option value="$c">$c</option>').appendTo(f);
  378. }
  379. }
  380. if( enumValue != null ) {
  381. var cst = Type.enumConstructor(current);
  382. f.val(cst);
  383. } else
  384. f.val(current);
  385. f.keyup(function(e) {
  386. if( e.keyCode == 13 ) {
  387. f.blur();
  388. return;
  389. }
  390. if( e.keyCode == 27 ) {
  391. f.blur();
  392. return;
  393. }
  394. tempChange = true;
  395. f.change();
  396. });
  397. f.change(function(e) {
  398. var newVal : Dynamic = f.val();
  399. if( f.is("[type=number]") )
  400. newVal = Std.parseFloat(newVal);
  401. if( enumValue != null )
  402. newVal = Type.createEnum(enumValue, newVal);
  403. if( f.is("select") )
  404. f.blur();
  405. setVal(newVal);
  406. });
  407. }
  408. }
  409. function getAccess() : { obj : Dynamic, index : Int, name : String } {
  410. var obj : Dynamic = context;
  411. var path = fname.split(".");
  412. var field = path.pop();
  413. for( p in path ) {
  414. var index = Std.parseInt(p);
  415. if( index != null )
  416. obj = obj[index];
  417. else
  418. obj = Reflect.getProperty(obj, p);
  419. }
  420. var index = Std.parseInt(field);
  421. if( index != null )
  422. return { obj : obj, index : index, name : null };
  423. return { obj : obj, index : -1, name : field };
  424. }
  425. function getFieldValue() {
  426. var a = getAccess();
  427. if( a.name != null )
  428. return Reflect.getProperty(a.obj, a.name);
  429. return a.obj[a.index];
  430. }
  431. function setFieldValue( value : Dynamic ) {
  432. var a = getAccess();
  433. if( a.name != null )
  434. Reflect.setProperty(a.obj, a.name, value);
  435. else
  436. a.obj[a.index] = value;
  437. }
  438. function undo( f : Void -> Void ) {
  439. var a = getAccess();
  440. if( a.name != null )
  441. props.undo.change(Field(a.obj, a.name, current), f);
  442. else
  443. props.undo.change(Array(a.obj, a.index, current), f);
  444. }
  445. function setVal(v) {
  446. if( current == v ) {
  447. // delay history save until last change
  448. if( tempChange || beforeTempChange == null )
  449. return;
  450. current = beforeTempChange.value;
  451. beforeTempChange = null;
  452. }
  453. isTempChange = tempChange;
  454. if( tempChange ) {
  455. tempChange = false;
  456. if( beforeTempChange == null ) beforeTempChange = { value : current };
  457. } else {
  458. undo(function() {
  459. var f = resolveField();
  460. var v = getFieldValue();
  461. f.current = v;
  462. f.element.val(v);
  463. f.element.parent().find("input[type=text]").val(v);
  464. f.onChange(true);
  465. });
  466. }
  467. current = v;
  468. setFieldValue(v);
  469. onChange(false);
  470. }
  471. public dynamic function onChange( wasUndo : Bool ) {
  472. }
  473. function resolveField() {
  474. /*
  475. If our panel has been removed but another bound to the same object has replaced it (a refresh for instance)
  476. let's try to locate the field with same context + name to refresh it instead
  477. */
  478. for( f in viewRoot.find("[field]") ) {
  479. var p : PropsField = Reflect.field(f, "propsField");
  480. if( p != null && p.context == context && p.fname == fname )
  481. return p;
  482. }
  483. return this;
  484. }
  485. }