PropsEditor.hx 13 KB

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