Editor.hx 74 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859186018611862186318641865186618671868186918701871187218731874187518761877187818791880188118821883188418851886188718881889189018911892189318941895189618971898189919001901190219031904190519061907190819091910191119121913191419151916191719181919192019211922192319241925192619271928192919301931193219331934193519361937193819391940194119421943194419451946194719481949195019511952195319541955195619571958195919601961196219631964196519661967196819691970197119721973197419751976197719781979198019811982198319841985198619871988198919901991199219931994199519961997199819992000200120022003200420052006200720082009201020112012201320142015201620172018201920202021202220232024202520262027202820292030203120322033203420352036203720382039204020412042204320442045204620472048204920502051205220532054205520562057205820592060206120622063206420652066206720682069207020712072207320742075207620772078207920802081208220832084208520862087208820892090209120922093209420952096209720982099210021012102210321042105210621072108210921102111211221132114211521162117211821192120212121222123212421252126212721282129213021312132213321342135213621372138213921402141214221432144214521462147214821492150215121522153215421552156215721582159216021612162216321642165216621672168216921702171217221732174217521762177217821792180218121822183218421852186218721882189219021912192219321942195219621972198219922002201220222032204220522062207220822092210221122122213221422152216221722182219222022212222222322242225222622272228222922302231223222332234223522362237223822392240224122422243224422452246224722482249225022512252225322542255225622572258225922602261226222632264226522662267226822692270227122722273227422752276227722782279228022812282228322842285228622872288228922902291229222932294229522962297229822992300230123022303230423052306230723082309231023112312231323142315231623172318231923202321232223232324232523262327232823292330233123322333233423352336233723382339234023412342234323442345234623472348234923502351235223532354235523562357235823592360236123622363236423652366236723682369237023712372237323742375237623772378237923802381238223832384238523862387238823892390239123922393239423952396239723982399240024012402240324042405240624072408240924102411241224132414241524162417241824192420242124222423242424252426242724282429243024312432243324342435243624372438243924402441244224432444244524462447244824492450245124522453245424552456245724582459246024612462246324642465246624672468246924702471247224732474247524762477247824792480248124822483248424852486248724882489249024912492249324942495249624972498249925002501250225032504250525062507250825092510251125122513251425152516251725182519252025212522252325242525252625272528252925302531253225332534253525362537253825392540
  1. package hide.comp.cdb;
  2. import hxd.Key in K;
  3. using hide.tools.Extensions;
  4. enum PathPart {
  5. Id(idCol:String, name:String, ?targetCol: String);
  6. Prop(name: String);
  7. Line(lineNo:Int, ?targetCol: String);
  8. Script(lineNo:Int);
  9. }
  10. typedef Path = Array<PathPart>;
  11. enum Direction {
  12. Left;
  13. Right;
  14. }
  15. typedef UndoSheet = {
  16. var sheet : String;
  17. var parent : { sheet : UndoSheet, line : Int, column : Int };
  18. }
  19. typedef UndoState = {
  20. var data : Any;
  21. var sheet : String;
  22. var cursor : Cursor.CursorState;
  23. var tables : Array<UndoSheet>;
  24. }
  25. typedef EditorApi = {
  26. function load( data : Any ) : Void;
  27. function copy() : Any;
  28. function save() : Void;
  29. }
  30. typedef EditorColumnProps = {
  31. var ?formula : String;
  32. var ?ignoreExport : Bool;
  33. var ?copyPasteImmutable : Bool;
  34. var ?categories : Array<String>;
  35. }
  36. typedef EditorSheetProps = {
  37. var ?categories : Array<String>;
  38. }
  39. typedef OptionalBackup = Array<{line: Dynamic, colName: String, data: Dynamic}>;
  40. @:allow(hide.comp.cdb)
  41. class Editor extends Component {
  42. static var CLIPBOARD_PREFIX = "[CDB_FORMAT]";
  43. static var COMPARISON_EXPR_CHARS = ["!=", ">=", "<=", "==", "<", ">"];
  44. var base : cdb.Database;
  45. var currentSheet : cdb.Sheet;
  46. var existsCache : Map<String,{ t : Float, r : Bool }> = new Map();
  47. var tables : Array<Table> = [];
  48. var pendingSearchRefresh : haxe.Timer = null;
  49. var displayMode : Table.DisplayMode;
  50. var changesDepth : Int = 0;
  51. var api : EditorApi;
  52. var undoState : Array<UndoState> = [];
  53. var currentValue : Any;
  54. var cdbTable : hide.view.CdbTable;
  55. var searchBox : Element;
  56. var searchHidden : Bool = true; // Search through hidden categories
  57. var searchExp : Bool = false; // Does filters are parsed by hscript parser
  58. var filters : Array<String> = [];
  59. public var view : cdb.DiffFile.ConfigView;
  60. public var config : hide.Config;
  61. public var cursor : Cursor;
  62. public var keys : hide.ui.Keys;
  63. public var undo : hide.ui.UndoHistory;
  64. public var formulas : Formulas;
  65. public var showGUIDs = false;
  66. public var gradientEditor: GradientEditor;
  67. public function new(config, api, ?cdbTable) {
  68. super(null,null);
  69. this.api = api;
  70. this.config = config;
  71. this.cdbTable = cdbTable;
  72. view = cast this.config.get("cdb.view");
  73. undo = new hide.ui.UndoHistory();
  74. }
  75. public function getCurrentSheet() {
  76. return currentSheet == null ? null : currentSheet.name;
  77. }
  78. public function show( sheet, ?parent : Element ) {
  79. if( element != null ) element.remove();
  80. element = new Element('<div>');
  81. if( parent != null )
  82. parent.append(element);
  83. currentSheet = sheet;
  84. element.attr("tabindex", 0);
  85. element.addClass("is-cdb-editor");
  86. element.data("cdb", this);
  87. element.on("keypress", function(e) {
  88. if( e.target.nodeName == "INPUT" )
  89. return;
  90. var cell = cursor.getCell();
  91. if( cell != null && cell.isTextInput() && !e.ctrlKey)
  92. cell.edit();
  93. });
  94. element.contextmenu(function(e) e.preventDefault());
  95. if( cdbTable == null ) {
  96. element.mousedown(onMouseDown);
  97. keys = new hide.ui.Keys(element);
  98. } else {
  99. cdbTable.element.off("mousedown" #if js, onMouseDown #end);
  100. cdbTable.element.mousedown(onMouseDown);
  101. keys = cdbTable.keys;
  102. }
  103. keys.clear();
  104. keys.addListener(onKey);
  105. keys.register("view.reopenLastClosedTab", function() ide.reopenLastClosedTab());
  106. keys.register("search", function() {
  107. searchBox.show();
  108. searchBox.find("input").val("").focus().select();
  109. });
  110. keys.register("copy", onCopy);
  111. keys.register("paste", onPaste);
  112. keys.register("delete", onDelete);
  113. keys.register("cdb.showReferences", () -> showReferences());
  114. keys.register("undo", function() undo.undo());
  115. keys.register("redo", function() undo.redo());
  116. keys.register("cdb.moveBack", () -> cursor.jump(true));
  117. keys.register("cdb.moveAhead", () -> cursor.jump(false));
  118. keys.register("cdb.insertLine", function() {
  119. cursor.table.insertLine(cursor.y);
  120. if (cursor.table.displayMode != Properties)
  121. cursor.move(0,1,false,false,false);
  122. });
  123. keys.register("duplicate", function() { cursor.table.duplicateLine(cursor.y); cursor.move(0,1,false,false,false); });
  124. for( k in ["cdb.editCell","rename"] )
  125. keys.register(k, function() {
  126. var c = cursor.getCell();
  127. if( c != null) c.edit();
  128. });
  129. keys.register("cdb.closeList", function() {
  130. var c = cursor.getCell();
  131. var l = cursor.getLine();
  132. var sub = Std.downcast(c == null ? cursor.table : c.table, SubTable);
  133. if (sub == null)
  134. sub = (c != null && c.line.subTable != null && c.line.subTable.cell == c) ? c.line.subTable : null;
  135. if( sub != null ) {
  136. sub.cell.elementHtml.click();
  137. return;
  138. }
  139. if( cursor.selection != null ) {
  140. cursor.selection = null;
  141. if (c != null)
  142. cursor.addElementToSelection(c.table, c.line, c.columnIndex, c.line.index);
  143. else if (l != null)
  144. cursor.addElementToSelection(l.table, l, -1, l.index);
  145. cursor.update();
  146. }
  147. });
  148. keys.register("cdb.gotoReference", () -> gotoReference(cursor.getCell()));
  149. keys.register("cdb.globalSeek", () -> {
  150. if (this.displayMode == Table.DisplayMode.AllProperties)
  151. return;
  152. new GlobalSeek(cdbTable.element, cdbTable, Sheets, currentSheet);
  153. });
  154. keys.register("cdb.sheetSeekIds", () -> new GlobalSeek(cdbTable.element, cdbTable, LocalIds, currentSheet));
  155. keys.register("cdb.globalSeekIds", () -> new GlobalSeek(cdbTable.element, cdbTable, GlobalIds, currentSheet));
  156. base = sheet.base;
  157. cursor = new Cursor(this);
  158. if( displayMode == null ) displayMode = Table;
  159. DataFiles.load();
  160. if( currentValue == null ) currentValue = api.copy();
  161. refresh();
  162. }
  163. function onMouseDown( e : hide.Element.Event ) {
  164. switch ( e.which ) {
  165. case 4:
  166. cursor.jump(true);
  167. return false;
  168. case 5:
  169. cursor.jump(false);
  170. return false;
  171. }
  172. return true;
  173. }
  174. function onKey( e : hide.Element.Event ) {
  175. var isRepeat: Bool = untyped e.originalEvent.repeat;
  176. switch( e.keyCode ) {
  177. case K.LEFT:
  178. if (e.altKey) {
  179. cursor.jump(true);
  180. return true;
  181. }
  182. cursor.move( -1, 0, e.shiftKey, e.ctrlKey, e.altKey);
  183. return true;
  184. case K.RIGHT:
  185. if (e.altKey) {
  186. cursor.jump(false);
  187. return true;
  188. }
  189. cursor.move( 1, 0, e.shiftKey, e.ctrlKey, e.altKey);
  190. return true;
  191. case K.UP:
  192. cursor.move( 0, -1, e.shiftKey, e.ctrlKey, e.altKey);
  193. return true;
  194. case K.DOWN:
  195. cursor.move( 0, 1, e.shiftKey, e.ctrlKey, e.altKey);
  196. return true;
  197. case K.TAB:
  198. cursor.move( e.shiftKey ? -1 : 1, 0, false, false, true);
  199. return true;
  200. case K.PGUP:
  201. var scrollView = element.parent(".hide-scroll");
  202. var stickyElHeight = scrollView.find(".separator").height();
  203. if (Math.isNaN(stickyElHeight))
  204. stickyElHeight = scrollView.find("thead").outerHeight();
  205. else
  206. stickyElHeight += scrollView.find("thead").outerHeight();
  207. var lines = scrollView.find("tbody").find(".start");
  208. var idx = lines.length - 1;
  209. while (idx >= 0) {
  210. var b = lines[idx].getBoundingClientRect();
  211. if (b.top <= stickyElHeight)
  212. break;
  213. idx--;
  214. }
  215. cursor.setDefault(cursor.table, cursor.x, idx);
  216. lines.get(idx).scrollIntoView({ block: js.html.ScrollLogicalPosition.END });
  217. // Handle sticky elements
  218. scrollView.scrollTop(scrollView.scrollTop() + scrollView.parent().siblings(".tabs-header").outerHeight());
  219. return true;
  220. case K.PGDOWN:
  221. var scrollView = element.parent(".hide-scroll");
  222. var height = scrollView.outerHeight() - (scrollView.find("thead").outerHeight() + scrollView.parent().siblings(".tabs-header").outerHeight());
  223. var lines = scrollView.find("tbody").find(".start");
  224. var idx = 0;
  225. for (el in lines) {
  226. var b = el.getBoundingClientRect();
  227. if (b.top >= height)
  228. break;
  229. idx++;
  230. }
  231. if (idx > lines.length - 1)
  232. idx = lines.length - 1;
  233. lines.get(idx).scrollIntoView(true);
  234. cursor.setDefault(cursor.table, cursor.x, idx);
  235. // Handle sticky elements
  236. var sepHeight = scrollView.find(".separator").height();
  237. if (Math.isNaN(sepHeight))
  238. sepHeight = 0;
  239. scrollView.scrollTop(scrollView.scrollTop() - (scrollView.find("thead").height() + sepHeight));
  240. return true;
  241. case K.SPACE:
  242. e.preventDefault(); // prevent scroll
  243. case K.ESCAPE:
  244. var c = cursor.getCell();
  245. var sub = Std.downcast(c == null ? cursor.table : c.table, SubTable); // Prevent closing search filter befor closing list
  246. if (sub == null && !isRepeat && searchBox != null && searchBox.is(":visible")) {
  247. searchBox.find(".close-search").click();
  248. return true;
  249. }
  250. }
  251. return false;
  252. }
  253. public dynamic function onScriptCtrlS() {
  254. }
  255. public function updateFilters() {
  256. if (filters.length > 0)
  257. searchFilter(filters, false);
  258. }
  259. function searchFilter( newFilters : Array<String>, updateCursor : Bool = true ) {
  260. function removeAccents(str: String) {
  261. var t = untyped str.toLowerCase().normalize('NFD');
  262. return ~/[\u0300-\u036f]/g.map(t, (r) -> "");
  263. }
  264. // Clean new filters
  265. var idx = newFilters.length;
  266. while (idx >= 0) {
  267. if (newFilters[idx] == null || newFilters[idx] == "")
  268. newFilters.remove(newFilters[idx]);
  269. idx--;
  270. }
  271. filters = newFilters;
  272. var table = tables.filter((t) -> t.sheet == currentSheet)[0];
  273. if (filters.length <= 0) @:privateAccess {
  274. if (table.lines != null) {
  275. for (l in table.lines)
  276. l.element.removeClass("filtered");
  277. }
  278. if (table.separators != null) {
  279. for (s in table.separators) {
  280. s.filtered = false;
  281. s.refresh(false);
  282. }
  283. }
  284. searchBox.find("#results").text('No results');
  285. return;
  286. }
  287. var isFiltered : (line: Dynamic) -> Bool;
  288. if (searchExp) {
  289. var parser = new hscript.Parser();
  290. parser.allowMetadata = true;
  291. parser.allowTypes = true;
  292. parser.allowJSON = true;
  293. var sheetNames = new Map();
  294. for( s in this.base.sheets )
  295. sheetNames.set(Formulas.getTypeName(s), s);
  296. function replaceRec( e : hscript.Expr ) {
  297. switch( e.e ) {
  298. case EField({ e : EIdent(s) }, name) if( sheetNames.exists(s) ):
  299. if( sheetNames.get(s).idCol != null )
  300. e.e = EConst(CString(name)); // replace for faster eval
  301. default:
  302. hscript.Tools.iter(e, replaceRec);
  303. }
  304. }
  305. var interp = new hscript.Interp();
  306. this.formulas.evaluateAll(this.currentSheet.realSheet);
  307. isFiltered = function(line: Dynamic) {
  308. @:privateAccess interp.resetVariables();
  309. @:privateAccess interp.initOps();
  310. interp.variables.set("Math", Math);
  311. // Need deep copy here, not ideal but works
  312. var cloned = haxe.Json.parse(haxe.Json.stringify(table.sheet.lines[line.index]));
  313. for (f in Reflect.fields(cloned)) {
  314. var c = table.columns[0];
  315. for (col in table.columns) {
  316. if (col.name == f) {
  317. c = col;
  318. break;
  319. }
  320. }
  321. switch(c.type) {
  322. case cdb.Data.ColumnType.TEnum(e):
  323. interp.variables.set(f, e[Reflect.getProperty(cloned, f)]);
  324. default:
  325. interp.variables.set(f, Reflect.getProperty(cloned, f));
  326. }
  327. }
  328. // Check if the current line is filtered or not
  329. for (f in filters) {
  330. var expr = try parser.parseString(f) catch( e : Dynamic ) { return true; }
  331. replaceRec(expr);
  332. var res = try interp.execute(expr) catch( e : hscript.Expr.Error ) { return true; } // Catch errors that can be thrown if search input text is not interpretabled
  333. if (res)
  334. return false;
  335. }
  336. return true;
  337. }
  338. }
  339. else {
  340. isFiltered = function(line: hide.comp.cdb.Line) {
  341. function isLineFiltered(line : hide.comp.cdb.Line) {
  342. var content = removeAccents(line.element.get(0).textContent);
  343. for (f in filters)
  344. if (content.indexOf(removeAccents(f)) >= 0)
  345. return false;
  346. if (line.subTable != null) {
  347. for (l in line.subTable.lines) {
  348. if (!isLineFiltered(l))
  349. return false;
  350. }
  351. }
  352. return true;
  353. }
  354. return isLineFiltered(line);
  355. }
  356. }
  357. // Create hidden lines to ensure they are take into account while searching
  358. if (searchHidden) {
  359. for (l in table.lines) {
  360. if (l.element.hasClass("hidden"))
  361. l.create();
  362. }
  363. }
  364. for (s in @:privateAccess table.separators)
  365. @:privateAccess s.filtered = true;
  366. var results = 0;
  367. for (l in table.lines) {
  368. var filtered = isFiltered(l);
  369. l.element.toggleClass("filtered", filtered);
  370. if (!filtered) {
  371. results++;
  372. var seps = Separator.getParentSeparators(l.index, @:privateAccess table.separators);
  373. for (s in seps)
  374. @:privateAccess s.filtered = false;
  375. }
  376. else {
  377. if (l.subTable != null)
  378. l.subTable.immediateClose();
  379. }
  380. }
  381. for (s in @:privateAccess table.separators)
  382. s.refresh(false);
  383. // Force show lines that are not filtered (even if their parent sep is collapsed)
  384. for (l in table.lines) {
  385. if (l.element.hasClass("hidden") && !l.element.hasClass("filtered"))
  386. l.create();
  387. }
  388. searchBox.find("#results").text(results > 0 ? '$results Results' : 'No results');
  389. if (updateCursor)
  390. cursor.update();
  391. }
  392. function stringToCol(str : String) : Null<Int> {
  393. str = str.toUpperCase();
  394. var hexChars = "0123456789ABCDEF";
  395. if( str.charAt(0) == "#" )
  396. str = str.substr(1, str.length);
  397. for( i in new haxe.iterators.StringIterator(str) ) {
  398. if( hexChars.indexOf(String.fromCharCode(i)) == -1 )
  399. return null;
  400. }
  401. var color = Std.parseInt("0x"+str);
  402. if( str.length == 6 )
  403. return color;
  404. else if( str.length == 3 ) {
  405. var r = color >> 8;
  406. var g = (color & 0xF0) >> 4;
  407. var b = color & 0xF;
  408. r |= r << 4;
  409. g |= g << 4;
  410. b |= b << 4;
  411. color = (r << 16) | (g << 8) | b;
  412. return color;
  413. }
  414. return null;
  415. }
  416. /* Change the id of a cell, propagating the changes to all the references in the database
  417. */
  418. function changeID(obj : Dynamic, newValue : Dynamic, column : cdb.Data.Column, table : Table) {
  419. if (column.type != TId)
  420. throw "Target column is not an ID";
  421. var value = Reflect.getProperty(obj, column.name);
  422. var prevValue = value;
  423. var realSheet = table.getRealSheet();
  424. var isLocal = realSheet.idCol.scope != null;
  425. var parentID = isLocal ? table.makeId([],realSheet.idCol.scope,null) : null;
  426. // most likely our obj, unless there was a #DUP
  427. var prevObj = value != null ? realSheet.index.get(isLocal ? parentID+":"+value : value) : null;
  428. // have we already an obj mapped to the same id ?
  429. var prevTarget = realSheet.index.get(isLocal ? parentID+":"+newValue : newValue);
  430. {
  431. beginChanges();
  432. if( prevObj == null || prevObj.obj == obj ) {
  433. // remap
  434. var m = new Map();
  435. m.set(value, newValue);
  436. if( isLocal ) {
  437. var scope = table.getScope();
  438. var parent = scope[scope.length - realSheet.idCol.scope];
  439. base.updateLocalRefs(realSheet, m, parent.obj, parent.s);
  440. } else
  441. base.updateRefs(realSheet, m);
  442. }
  443. Reflect.setField(obj, column.name, newValue);
  444. endChanges();
  445. refreshRefs();
  446. // Refresh display of all ids in the column manually
  447. var colId = table.sheet.columns.indexOf(column);
  448. for (l in table.lines) {
  449. if (l.cells[colId] != null)
  450. l.cells[colId].refresh(false);
  451. }
  452. }
  453. if( prevTarget != null || (prevObj != null && (prevObj.obj != obj || (table.sheet.index != null && table.sheet.index.get(prevValue) != null))) ) {
  454. table.refresh();
  455. updateFilters();
  456. }
  457. }
  458. function onCopy() {
  459. if( cursor.selection == null )
  460. return;
  461. function saveValue(out: Dynamic, obj: Dynamic, c: cdb.Data.Column) {
  462. var form = @:privateAccess formulas.getFormulaNameFromValue(obj, c);
  463. if( form != null ) {
  464. Reflect.setField(out, c.name+"__f", form);
  465. return;
  466. }
  467. var v = Reflect.field(obj, c.name);
  468. if( v != null )
  469. Reflect.setField(out, c.name, v);
  470. }
  471. var data = [];
  472. var schema = [];
  473. if (cursor.table.displayMode == Properties) {
  474. for (sel in cursor.selection) {
  475. for (y in sel.y1 ... sel.y2+1) {
  476. var out = {};
  477. var obj = cursor.table.lines[y].obj;
  478. var column = cursor.table.lines[y].columns[0];
  479. saveValue(out, obj ,column);
  480. schema.pushUnique(column);
  481. data.push(out);
  482. }
  483. }
  484. }
  485. else {
  486. var dataPerLines = new Map<Int, Dynamic>();
  487. for (sel in cursor.selection) {
  488. for( y in sel.y1...sel.y2+1 ) {
  489. // If there is several selection in only one line, we want to merge them into on single object for the line
  490. var out : Dynamic = null;
  491. if (sel.y1 == sel.y2) {
  492. out = dataPerLines.get(sel.y1);
  493. if (out == null) {
  494. out = {};
  495. dataPerLines.set(sel.y1, out);
  496. }
  497. }
  498. else {
  499. out = {};
  500. }
  501. var obj = cursor.table.lines[y].obj;
  502. var start = sel.x1;
  503. var end = sel.x2 + 1;
  504. if (start < 0) {
  505. start = 0;
  506. end = cursor.table.columns.length;
  507. }
  508. for( x in start...end ) {
  509. var c = cursor.table.columns[x];
  510. saveValue(out, obj, c);
  511. schema.pushUnique(c);
  512. }
  513. if (!data.contains(out))
  514. data.push(out);
  515. }
  516. }
  517. }
  518. // We're writting data format infos in Rtf MIME field because customs MIME types
  519. // aren't allowed anymore
  520. var rtfText = '${CLIPBOARD_PREFIX}${haxe.Json.stringify({data: data, schema: schema})}';
  521. // Plain text will contain only text value of cells
  522. var plainText = "";
  523. for (cell in cursor.getSelectedCells())
  524. plainText += (plainText != "" ? " " : "") + Std.string(cell.value);
  525. ide.setClipboardMultiple([
  526. { type: nw.Clipboard.ClipboardType.Text, data: plainText },
  527. { type:nw.Clipboard.ClipboardType.Rtf, data: rtfText }
  528. ]);
  529. }
  530. function onPaste() {
  531. if (this.cursor.table == null)
  532. return;
  533. var cdbDataText = ide.getClipboard(nw.Clipboard.ClipboardType.Rtf);
  534. if (cdbDataText.indexOf(CLIPBOARD_PREFIX) >= 0)
  535. cdbDataText = StringTools.replace(cdbDataText, CLIPBOARD_PREFIX, "");
  536. else
  537. cdbDataText = null; // Rtf data has been set by another application OR data in the plain text clipboard isn't comming from cdb
  538. var cdbData = cdbDataText != null ? haxe.Json.parse(cdbDataText) : null;
  539. var data : Array<Dynamic> = cdbData?.data;
  540. var schema : Array<cdb.Data.Column> = cdbData?.schema;
  541. // Hack to force col.type to be an enum value
  542. if (schema != null)
  543. for (s in schema) {
  544. var params = [];
  545. for (f in Reflect.fields(s.type)) {
  546. if (f.indexOf("_") >= 0)
  547. continue;
  548. params.push(Reflect.field(s.type, f));
  549. }
  550. if (params.length == 0)
  551. params = null;
  552. s.type = cdb.Data.ColumnType.createByName(s.type.getName(), params);
  553. }
  554. var toRefresh : Array<Cell> = [];
  555. var shouldFullRefresh = false;
  556. var targetCells = cursor.getSelectedCells();
  557. var targetSheet = cursor.table.sheet;
  558. function refresh() {
  559. formulas.evaluateAll(targetSheet.realSheet);
  560. if (targetSheet.realSheet.parent == null)
  561. targetSheet.realSheet.sync();
  562. if (shouldFullRefresh) {
  563. refreshAll();
  564. }
  565. else {
  566. for (c in toRefresh)
  567. c.refresh(true);
  568. }
  569. refreshRefs();
  570. }
  571. // We are trying to paste value copied from outisde CDB into CDB
  572. if (schema == null || data == null) {
  573. function parseCDBValue(v: String, type: cdb.Data.ColumnType) : Dynamic {
  574. switch( type ) {
  575. case TId:
  576. if( ~/^[A-Za-z0-9_]+$/.match(v) )
  577. return v;
  578. case TString:
  579. return v;
  580. case TFile:
  581. return ide.makeRelative(v);
  582. case TInt:
  583. v = v.split(",").join("").split(" ").join("");
  584. return Std.parseInt(v);
  585. case TFloat:
  586. v = v.split(",").join("").split(" ").join("");
  587. var value = Std.parseFloat(v);
  588. if( Math.isNaN(value) )
  589. return null;
  590. return value;
  591. case TColor:
  592. return stringToCol(v);
  593. case TGradient:
  594. try {
  595. var json = haxe.Json.parse(v);
  596. var grad : cdb.Types.Gradient = {colors: [], positions: []};
  597. if (Reflect.hasField(json, "stops")) {
  598. for (i => stop in (json.stops: Array<Dynamic>)) {
  599. grad.data.colors[i] = stop.color;
  600. grad.data.positions[i] = stop.position;
  601. }
  602. }
  603. else if (Reflect.hasField(json, "colors") && Reflect.hasField(json, "positions")) {
  604. grad.data.colors = json.colors;
  605. grad.data.positions = json.positions;
  606. }
  607. return grad;
  608. } catch (_) {
  609. return null;
  610. }
  611. default:
  612. }
  613. return null;
  614. }
  615. var plainText = ide.getClipboard(nw.Clipboard.ClipboardType.Text);
  616. beginChanges();
  617. for (c in targetCells) {
  618. var col = c.column;
  619. if (!c.table.canEditColumn(col.name) || Editor.getColumnProps(col).copyPasteImmutable)
  620. continue;
  621. var parsedValue = parseCDBValue(plainText, col.type);
  622. if (parsedValue == null)
  623. continue;
  624. Reflect.setField(c.line.obj, col.name, parsedValue);
  625. toRefresh.push(c);
  626. }
  627. endChanges();
  628. refresh();
  629. return;
  630. }
  631. function setValue(cliObj : Dynamic, destObj : Dynamic, clipSchema : cdb.Data.Column, destCol : cdb.Data.Column) {
  632. var sheet = targetSheet;
  633. var form = Reflect.field(cliObj, clipSchema.name+"__f");
  634. if( form != null && destCol.type.equals(clipSchema.type) ) {
  635. formulas.setForValue(destObj, sheet, destCol, form);
  636. return;
  637. }
  638. var f = base.getConvFunction(clipSchema.type, destCol.type);
  639. var v : Dynamic = Reflect.field(cliObj, clipSchema.name);
  640. if (f == null) {
  641. switch ([clipSchema.type, destCol.type]) {
  642. case [TId, TRef(destSheet)]:
  643. if (v != null)
  644. v = haxe.Json.parse(haxe.Json.stringify(v));
  645. if (!doesSheetContainsId(base.getSheet(destSheet), v))
  646. v = base.getDefault(destCol, sheet);
  647. case [TRef(_), TId]:
  648. // do nothing
  649. default:
  650. v = base.getDefault(destCol, sheet);
  651. }
  652. }
  653. else {
  654. // make a deep copy to erase references
  655. if( v != null ) v = haxe.Json.parse(haxe.Json.stringify(v));
  656. if( f.f != null )
  657. v = f.f(v);
  658. }
  659. if( v == null && !destCol.opt )
  660. v = base.getDefault(destCol, sheet);
  661. if (destCol.type == TId) {
  662. v = ensureUniqueId(v, cursor.table, destCol);
  663. if (v != null) {
  664. changeID(destObj, v, destCol, cursor.table);
  665. }
  666. return;
  667. }
  668. if( v == null )
  669. Reflect.deleteField(destObj, destCol.name);
  670. else
  671. Reflect.setField(destObj, destCol.name, v);
  672. }
  673. // Manage pasting one value into several cells
  674. if (data.length == 1) {
  675. beginChanges();
  676. // We copied one cell
  677. if (schema.length == 1) {
  678. for (c in targetCells) {
  679. var col = c.column;
  680. if (!c.table.canEditColumn(col.name) || Editor.getColumnProps(col).copyPasteImmutable)
  681. continue;
  682. setValue(data[0], c.line.obj, schema[0], col);
  683. toRefresh.push(c);
  684. }
  685. }
  686. else {
  687. // We copied one line (could be several cells of one line)
  688. var targetLines = cursor.getSelectedLines();
  689. if (targetLines.length == 0) {
  690. for (c in targetCells)
  691. targetLines.pushUnique(c.line);
  692. }
  693. for (l in targetLines) {
  694. for (c in l.cells) {
  695. var col = c.column;
  696. if (!l.table.canEditColumn(col.name) || Editor.getColumnProps(col).copyPasteImmutable || !Reflect.hasField(data[0], col.name))
  697. continue;
  698. var sc = schema[0];
  699. for (s in schema)
  700. if (col.name == s.name)
  701. sc = s;
  702. setValue(data[0], c.line.obj, sc, col);
  703. toRefresh.push(c);
  704. }
  705. }
  706. }
  707. endChanges();
  708. refresh();
  709. return;
  710. }
  711. beginChanges();
  712. var curPosY = Std.int(Math.max(0, cursor.y));
  713. var curPosX = Std.int(Math.max(0, cursor.x));
  714. for (d in data) {
  715. // Insert lines if we still got data to paste and that we are at the end of the sheet
  716. if ( curPosY == targetSheet.lines.length ) {
  717. if( !cursor.table.canInsert() ) break;
  718. targetSheet.newLine();
  719. shouldFullRefresh = true;
  720. }
  721. var obj = targetSheet.lines[curPosY];
  722. for( cid in 0...schema.length ) {
  723. var c1 = schema[cid];
  724. var c2 = cursor.table.columns[cid + curPosX];
  725. if( c2 == null ) continue;
  726. var p = Editor.getColumnProps(c2);
  727. if( !cursor.table.canEditColumn(c2.name) || p.copyPasteImmutable)
  728. continue;
  729. setValue(d, obj, c1, c2);
  730. if( c2.type == TList || c2.type == TProperties )
  731. shouldFullRefresh = true;
  732. if( !shouldFullRefresh )
  733. toRefresh.push(cursor.table.lines[curPosY].cells[cid + curPosX]);
  734. }
  735. curPosY++;
  736. }
  737. refresh();
  738. endChanges();
  739. }
  740. function onDelete() {
  741. if( cursor.selection == null )
  742. return;
  743. beginChanges();
  744. cursor.selection.sort((el1, el2) -> { return el1.y1 == el2.y1 ? 0 : el1.y1 < el2.y1 ? 1 : -1; });
  745. for (s in cursor.selection)
  746. delete(s.x1, s.x2, s.y1, s.y2);
  747. endChanges();
  748. }
  749. function delete(x1 : Int, x2 : Int, y1 : Int, y2 : Int) {
  750. var modifiedTables = [];
  751. var sheet = cursor.table.sheet;
  752. if (cursor.getCell() == null || cursor.getCell().column.type == TId) {
  753. var id = getCursorId(sheet, true);
  754. if(id != null && id.length > 0) {
  755. var refs = getReferences(id, sheet);
  756. if( refs.length > 0 ) {
  757. var message = [for (r in refs) r.str].join("\n");
  758. if( !ide.confirm('$id is referenced elswhere. Are you sure you want to delete?\n$message') )
  759. return;
  760. }
  761. }
  762. }
  763. beginChanges();
  764. if( cursor.x < 0 ) {
  765. // delete lines
  766. var y = y2;
  767. if( !cursor.table.canInsert() ) {
  768. endChanges();
  769. return;
  770. }
  771. while( y >= y1 ) {
  772. var line = cursor.table.lines[y];
  773. if(!cursor.table.lines[y].element.hasClass("filtered")) {
  774. sheet.deleteLine(line.index);
  775. modifiedTables.pushUnique(cursor.table);
  776. }
  777. y--;
  778. }
  779. cursor.set(cursor.table, -1, y1, null, true, true, false);
  780. }
  781. else {
  782. // delete cells
  783. for( y in y1...y2+1 ) {
  784. var line = cursor.table.lines[y];
  785. if (line.element.hasClass("filtered"))
  786. continue;
  787. for( x in x1...x2+1 ) {
  788. var c = line.columns[x];
  789. if( !line.cells[x].canEdit() )
  790. continue;
  791. var old = Reflect.field(line.obj, c.name);
  792. var def = base.getDefault(c,false,sheet);
  793. if( old == def )
  794. continue;
  795. changeObject(line,c,def);
  796. modifiedTables.pushUnique(cursor.table);
  797. }
  798. }
  799. }
  800. endChanges();
  801. refreshAll();
  802. updateFilters();
  803. }
  804. public function changeObject( line : Line, column : cdb.Data.Column, value : Dynamic ) {
  805. beginChanges();
  806. var prev = Reflect.field(line.obj, column.name);
  807. if( value == null ) {
  808. formulas.setForValue(line.obj, line.table.sheet, column, null);
  809. } else {
  810. Reflect.setField(line.obj, column.name, value);
  811. formulas.removeFromValue(line.obj, column);
  812. }
  813. line.table.getRealSheet().updateValue(column, line.index, prev);
  814. line.evaluate(); // propagate
  815. line.getRootLine().validate();
  816. endChanges();
  817. }
  818. /**
  819. Call before modifying the database, allow to group several changes together.
  820. Allow recursion, only last endChanges() will trigger db save and undo point creation.
  821. **/
  822. public function beginChanges( ?structure : Bool ) {
  823. if( changesDepth == 0 )
  824. undoState.unshift(getState());
  825. changesDepth++;
  826. }
  827. function getState() : UndoState {
  828. return {
  829. data : currentValue,
  830. sheet : getCurrentSheet(),
  831. cursor : cursor.getState(),
  832. tables : [for( i in 1...tables.length ) {
  833. function makeParent(t:Table) : UndoSheet {
  834. var tp = t.parent;
  835. return { sheet : t.sheet.name, parent : tp == null ? null : {
  836. sheet : makeParent(tp),
  837. line : t.sheet.parent.line,
  838. column : tp.columns.indexOf(tp.sheet.columns[t.sheet.parent.column]),
  839. } };
  840. }
  841. makeParent(tables[i]);
  842. }],
  843. };
  844. }
  845. function setState( state : UndoState, doFocus : Bool ) {
  846. var cur = state.cursor;
  847. for( t in state.tables ) {
  848. function openRec(s:UndoSheet) : Table {
  849. if( s.parent != null ) {
  850. var t = openRec(s.parent.sheet);
  851. if( t != null && s.parent.line < t.lines.length ) {
  852. var cell = t.lines[s.parent.line].cells[t.displayMode == Properties || t.displayMode == AllProperties ? 0 : s.parent.column];
  853. if (cell == null)
  854. return null;
  855. if( cell.line.subTable == null && (cell.column.type == TList || cell.column.type == TProperties) )
  856. cell.open(true);
  857. return cell.line.subTable;
  858. }
  859. } else {
  860. for( tp in tables )
  861. if( tp.sheet.name == s.sheet )
  862. return tp;
  863. }
  864. return null;
  865. }
  866. openRec(t);
  867. }
  868. if( cur != null ) {
  869. var table = null;
  870. for( t in tables ) {
  871. if( t.sheet.getPath() == cur.sheet ) {
  872. table = t;
  873. break;
  874. }
  875. }
  876. if( table != null && doFocus )
  877. focus();
  878. cursor.setState(cur, table);
  879. } else
  880. cursor.set();
  881. }
  882. /**
  883. Call when changes are done, after endChanges.
  884. **/
  885. public function endChanges() {
  886. changesDepth--;
  887. if( changesDepth != 0 ) return;
  888. var newValue = api.copy();
  889. if( newValue == currentValue )
  890. return;
  891. var state = undoState[0];
  892. var newSheet = getCurrentSheet();
  893. currentValue = newValue;
  894. save();
  895. undo.change(Custom(function(undo) {
  896. var currentSheet;
  897. if( undo ) {
  898. undoState.shift();
  899. currentValue = state.data;
  900. currentSheet = state.sheet;
  901. } else {
  902. undoState.unshift(state);
  903. currentValue = newValue;
  904. currentSheet = newSheet;
  905. }
  906. api.load(currentValue);
  907. DataFiles.save(true); // save reloaded data
  908. element.removeClass("is-cdb-editor");
  909. refreshAll();
  910. element.addClass("is-cdb-editor");
  911. syncSheet(currentSheet);
  912. refresh(state);
  913. save();
  914. }));
  915. }
  916. static var runningHooks = false;
  917. static var queuedCommand: Void -> Void = null;
  918. function save() {
  919. api.save();
  920. function hookEnd() {
  921. runningHooks = false;
  922. if (queuedCommand != null) {
  923. var a = queuedCommand;
  924. queuedCommand = null;
  925. a();
  926. }
  927. }
  928. var hooks: Array<{cmd: String, sheets: Array<String>}> = this.config.get("cdb.onChangeHooks");
  929. if( hooks != null ) {
  930. var s = getCurrentSheet();
  931. var commands = [for (h in hooks) if (h.sheets.has(s)) h.cmd];
  932. function runRec(i: Int) {
  933. runningHooks = true;
  934. ide.runCommand(commands[i], (e) -> {
  935. if (e != null) {
  936. ide.quickError('Hook error:\n$e');
  937. hookEnd();
  938. } else {
  939. if (i < commands.length - 1) {
  940. runRec(i + 1);
  941. } else {
  942. hookEnd();
  943. }
  944. }
  945. });
  946. }
  947. if (!commands.isEmpty()) {
  948. if (runningHooks) {
  949. queuedCommand = () -> runRec(0);
  950. } else {
  951. runRec(0);
  952. }
  953. }
  954. }
  955. }
  956. /**
  957. Recursively removes empty list/properties from the optional columns of `sheet` in the given `lines` objects.
  958. A record of the removed items are stored in `optionalBackup` so they can be restored later using `restoreOptionals()`.
  959. **/
  960. public static function cleanupOptionalLines(lines: Array<Dynamic>, sheet: cdb.Sheet, optionalBackup: OptionalBackup) {
  961. if (lines == null)
  962. return;
  963. for (column in sheet.columns) {
  964. for (line in lines) {
  965. var data = Reflect.field(line, column.name);
  966. if (data == null)
  967. continue;
  968. switch (column.type) {
  969. case TList:
  970. var list : Array<Dynamic> = cast data;
  971. if (list.length == 0 && column.opt) {
  972. optionalBackup.push({line: line, colName: column.name, data: list});
  973. Reflect.deleteField(line, column.name);
  974. } else {
  975. var sub = sheet.getSub(column);
  976. cleanupOptionalLines(list, sub, optionalBackup);
  977. }
  978. case TProperties:
  979. var props : Dynamic = cast data;
  980. if (Reflect.fields(props).length == 0 && column.opt) {
  981. optionalBackup.push({line: line, colName: column.name, data: props});
  982. Reflect.deleteField(line, column.name);
  983. } else {
  984. var sub = sheet.getSub(column);
  985. cleanupOptionalLines([props], sub, optionalBackup);
  986. }
  987. default:
  988. }
  989. }
  990. }
  991. }
  992. public static function restoreOptionals(optionalBackup: OptionalBackup) {
  993. for (backup in optionalBackup) {
  994. Reflect.setField(backup.line, backup.colName, backup.data);
  995. }
  996. }
  997. public static var inRefreshAll(default,null) : Bool;
  998. public static function refreshAll( eraseUndo = false, loadDataFiles = true) {
  999. var editors : Array<Editor> = [for( e in new Element(".is-cdb-editor").elements() ) e.data("cdb")];
  1000. if (loadDataFiles)
  1001. DataFiles.load();
  1002. inRefreshAll = true;
  1003. for( e in editors ) {
  1004. e.syncSheet(Ide.inst.database);
  1005. e.refresh();
  1006. // prevent undo over input changes
  1007. if( eraseUndo ) {
  1008. e.currentValue = e.api.copy();
  1009. e.undo.clear();
  1010. e.undoState = [];
  1011. }
  1012. }
  1013. inRefreshAll = false;
  1014. }
  1015. public function getCursorId(?sheet, ?childOnly = false): String {
  1016. var id: String = null;
  1017. if( sheet == null )
  1018. sheet = cursor.table.sheet;
  1019. var cell = cursor.getCell();
  1020. switch (cell == null ? null : cell.column.type) {
  1021. case TRef(sname):
  1022. id = cell.value;
  1023. case TId:
  1024. id = cell.value;
  1025. default:
  1026. if (!childOnly || cursor.x < 0) {
  1027. for( c in sheet.columns ) {
  1028. switch( c.type ) {
  1029. case TId:
  1030. id = Reflect.field(sheet.lines[cursor.y], c.name);
  1031. break;
  1032. default:
  1033. }
  1034. }
  1035. }
  1036. }
  1037. return id;
  1038. }
  1039. public static function splitPath(rs: {s:Array<{s:cdb.Sheet, c:String, id:Null<String>}>, o:{path:Array<Dynamic>, indexes:Array<Int>}}) {
  1040. var path = [];
  1041. var coords = [];
  1042. for( i in 0...rs.s.length ) {
  1043. var s = rs.s[i];
  1044. var oid = Reflect.field(rs.o.path[i], s.id);
  1045. var idx = rs.o.indexes[i];
  1046. if( oid == null || oid == "" )
  1047. path.push(s.s.name.split("@").pop() + (idx < 0 ? "" : "[" + idx +"]"));
  1048. else {
  1049. path.push(oid);
  1050. }
  1051. if (i == rs.s.length - 1 && s.c != "" && s.c != null) {
  1052. path.push(s.c);
  1053. }
  1054. }
  1055. var coords = [];
  1056. var curIdx = 0;
  1057. while(curIdx < rs.o.indexes.length) {
  1058. var sheet = rs.s[curIdx];
  1059. var isSheet = !sheet.s.props.isProps;
  1060. if (isSheet) {
  1061. var oid = Reflect.field(rs.o.path[curIdx], sheet.id);
  1062. var next = sheet.c;
  1063. if (oid != null) {
  1064. coords.push(Id(sheet.id, oid, next));
  1065. }
  1066. else {
  1067. coords.push(Line(rs.o.indexes[curIdx], next));
  1068. }
  1069. }
  1070. else {
  1071. coords.push(Prop(rs.s[curIdx].c));
  1072. }
  1073. curIdx += 1;
  1074. }
  1075. return {pathNames: path, pathParts: coords};
  1076. }
  1077. public function getReferences(id: String, withCodePaths = true, returnAtFirstRef = false, sheet: cdb.Sheet, ?codeFileCache: Array<{path: String, data:String}>, ?prefabFileCache: Array<{path: String, data:String}>) : Array<{str:String, ?goto:Void->Void, ?file: String}> {
  1078. #if hl
  1079. return [];
  1080. #else
  1081. if( id == null )
  1082. return [];
  1083. var results = sheet.getReferencesFromId(id);
  1084. var message = new Array<{str:String, ?goto:Void->Void, ?file: String}>();
  1085. if( results != null ) {
  1086. for( rs in results ) {
  1087. var path = splitPath(rs);
  1088. message.push({str: rs.s[0].s.name+"."+path.pathNames.join("."), goto: () -> openReference2(rs.s[0].s, path.pathParts), file: @:privateAccess Ide.inst.databaseFile});
  1089. if (returnAtFirstRef) return message;
  1090. }
  1091. }
  1092. if (withCodePaths) {
  1093. var paths : Array<String> = this.config.get("haxe.classPath");
  1094. if( paths != null ) {
  1095. if (codeFileCache == null) {
  1096. codeFileCache = [];
  1097. }
  1098. if (codeFileCache.length == 0) {
  1099. function lookupRec(p) {
  1100. for( f in sys.FileSystem.readDirectory(p) ) {
  1101. var fpath = p+"/"+f;
  1102. if( sys.FileSystem.isDirectory(fpath) ) {
  1103. lookupRec(fpath);
  1104. if (returnAtFirstRef && message.length > 0) return;
  1105. continue;
  1106. }
  1107. if( StringTools.endsWith(f, ".hx") ) {
  1108. codeFileCache.push({path: fpath, data: sys.io.File.getContent(fpath)});
  1109. }
  1110. }
  1111. }
  1112. for( p in paths ) {
  1113. var path = ide.getPath(p);
  1114. if( sys.FileSystem.exists(path) && sys.FileSystem.isDirectory(path) )
  1115. lookupRec(path);
  1116. }
  1117. var formulasPath = ide.getPath("formulas.hx");
  1118. if (sys.FileSystem.exists(formulasPath)) {
  1119. codeFileCache.push({path: formulasPath, data: sys.io.File.getContent(formulasPath)});
  1120. }
  1121. }
  1122. var spaces = "[ \\n\\t]";
  1123. var prevChars = ",\\(:=\\?\\[|";
  1124. var postChars = ",\\):;\\?\\]&|";
  1125. var regexp = new EReg('((return$spaces+)|(case$spaces+)|[$prevChars])$spaces*$id$spaces*[$postChars]*.*',"");
  1126. var regall = new EReg("\\b"+id+"\\b", "");
  1127. var tableName = sheet.name;
  1128. var first = tableName.substr(0,1);
  1129. var caseInsentive = '[${first.toLowerCase()}${first.toUpperCase()}]${tableName.substr(1)}';
  1130. var regResolve = new EReg('${caseInsentive}\\.resolve\\(\\s*"$id"\\s*\\)', "");
  1131. for (file in codeFileCache) {
  1132. var fpath = file.path;
  1133. var content = file.data;
  1134. if( content.indexOf(id) < 0 ) continue;
  1135. for( line => str in content.split("\n") ) {
  1136. if( regall.match(str) ) {
  1137. if( !regexp.match(str) && !regResolve.match(str) ) {
  1138. var str2 = str.split(id+".").join("").split("."+id).join("").split(id+"(").join("").split(id+"<").join("");
  1139. if( regall.match(str2) ) trace("Skip "+str);
  1140. continue;
  1141. }
  1142. var path = ide.makeRelative(fpath);
  1143. var fn = function () {
  1144. var ext = Extension.getExtension(path);
  1145. ide.open(ext.component, { path : path }, function (v) {
  1146. var scr : hide.view.Script = cast v;
  1147. function checkSetPos() {
  1148. var s = @:privateAccess scr.script;
  1149. if (s != null) {
  1150. var e = @:privateAccess s.editor;
  1151. e.setPosition({column:0, lineNumber: line+1});
  1152. haxe.Timer.delay(() ->e.revealLineInCenter(line+1), 1);
  1153. return;
  1154. }
  1155. // needed because the editor can be created after our
  1156. // function is called (if the tab was created but never opened,
  1157. // likely because hide was closed and reopened)
  1158. // see : View.rebuild()
  1159. haxe.Timer.delay(checkSetPos, 200);
  1160. }
  1161. checkSetPos();
  1162. });
  1163. }
  1164. message.push({str: path+":"+(line+1), goto: fn, file: fpath});
  1165. if (returnAtFirstRef) return message;
  1166. }
  1167. }
  1168. }
  1169. }
  1170. var paths : Array<String> = this.config.get("cdb.prefabsSearchPaths");
  1171. var scriptStr = new EReg("\\b"+sheet.name.charAt(0).toUpperCase() + sheet.name.substr(1) + "\\." + id + "\\b","");
  1172. if( paths != null ) {
  1173. if (prefabFileCache == null)
  1174. prefabFileCache = [];
  1175. if (prefabFileCache.length == 0) {
  1176. function lookupPrefabRec(path) {
  1177. for( f in sys.FileSystem.readDirectory(path) ) {
  1178. var fpath = path+"/"+f;
  1179. if( sys.FileSystem.isDirectory(fpath) ) {
  1180. lookupPrefabRec(fpath);
  1181. continue;
  1182. }
  1183. var ext = f.split(".").pop();
  1184. if( @:privateAccess hrt.prefab.Prefab.extensionRegistry.exists(ext) ) {
  1185. prefabFileCache.push({path: fpath, data: sys.io.File.getContent(fpath)});
  1186. }
  1187. }
  1188. }
  1189. for( p in paths ) {
  1190. var path = ide.getPath(p);
  1191. if( sys.FileSystem.exists(path) && sys.FileSystem.isDirectory(path) )
  1192. lookupPrefabRec(path);
  1193. }
  1194. }
  1195. for (file in prefabFileCache) {
  1196. var fpath = file.path;
  1197. var content = file.data;
  1198. if( !scriptStr.match(content) ) continue;
  1199. for( line => str in content.split("\n") ) {
  1200. if( scriptStr.match(str) ) {
  1201. var path = ide.makeRelative(fpath);
  1202. var fn = function () {
  1203. ide.openFile(path, function (v) {
  1204. var scr : hide.view.Script = cast v;
  1205. haxe.Timer.delay(function() {
  1206. @:privateAccess scr.script.editor.setPosition({column:0, lineNumber: line+1});
  1207. haxe.Timer.delay(() ->@:privateAccess scr.script.editor.revealLineInCenter(line+1), 1);
  1208. }, 1);
  1209. });
  1210. }
  1211. message.push({str: path+":"+(line+1), goto: fn, file: fpath});
  1212. }
  1213. }
  1214. }
  1215. }
  1216. // Script references
  1217. {
  1218. var results = [];
  1219. for( s in sheet.base.sheets ) {
  1220. for( cid => c in s.columns )
  1221. switch( c.type ) {
  1222. case TString:
  1223. if (c.kind == cdb.Data.ColumnKind.Script) {
  1224. var sheets = [];
  1225. var p = { s : s, c : c.name, id : null };
  1226. while( true ) {
  1227. for( c in p.s.columns )
  1228. switch( c.type ) {
  1229. case TId: p.id = c.name; break;
  1230. default:
  1231. }
  1232. sheets.unshift(p);
  1233. var p2 = p.s.getParent();
  1234. if( p2 == null ) break;
  1235. p = { s : p2.s, c : p2.c, id : null };
  1236. }
  1237. var objs = s.getObjects();
  1238. var i = 0;
  1239. for( sheetline => o in objs ) {
  1240. i += 1;
  1241. var obj = o.path[o.path.length - 1];
  1242. var content = Reflect.field(obj, c.name);
  1243. if( !scriptStr.match(content) ) continue;
  1244. for( line => str in content.split("\n") ) {
  1245. if( scriptStr.match(str) )
  1246. {
  1247. var res = splitPath({s: sheets, o: o});
  1248. res.pathParts.push(Script(line));
  1249. message.push({str: sheets[0].s.name+"."+res.pathNames.join(".") + "." + c.name + ":" + Std.string(line + 1), goto: () -> openReference2(sheets[0].s, res.pathParts), file:@:privateAccess Ide.inst.databaseFile});
  1250. if (returnAtFirstRef) return message;
  1251. }
  1252. }
  1253. }
  1254. }
  1255. /*case TRef(sname) if( sname == sheet.sheet.name ):
  1256. var sheets = [];
  1257. var p = { s : s, c : c.name, id : null };
  1258. while( true ) {
  1259. for( c in p.s.columns )
  1260. switch( c.type ) {
  1261. case TId: p.id = c.name; break;
  1262. default:
  1263. }
  1264. sheets.unshift(p);
  1265. var p2 = p.s.getParent();
  1266. if( p2 == null ) break;
  1267. p = { s : p2.s, c : p2.c, id : null };
  1268. }
  1269. for( o in s.getObjects() ) {
  1270. var obj = o.path[o.path.length - 1];
  1271. if( Reflect.field(obj, c.name) == id )
  1272. results.push({ s : sheets, o : o });
  1273. }*/
  1274. default:
  1275. }
  1276. }
  1277. }
  1278. }
  1279. return message;
  1280. #end
  1281. }
  1282. public function findUnreferenced(col: cdb.Data.Column, table: Table) {
  1283. var sheet = table.getRealSheet();
  1284. var codeFileCache = [];
  1285. var prefabFileCache = [];
  1286. var nonrefs: Array<hide.view.RefViewer.Result> = [];
  1287. for (o in sheet.lines) {
  1288. var id = Reflect.getProperty(o, col.name);
  1289. var refs = getReferences(id, true, true, sheet, codeFileCache, prefabFileCache);
  1290. if (refs.length == 0)
  1291. nonrefs.push({ text: id, goto: () -> openReference2(sheet, [Id(col.name, id)]) });
  1292. }
  1293. ide.open("hide.view.RefViewer", null, function(view) {
  1294. var refViewer : hide.view.RefViewer = cast view;
  1295. refViewer.showUnreferenced(nonrefs);
  1296. });
  1297. }
  1298. public function showReferences(?id: String, ?cell : hide.comp.cdb.Cell, ?sheet: cdb.Sheet) {
  1299. if( cursor.table == null ) return;
  1300. if( sheet == null )
  1301. sheet = cursor.table.sheet;
  1302. if( id == null )
  1303. id = getCursorId(sheet);
  1304. var cell = cursor.getCell();
  1305. if (cell != null) {
  1306. switch (cell.column.type) {
  1307. case TRef(sname):
  1308. sheet = base.getSheet(sname);
  1309. default:
  1310. }
  1311. }
  1312. var refs = [];
  1313. if( id != null )
  1314. refs = getReferences(id, sheet);
  1315. if( refs.length == 0 ) {
  1316. ide.message("No reference found");
  1317. return;
  1318. }
  1319. var files = new Map<String, hide.view.RefViewer.Reference>();
  1320. for (r in refs) {
  1321. var ref = files.get(r.file);
  1322. if (ref == null) {
  1323. ref = { file : r.file.substr(r.file.lastIndexOf("/") + 1), path : r.file, results: [] };
  1324. files.set(r.file, ref);
  1325. }
  1326. ref.results.push({ text: r.str, goto: r.goto });
  1327. }
  1328. ide.open("hide.view.RefViewer", null, function(view) {
  1329. var refViewer : hide.view.RefViewer = cast view;
  1330. refViewer.showRefs([for (f in files.keys()) files.get(f)], id, () -> {
  1331. openReference(cell.table.sheet, cell.line.index, cell.columnIndex);
  1332. });
  1333. });
  1334. }
  1335. function gotoReference( c : Cell ) {
  1336. if( c == null || c.value == null ) return;
  1337. switch( c.column.type ) {
  1338. case TRef(s):
  1339. var sd = base.getSheet(s);
  1340. if( sd == null ) return;
  1341. var k = sd.index.get(c.value);
  1342. if( k == null ) return;
  1343. var index = sd.lines.indexOf(k.obj);
  1344. if( index >= 0 ) openReference(sd, index, 0);
  1345. default:
  1346. }
  1347. }
  1348. public static function openReference2(rootSheet : cdb.Sheet, path: Path) {
  1349. hide.Ide.inst.open("hide.view.CdbTable", {}, null, function(view) Std.downcast(view,hide.view.CdbTable).goto2(rootSheet,path));
  1350. }
  1351. function openReference( s : cdb.Sheet, line : Int, column : Int, ?scriptLine: Int ) {
  1352. ide.open("hide.view.CdbTable", {}, function(view) Std.downcast(view,hide.view.CdbTable).goto(s,line,column,scriptLine));
  1353. }
  1354. public function syncSheet( ?base, ?name ) {
  1355. if( base == null ) base = this.base;
  1356. this.base = base;
  1357. if( name == null ) name = getCurrentSheet();
  1358. // swap sheet if it was modified
  1359. this.currentSheet = null;
  1360. for( s in base.sheets )
  1361. if( s.name == name ) {
  1362. this.currentSheet = s;
  1363. break;
  1364. }
  1365. }
  1366. function isUniqueID( sheet : cdb.Sheet, obj : {}, id : String ) {
  1367. var idx = base.getSheet(sheet.name).index;
  1368. var uniq = idx.get(id);
  1369. return uniq == null || uniq.obj == obj;
  1370. }
  1371. public function refreshRefs() {
  1372. base.sync();
  1373. for( t in tables ) {
  1374. for( l in t.lines ) {
  1375. for( c in l.cells ) {
  1376. switch( c.column.type ){
  1377. case TRef(_):
  1378. c.refresh();
  1379. case TString:
  1380. if( c.column.kind == Script )
  1381. c.refresh();
  1382. default:
  1383. }
  1384. }
  1385. }
  1386. }
  1387. }
  1388. public function refresh( ?state : UndoState ) {
  1389. if( state == null )
  1390. state = getState();
  1391. var hasFocus = element.find(":focus").length > 0;
  1392. base.sync();
  1393. element.empty();
  1394. element.addClass('cdb');
  1395. formulas = new Formulas(this);
  1396. formulas.enable = ide.ideConfig.enableDBFormulas;
  1397. formulas.evaluateAll(currentSheet.realSheet);
  1398. var content = new Element("<table>");
  1399. tables = [];
  1400. new Table(this, currentSheet, content, displayMode);
  1401. content.appendTo(element);
  1402. setState(state, hasFocus);
  1403. if( cursor.table != null ) {
  1404. for( t in tables )
  1405. if( t.sheet.getPath() == cursor.table.sheet.getPath() )
  1406. cursor.table = t;
  1407. cursor.update();
  1408. }
  1409. // Setup for search bar
  1410. searchBox = new Element('<div>
  1411. <div class="buttons">
  1412. <div class="btn add-btn ico ico-plus" title="Add filter"></div>
  1413. <div class="btn remove-btn ico ico-minus" title="Remove filter"></div>
  1414. </div>
  1415. <div class="input-col">
  1416. <div class="input-cont"/>
  1417. <input type="text" class="search-bar-cdb"></input>
  1418. </div>
  1419. </div>
  1420. <p id="results">No results</p>
  1421. <div class="btn search-type fa fa-font" title="Change search type"></div>
  1422. <div class="btn search-hidden fa fa-eye" title="Search through hidden categories"></div>
  1423. <div class="btn close-search ico ico-close" title="Close (Escape)"></div>
  1424. </div>').addClass("search-box").appendTo(element);
  1425. searchBox.hide();
  1426. function search(e: js.jquery.Event) {
  1427. // Close search with escape
  1428. if( e.keyCode == K.ESCAPE ) {
  1429. searchBox.find(".close-search").click();
  1430. return;
  1431. }
  1432. // Change to expresion mode if we detect an expression character in the search (qol)
  1433. for (c in Editor.COMPARISON_EXPR_CHARS) {
  1434. if (StringTools.contains(Element.getVal(e.getThis()), c) && !searchExp) {
  1435. searchExp = true;
  1436. var searchTypeBtn = searchBox.find(".search-type");
  1437. searchTypeBtn.toggleClass("fa-superscript", searchExp);
  1438. searchTypeBtn.toggleClass("fa-font", !searchExp);
  1439. updateFilters();
  1440. break;
  1441. }
  1442. }
  1443. var index = e.getThis().parent().find('.search-bar-cdb').index(e.getThis());
  1444. if (filters[index] == null)
  1445. filters[index] = "";
  1446. filters[index] = Element.getVal(e.getThis());
  1447. // Slow table refresh protection
  1448. if (currentSheet.lines.length > 300) {
  1449. if (pendingSearchRefresh != null) {
  1450. pendingSearchRefresh.stop();
  1451. }
  1452. pendingSearchRefresh = haxe.Timer.delay(function()
  1453. {
  1454. searchFilter(filters.copy());
  1455. pendingSearchRefresh = null;
  1456. }, 500);
  1457. }
  1458. else {
  1459. searchFilter(filters.copy());
  1460. }
  1461. }
  1462. var inputs = searchBox.find(".search-bar-cdb");
  1463. inputs.attr("placeholder", "Find");
  1464. inputs.keyup(search);
  1465. var inputCont = searchBox.find(".input-cont");
  1466. searchBox.find(".add-btn").click(function(_) {
  1467. var newInput = new Element('<input type="text" class="search-bar-cdb"></input>');
  1468. newInput.attr("placeholder", "Find");
  1469. newInput.appendTo(searchBox.find(".input-cont"));
  1470. newInput.css({"margin-top": "2px"});
  1471. updateFilters();
  1472. searchBox.find(".remove-btn").show();
  1473. });
  1474. searchBox.find(".remove-btn").hide();
  1475. searchBox.find(".remove-btn").click(function(_) {
  1476. var searchBars = inputCont.find(".search-bar-cdb");
  1477. if( searchBars.length > 1 ) {
  1478. searchBars.last().remove();
  1479. filters.pop();
  1480. searchFilter(filters.copy());
  1481. if (filters.length <= 1)
  1482. searchBox.find(".remove-btn").hide();
  1483. }
  1484. });
  1485. searchBox.find(".close-search").click(function(_) {
  1486. searchFilter([]);
  1487. searchBox.find(".search-bar-cdb").not(':first').remove();
  1488. searchBox.find(".expr-btn").not(':first').remove();
  1489. filters.clear();
  1490. if(searchBox.find(".expr-btn").hasClass("fa-superscript"))
  1491. searchBox.find(".expr-btn").removeClass("fa-superscript").addClass("fa-font");
  1492. searchBox.toggle();
  1493. var c = cursor.save();
  1494. focus();
  1495. cursor.load(c);
  1496. var hiddenSeps = element.find("table.cdb-sheet > tbody > tr").not(".head").filter(".separator").filter(".sep-hidden").find("a.toggle");
  1497. hiddenSeps.click();
  1498. cursor.scrollIntoView();
  1499. });
  1500. searchBox.find(".search-type").click(function(_) {
  1501. searchExp = !searchExp;
  1502. searchBox.find(".search-type").toggleClass("fa-superscript", searchExp);
  1503. searchBox.find(".search-type").toggleClass("fa-font", !searchExp);
  1504. updateFilters();
  1505. });
  1506. searchBox.find(".search-hidden").click(function(_) {
  1507. searchHidden = !searchHidden;
  1508. searchBox.find(".search-hidden").toggleClass("fa-eye", searchHidden);
  1509. searchBox.find(".search-hidden").toggleClass("fa-eye-slash", !searchHidden);
  1510. if (!searchHidden) {
  1511. var hiddenSeps = element.find("table.cdb-sheet > tbody > tr").not(".head").filter(".separator").filter(".sep-hidden").find("a.toggle");
  1512. hiddenSeps.click();
  1513. hiddenSeps.click();
  1514. }
  1515. updateFilters();
  1516. });
  1517. // If there is still a search apply it
  1518. if (filters.length > 0) {
  1519. searchBox.show();
  1520. for (f in filters)
  1521. inputs.val(f);
  1522. if (searchExp)
  1523. searchBox.find(".search-type").click();
  1524. if (!searchHidden)
  1525. searchBox.find(".search-type").click();
  1526. searchFilter(filters);
  1527. }
  1528. }
  1529. function quickExists(path) {
  1530. var c = existsCache.get(path);
  1531. if( c == null ) {
  1532. c = { t : -1e9, r : false };
  1533. existsCache.set(path, c);
  1534. }
  1535. var t = haxe.Timer.stamp();
  1536. if( c.t < t - 10 ) { // cache result for 10s
  1537. c.r = sys.FileSystem.exists(path);
  1538. c.t = t;
  1539. }
  1540. return c.r;
  1541. }
  1542. function getLine( sheet : cdb.Sheet, index : Int ) {
  1543. for( t in tables )
  1544. if( t.sheet == sheet )
  1545. return t.lines[index];
  1546. return null;
  1547. }
  1548. static public function getColumnProps( c : cdb.Data.Column ) {
  1549. var pr : EditorColumnProps = c.editor;
  1550. if( pr == null ) pr = {};
  1551. return pr;
  1552. }
  1553. public function isColumnVisible( c : cdb.Data.Column ) {
  1554. var props = getColumnProps(c);
  1555. var cats = ide.projectConfig.dbCategories;
  1556. return cats == null || props.categories == null || cats.filter(c -> props.categories.indexOf(c) >= 0).length > 0;
  1557. }
  1558. public function moveColumn(targetSheet: cdb.Sheet, origSheet: cdb.Sheet, col: cdb.Data.Column) : String {
  1559. beginChanges(true);
  1560. var err = targetSheet.addColumn(col);
  1561. function createSubCols(targetSheet: cdb.Sheet, origSheet: cdb.Sheet, column: cdb.Data.Column) : String {
  1562. // Check to see if the column contains other columns
  1563. var subSheetPath = origSheet.getPath() + "@" + column.name;
  1564. var subSheet = base.getSheet(subSheetPath);
  1565. var subTargetPath = targetSheet.getPath() + "@" + column.name;
  1566. var subTarget = base.getSheet(subTargetPath);
  1567. if (subSheet != null) {
  1568. if (subTarget == null)
  1569. return 'original sheet $subSheetPath contains columns but target sheet $subTargetPath does not exist';
  1570. for (c in subSheet.columns) {
  1571. var err = subTarget.addColumn(c);
  1572. if (err != null)
  1573. return err;
  1574. createSubCols(subTarget, subSheet, c);
  1575. }
  1576. }
  1577. return null;
  1578. }
  1579. if (err == null) {
  1580. var err = createSubCols(targetSheet, origSheet, col);
  1581. if (err == null) {
  1582. // Copy the data from the original column to the new one
  1583. var commonSheet = origSheet;
  1584. var commonPath = origSheet.getPath().split("@");
  1585. while(true) {
  1586. if (commonPath.length <= 0) {
  1587. throw "missing parent table that is not props";
  1588. }
  1589. commonSheet = base.getSheet(commonPath.join("@"));
  1590. if (!commonSheet.props.isProps)
  1591. break;
  1592. commonPath.pop();
  1593. }
  1594. var origPath = origSheet.getPath().split("@");
  1595. origPath.splice(0, commonPath.length);
  1596. origPath.push(col.name);
  1597. var targetPath = targetSheet.getPath().split("@");
  1598. targetPath.splice(0, commonPath.length);
  1599. var lines = commonSheet.getLines();
  1600. for (i => line in lines) {
  1601. // read value from origPath
  1602. var value : Dynamic = line;
  1603. for (p in origPath) {
  1604. value = Reflect.field(value, p);
  1605. if (value == null)
  1606. break;
  1607. }
  1608. if (value != null) {
  1609. // Get or insert intermediates props value along targetPath
  1610. var target : Dynamic = line;
  1611. for (p in targetPath) {
  1612. var newTarget = Reflect.field(target, p);
  1613. if (newTarget == null) {
  1614. newTarget = {};
  1615. Reflect.setField(target, p, newTarget);
  1616. }
  1617. target = newTarget;
  1618. }
  1619. Reflect.setField(target, col.name, value);
  1620. }
  1621. }
  1622. origSheet.deleteColumn(col.name);
  1623. }
  1624. }
  1625. endChanges();
  1626. return err;
  1627. }
  1628. public function newColumn( sheet : cdb.Sheet, ?index : Int, ?onDone : cdb.Data.Column -> Void, ?col ) {
  1629. #if js
  1630. var modal = new hide.comp.cdb.ModalColumnForm(this, sheet, col, element);
  1631. modal.setCallback(function() {
  1632. var c = modal.getColumn(col);
  1633. if (c == null)
  1634. return;
  1635. beginChanges(true);
  1636. var err;
  1637. if( col != null ) {
  1638. base.mapType(function(t) {
  1639. return switch( t ) {
  1640. case TRef(o) if( o.indexOf('${sheet.name}@${col.name}') >= 0 ):
  1641. TRef(StringTools.replace(o, col.name, c.name));
  1642. case TLayer(o) if( o.indexOf('${sheet.name}@${col.name}') >= 0 ):
  1643. TLayer(StringTools.replace(o, col.name, c.name));
  1644. default:
  1645. t;
  1646. }
  1647. });
  1648. var newPath = c.name;
  1649. var back = newPath.split("/");
  1650. var finalPart = back.pop();
  1651. var path = finalPart.split(".");
  1652. c.name = path.pop();
  1653. err = base.updateColumn(sheet, col, c);
  1654. if (path.length > 0 || back.length > 0) {
  1655. function handleMoveTable() {
  1656. var cdbPath = sheet.getPath().split("@");
  1657. for(i in 0...back.length) {
  1658. var b = back[back.length - i - 1];
  1659. // if it's not actually a backards move
  1660. if (b != "..") {
  1661. path.unshift(b);
  1662. continue;
  1663. }
  1664. if (cdbPath.length <= 0) {
  1665. return 'Backwards path "${back.join("/")}" goes outside of base sheet';
  1666. }
  1667. var subSheet = base.getSheet(cdbPath.join("@"));
  1668. if (!subSheet.props.isProps) {
  1669. return 'Target path "${cdbPath.join(".")}" goes inside or outside another sheet';
  1670. }
  1671. cdbPath.pop();
  1672. }
  1673. for (p in path) {
  1674. cdbPath.push(p);
  1675. var subSheet = base.getSheet(cdbPath.join("@"));
  1676. if (subSheet == null) {
  1677. return 'Target sheet "${cdbPath.join(".")}" does not exist';
  1678. }
  1679. if (!subSheet.props.isProps) {
  1680. return 'Target path "${cdbPath.join(".")}" goes inside or outside another sheet';
  1681. }
  1682. }
  1683. var finalPath = cdbPath.join("@");
  1684. var targetSheet = base.getSheet(finalPath);
  1685. if (targetSheet != null) {
  1686. if (ide.confirm('Move column to "$finalPath" ?')) {
  1687. return moveColumn(targetSheet, sheet, c);
  1688. }
  1689. return 'Move canceled';
  1690. }
  1691. else {
  1692. return 'Invalid move path "$newPath"';
  1693. }
  1694. return null;
  1695. }
  1696. err = handleMoveTable();
  1697. }
  1698. }
  1699. else
  1700. err = sheet.addColumn(c, index == null ? null : index + 1);
  1701. endChanges();
  1702. if (err != null) {
  1703. modal.error(err);
  1704. return;
  1705. }
  1706. // perform side effects before refresh
  1707. if( onDone != null )
  1708. onDone(c);
  1709. // if first column or subtable, refresh all
  1710. if( sheet.columns.length == 1 || sheet.name.indexOf("@") > 0 )
  1711. refresh();
  1712. else
  1713. sheet.sync();
  1714. for( t in tables )
  1715. if( t.sheet == sheet )
  1716. t.refresh();
  1717. modal.closeModal();
  1718. });
  1719. #end
  1720. }
  1721. public function editColumn( sheet : cdb.Sheet, col : cdb.Data.Column ) {
  1722. newColumn(sheet,col);
  1723. }
  1724. public function ensureUniqueId(originalId : String, table : Table, column : cdb.Data.Column) {
  1725. var scope = table.getScope();
  1726. var idWithScope : String = if(column.scope != null) table.makeId(scope, column.scope, originalId) else originalId;
  1727. if (isUniqueID(table.getRealSheet(), {}, idWithScope)) {
  1728. return originalId;
  1729. }
  1730. return getNewUniqueId(originalId, table, column);
  1731. }
  1732. public function doesSheetContainsId(sheet:cdb.Sheet, id:String) {
  1733. var idx = base.getSheet(sheet.name).index;
  1734. var uniq = idx.get(id);
  1735. return uniq != null;
  1736. }
  1737. public function getNewUniqueId(originalId : String, table : Table, column : cdb.Data.Column) {
  1738. var str = originalId;
  1739. var currentValue : Null<Int> = null;
  1740. var strIdx : Int = 0;
  1741. // Find the number at the end of the string
  1742. while (strIdx < str.length) {
  1743. var substr = str.substr(str.length-1-strIdx);
  1744. var newValue = Std.parseInt(substr);
  1745. if (newValue != null)
  1746. currentValue = newValue;
  1747. else {
  1748. break;
  1749. }
  1750. strIdx += 1;
  1751. }
  1752. var scope = table.getScope();
  1753. if (currentValue == null) {
  1754. currentValue = 0;
  1755. strIdx = 0;
  1756. }
  1757. var newId : String;
  1758. var idWithScope : String;
  1759. do {
  1760. currentValue+=1;
  1761. var valStr = Std.string(currentValue);
  1762. // Pad with zeroes
  1763. for (i in 0...strIdx - valStr.length) {
  1764. valStr = "0" + valStr;
  1765. }
  1766. newId = str.substr(0, str.length-strIdx) + valStr;
  1767. idWithScope = if(column.scope != null) table.makeId(scope, column.scope, newId) else newId;
  1768. }
  1769. while (!isUniqueID(table.getRealSheet(), {}, idWithScope));
  1770. return newId;
  1771. }
  1772. public function popupColumn( table : Table, col : cdb.Data.Column, ?cell : Cell ) {
  1773. if( view != null )
  1774. return;
  1775. var sheet = table.getRealSheet();
  1776. var indexColumn = sheet.columns.indexOf(col);
  1777. var menu : Array<hide.comp.ContextMenu.MenuItem> = [
  1778. { label : "Edit", click : function () editColumn(sheet, col) },
  1779. {
  1780. label : "Add Column",
  1781. click : function () newColumn(sheet, indexColumn),
  1782. enabled : table.displayMode != AllProperties,
  1783. },
  1784. { label : "", isSeparator: true },
  1785. { label : "Move Left", enabled: (indexColumn > 0 &&
  1786. nextVisibleColumnIndex(table, indexColumn, Left) > -1), click : function () {
  1787. beginChanges();
  1788. var nextIndex = nextVisibleColumnIndex(table, indexColumn, Left);
  1789. sheet.columns.remove(col);
  1790. sheet.columns.insert(nextIndex, col);
  1791. if (cursor.x == indexColumn)
  1792. cursor.setDefault(cursor.table, nextIndex, cursor.y);
  1793. else if (cursor.x == nextIndex)
  1794. cursor.setDefault(cursor.table, nextIndex + 1, cursor.y);
  1795. endChanges();
  1796. refresh();
  1797. }},
  1798. { label : "Move Right", enabled: (indexColumn < sheet.columns.length - 1 &&
  1799. nextVisibleColumnIndex(table, indexColumn, Right) < sheet.columns.length), click : function () {
  1800. beginChanges();
  1801. var nextIndex = nextVisibleColumnIndex(table, indexColumn, Right);
  1802. sheet.columns.remove(col);
  1803. sheet.columns.insert(nextIndex, col);
  1804. if (cursor.x == indexColumn)
  1805. cursor.setDefault(cursor.table, nextIndex, cursor.y);
  1806. else if (cursor.x == nextIndex)
  1807. cursor.setDefault(cursor.table, nextIndex - 1, cursor.y);
  1808. endChanges();
  1809. refresh();
  1810. }},
  1811. { label: "", isSeparator: true },
  1812. {
  1813. label : "Delete",
  1814. click : function () {
  1815. if( table.displayMode == Properties ) {
  1816. beginChanges();
  1817. changeObject(cell.line, col, base.getDefault(col,sheet));
  1818. } else {
  1819. beginChanges(true);
  1820. sheet.deleteColumn(col.name);
  1821. }
  1822. endChanges();
  1823. refresh();
  1824. },
  1825. enabled : table.displayMode != AllProperties,
  1826. },
  1827. ];
  1828. if( table.parent == null ) {
  1829. var props = table.sheet.props;
  1830. switch( col.type ) {
  1831. case TString, TRef(_):
  1832. menu.push({ label : "Display Name", click : function() {
  1833. beginChanges();
  1834. props.displayColumn = (props.displayColumn == col.name ? null : col.name);
  1835. endChanges();
  1836. refresh();
  1837. }, checked: props.displayColumn == col.name });
  1838. case TTilePos:
  1839. menu.push({ label : "Display Icon", click : function() {
  1840. beginChanges();
  1841. props.displayIcon = (props.displayIcon == col.name ? null : col.name);
  1842. endChanges();
  1843. refresh();
  1844. }, checked: props.displayIcon == col.name });
  1845. default:
  1846. }
  1847. var editProps = getColumnProps(col);
  1848. menu.push({ label : "Categories", menu: categoriesMenu(editProps.categories, function(cats) {
  1849. beginChanges();
  1850. editProps.categories = cats;
  1851. col.editor = editProps;
  1852. endChanges();
  1853. refresh();
  1854. })});
  1855. switch(col.type) {
  1856. case TId | TString | TGuid:
  1857. menu.push({ label : "Sort", click: () -> table.sortBy(col), enabled : table.displayMode != AllProperties });
  1858. default:
  1859. }
  1860. var hasGUID = false;
  1861. for( s in base.sheets )
  1862. for( c in s.columns )
  1863. if( c.type == TGuid ) {
  1864. hasGUID = true;
  1865. break;
  1866. }
  1867. if( hasGUID ) {
  1868. menu.push({ label : "Display GUIDs", checked : showGUIDs, click : function() {
  1869. showGUIDs = !showGUIDs;
  1870. refresh();
  1871. } });
  1872. }
  1873. }
  1874. if( col.type == TString && col.kind == Script )
  1875. menu.insert(1,{ label : "Edit all", click : function() editScripts(table,col) });
  1876. if( table.displayMode == Properties ) {
  1877. menu.push({ label : "Delete All", click : function() {
  1878. if( !ide.confirm('*** WARNING ***\nThis will delete the row for all properties!\n${col.name}') )
  1879. return;
  1880. beginChanges(true);
  1881. table.sheet.deleteColumn(col.name);
  1882. endChanges();
  1883. refresh();
  1884. }});
  1885. }
  1886. hide.comp.ContextMenu.createFromPoint(ide.mouseX, ide.mouseY, menu);
  1887. }
  1888. function nextVisibleColumnIndex( table : Table, index : Int, dir : Direction){
  1889. var next = index;
  1890. do {
  1891. next += (dir == Left ? -1 : 1);
  1892. }
  1893. while (next >= 0 && next <= table.columns.length - 1 && !isColumnVisible(table.columns[next]));
  1894. return next;
  1895. }
  1896. function editScripts( table : Table, col : cdb.Data.Column ) {
  1897. // TODO : create single edit-all script view allowing global search & replace
  1898. }
  1899. public function popupLine( line : Line ) {
  1900. var sheet = line.table.sheet;
  1901. var remoteMenu: Array<hide.comp.ContextMenu.MenuItem> = [];
  1902. if (sheet.idCol != null) {
  1903. var id = Reflect.field(line.obj, sheet.idCol.name);
  1904. remoteMenu.append(hide.view.RemoteConsoleView.getCdbMenuActions(sheet.name, id));
  1905. }
  1906. if( !line.table.canInsert() ) {
  1907. if (!remoteMenu.isEmpty()) {
  1908. hide.comp.ContextMenu.createFromPoint(ide.mouseX, ide.mouseY, remoteMenu);
  1909. }
  1910. return;
  1911. }
  1912. var selectedLines = cursor.getSelectedLines();
  1913. var isSelectedLine = selectedLines.contains(line);
  1914. var firstLine = isSelectedLine ? selectedLines[0] : line;
  1915. var lastLine = isSelectedLine ? selectedLines[selectedLines.length - 1] : line;
  1916. var sepIndex = -1;
  1917. for( i in 0...sheet.separators.length )
  1918. if( sheet.separators[i].index == line.index ) {
  1919. sepIndex = i;
  1920. break;
  1921. }
  1922. var moveSubmenu : Array<hide.comp.ContextMenu.MenuItem> = [];
  1923. var moveStack : Array<hide.comp.ContextMenu.MenuItem> = [];
  1924. for( sepIndex => sep in sheet.separators ) {
  1925. if( sep.title == null )
  1926. continue;
  1927. var level = sep.level ?? 0;
  1928. function separatorCount( fromLine : Int ) {
  1929. var count = 0;
  1930. if( fromLine >= sep.index ) {
  1931. for( i in (sepIndex + 1)...sheet.separators.length ) {
  1932. if( sheet.separators[i].index > fromLine )
  1933. break;
  1934. count--;
  1935. }
  1936. } else {
  1937. for( i in 0...(sepIndex + 1) ) {
  1938. if( sheet.separators[i].index <= fromLine )
  1939. continue;
  1940. count++;
  1941. }
  1942. }
  1943. return count;
  1944. }
  1945. var lastOfGroup = sepIndex == sheet.separators.length - 1 ? line.table.lines.length : sheet.separators[sepIndex + 1].index;
  1946. var usedLine = firstLine;
  1947. if( lastOfGroup > line.index ) {
  1948. lastOfGroup--;
  1949. usedLine = lastLine;
  1950. }
  1951. var delta = lastOfGroup - usedLine.index + separatorCount(usedLine.index);
  1952. var linesToMove = isSelectedLine ? selectedLines : [usedLine];
  1953. var item = {
  1954. label : sep.title,
  1955. enabled : true,
  1956. click : function() {
  1957. usedLine.table.moveLines(linesToMove, delta);
  1958. },
  1959. stayOpen: false,
  1960. };
  1961. moveStack[level] = item;
  1962. moveStack.splice(level + 1, moveStack.length);
  1963. var arr = moveSubmenu;
  1964. if (level > 0 && moveStack[level - 1] != null) {
  1965. var m = moveStack[level - 1];
  1966. if (m.menu == null)
  1967. m.menu = [];
  1968. arr = m.menu;
  1969. }
  1970. arr.push(item);
  1971. }
  1972. var hasLocText = false;
  1973. function checkRec(s:cdb.Sheet) {
  1974. for( c in s.columns ) {
  1975. switch( c.type ) {
  1976. case TList, TProperties:
  1977. var sub = s.getSub(c);
  1978. checkRec(sub);
  1979. case TString if( c.kind == Localizable ):
  1980. hasLocText = true;
  1981. default:
  1982. }
  1983. }
  1984. }
  1985. if( sheet.parent == null )
  1986. checkRec(sheet);
  1987. var menu : Array<hide.comp.ContextMenu.MenuItem> = [
  1988. {
  1989. label : "Move Up",
  1990. enabled: (firstLine.index > 0 || sepIndex >= 0),
  1991. click : () -> line.table.moveLines(isSelectedLine ? selectedLines : [line] , -1),
  1992. },
  1993. {
  1994. label : "Move Down",
  1995. enabled: (lastLine.index < sheet.lines.length - 1),
  1996. click : () -> line.table.moveLines(isSelectedLine ? selectedLines : [line], 1),
  1997. },
  1998. { label : "Move to Group", enabled : moveSubmenu.length > 0, menu : moveSubmenu },
  1999. { label : "", isSeparator : true },
  2000. { label : "Insert", click : function() {
  2001. line.table.insertLine(line.index);
  2002. cursor.set(line.table, -1, line.index + 1);
  2003. focus();
  2004. }, keys : config.get("key.cdb.insertLine") },
  2005. { label : "Duplicate", click : function() {
  2006. line.table.duplicateLine(line.index);
  2007. cursor.set(line.table, -1, line.index + 1);
  2008. focus();
  2009. }, keys : config.get("key.duplicate") },
  2010. { label : "Delete", click : function() {
  2011. beginChanges();
  2012. if (cursor.selection == null) {
  2013. line.table.sheet.deleteLine(line.index);
  2014. line.table.refresh();
  2015. }
  2016. else {
  2017. cursor.selection.sort((el1, el2) -> { return el1.y1 == el2.y1 ? 0 : el1.y1 < el2.y1 ? 1 : -1; });
  2018. for (s in cursor.selection)
  2019. delete(s.x1, s.x2, s.y1, s.y2);
  2020. }
  2021. endChanges();
  2022. } },
  2023. { label : "Separator", enabled : !sheet.props.hide, checked : sepIndex >= 0, click : function() {
  2024. beginChanges();
  2025. if( sepIndex >= 0 ) {
  2026. sheet.separators.splice(sepIndex, 1);
  2027. } else {
  2028. sepIndex = sheet.separators.length;
  2029. var level = 1;
  2030. for( i in 0...sheet.separators.length ) {
  2031. if( sheet.separators[i].index > line.index ) {
  2032. sepIndex = i;
  2033. break;
  2034. }
  2035. var lv = sheet.separators[i].level;
  2036. if( lv == null ) lv = 0;
  2037. level = lv + 1;
  2038. }
  2039. sheet.separators.insert(sepIndex, { index : line.index, level : level });
  2040. }
  2041. endChanges();
  2042. refresh();
  2043. } }
  2044. ];
  2045. if( hasLocText ) {
  2046. menu.push({ label : "", isSeparator : true });
  2047. menu.push({
  2048. label : "Export Localized Texts",
  2049. checked : !Reflect.hasField(line.obj,cdb.Lang.IGNORE_EXPORT_FIELD),
  2050. click : function() {
  2051. beginChanges();
  2052. if( Reflect.hasField(line.obj,cdb.Lang.IGNORE_EXPORT_FIELD) )
  2053. Reflect.deleteField(line.obj,cdb.Lang.IGNORE_EXPORT_FIELD);
  2054. else
  2055. Reflect.setField(line.obj,cdb.Lang.IGNORE_EXPORT_FIELD, true);
  2056. endChanges();
  2057. line.syncClasses();
  2058. },
  2059. });
  2060. }
  2061. if (!remoteMenu.isEmpty()) {
  2062. menu.push({ label : "", isSeparator : true });
  2063. menu.append(remoteMenu);
  2064. }
  2065. hide.comp.ContextMenu.createFromPoint(ide.mouseX, ide.mouseY, menu);
  2066. }
  2067. function rename( sheet : cdb.Sheet, name : String ) {
  2068. if( !base.r_ident.match(name) ) {
  2069. ide.error("Invalid sheet name");
  2070. return false;
  2071. }
  2072. var f = base.getSheet(name);
  2073. if( f != null ) {
  2074. if( f != sheet ) ide.error("Sheet name already in use");
  2075. return false;
  2076. }
  2077. beginChanges();
  2078. var old = sheet.name;
  2079. sheet.rename(name);
  2080. base.mapType(function(t) {
  2081. return switch( t ) {
  2082. case TRef(o) if( o == old ):
  2083. TRef(name);
  2084. case TLayer(o) if( o == old ):
  2085. TLayer(name);
  2086. default:
  2087. t;
  2088. }
  2089. });
  2090. for( s in base.sheets )
  2091. if( StringTools.startsWith(s.name, old + "@") )
  2092. s.rename(name + "@" + s.name.substr(old.length + 1));
  2093. endChanges();
  2094. DataFiles.save(true,[ sheet.name => old ]);
  2095. return true;
  2096. }
  2097. function categoriesMenu(categories: Array<String>, setFunc : Array<String> -> Void) {
  2098. var menu : Array<ContextMenu.MenuItem> = [{ label : "Set...", click : function() {
  2099. var wstr = "";
  2100. if(categories != null)
  2101. wstr = categories.join(",");
  2102. wstr = ide.ask("Set Categories (comma separated)", wstr);
  2103. if(wstr == null)
  2104. return;
  2105. categories = [for(s in wstr.split(",")) { var t = StringTools.trim(s); if(t.length > 0) t; }];
  2106. setFunc(categories.length > 0 ? categories : null);
  2107. #if editor
  2108. ide.initMenu();
  2109. #end
  2110. }}];
  2111. for(name in getCategories(base)) {
  2112. var has = categories != null && categories.indexOf(name) >= 0;
  2113. menu.push({
  2114. label: name, checked: has, stayOpen: true, click: function() {
  2115. if(has)
  2116. categories.remove(name);
  2117. else {
  2118. if(categories == null)
  2119. categories = [];
  2120. categories.push(name);
  2121. }
  2122. has = !has;
  2123. setFunc(categories.length > 0 ? categories : null);
  2124. }
  2125. });
  2126. }
  2127. return menu;
  2128. }
  2129. public function createDBSheet( ?index : Int ) {
  2130. var value = ide.ask("Sheet name");
  2131. if( value == "" || value == null ) return null;
  2132. var s = ide.database.createSheet(value, index);
  2133. if( s == null ) {
  2134. ide.error("Name already exists");
  2135. return null;
  2136. }
  2137. ide.saveDatabase();
  2138. refreshAll();
  2139. return s;
  2140. }
  2141. public function popupSheet( withMacro = true, ?sheet : cdb.Sheet, ?onChange : Void -> Void ) {
  2142. if( view != null )
  2143. return;
  2144. if( sheet == null ) sheet = this.currentSheet;
  2145. if( onChange == null ) onChange = function() {}
  2146. var index = base.sheets.indexOf(sheet);
  2147. var content : Array<ContextMenu.MenuItem> = [];
  2148. if (withMacro) {
  2149. content = content.concat([
  2150. { label : "Add Sheet", click : function() { beginChanges(); var db = createDBSheet(index+1); endChanges(); if( db != null ) onChange(); } },
  2151. { label : "Move Left", click : function() { beginChanges(); base.moveSheet(sheet,-1); endChanges(); onChange(); } },
  2152. { label : "Move Right", click : function() { beginChanges(); base.moveSheet(sheet,1); endChanges(); onChange(); } },
  2153. { label : "Rename", click : function() {
  2154. var name = ide.ask("New name", sheet.name);
  2155. if( name == null || name == "" || name == sheet.name ) return;
  2156. if( !rename(sheet, name) ) return;
  2157. onChange();
  2158. }},
  2159. { label : "Delete", click : function() {
  2160. beginChanges();
  2161. base.deleteSheet(sheet);
  2162. endChanges();
  2163. onChange();
  2164. }},
  2165. { label : "Categories", menu: categoriesMenu(getSheetProps(sheet).categories, function(cats) {
  2166. beginChanges();
  2167. var props = getSheetProps(sheet);
  2168. props.categories = cats;
  2169. sheet.props.editor = props;
  2170. endChanges();
  2171. onChange();
  2172. })},
  2173. { label : "", isSeparator: true },
  2174. ]);
  2175. }
  2176. if( sheet.props.dataFiles == null )
  2177. content = content.concat([
  2178. { label : "Add Index", checked : sheet.props.hasIndex, click : function() {
  2179. beginChanges();
  2180. if( sheet.props.hasIndex ) {
  2181. for( o in sheet.getLines() )
  2182. Reflect.deleteField(o, "index");
  2183. sheet.props.hasIndex = false;
  2184. } else {
  2185. for( c in sheet.columns )
  2186. if( c.name == "index" ) {
  2187. ide.error("Column 'index' already exists");
  2188. return;
  2189. }
  2190. sheet.props.hasIndex = true;
  2191. }
  2192. endChanges();
  2193. }},
  2194. { label : "Add Group", checked : sheet.props.hasGroup, click : function() {
  2195. beginChanges();
  2196. if( sheet.props.hasGroup ) {
  2197. for( o in sheet.getLines() )
  2198. Reflect.deleteField(o, "group");
  2199. sheet.props.hasGroup = false;
  2200. } else {
  2201. for( c in sheet.columns )
  2202. if( c.name == "group" ) {
  2203. ide.error("Column 'group' already exists");
  2204. return;
  2205. }
  2206. sheet.props.hasGroup = true;
  2207. }
  2208. endChanges();
  2209. }},
  2210. ]);
  2211. if( sheet.lines.length == 0 || sheet.props.dataFiles != null )
  2212. content.push({
  2213. label : "Data Files",
  2214. checked : sheet.props.dataFiles != null,
  2215. click : function() {
  2216. var txt = ide.ask("Data Files Path", sheet.props.dataFiles);
  2217. if( txt == null ) return;
  2218. txt = StringTools.trim(txt);
  2219. beginChanges();
  2220. if( txt == "" ) {
  2221. sheet.props.dataFiles = js.Lib.undefined;
  2222. for( l in @:privateAccess sheet.sheet.lines )
  2223. Reflect.deleteField(l, "$cdbtype");
  2224. @:privateAccess sheet.sheet.linesData = js.Lib.undefined;
  2225. @:privateAccess sheet.sheet.separators = [];
  2226. } else {
  2227. sheet.props.dataFiles = txt;
  2228. @:privateAccess sheet.sheet.lines = null;
  2229. }
  2230. DataFiles.load();
  2231. endChanges();
  2232. refresh();
  2233. }
  2234. });
  2235. ContextMenu.createFromPoint(ide.mouseX, ide.mouseY, content);
  2236. }
  2237. public function close() {
  2238. for( t in tables.copy() )
  2239. t.dispose();
  2240. }
  2241. public function focus() {
  2242. #if js
  2243. if( element.is(":focus") ) return;
  2244. (element[0] : Dynamic).focus({ preventScroll : true });
  2245. #end
  2246. }
  2247. static public function getSheetProps( s : cdb.Sheet ) {
  2248. var pr : EditorSheetProps = s.props.editor;
  2249. if( pr == null ) pr = {};
  2250. return pr;
  2251. }
  2252. static public function getCategories(db: cdb.Database) : Array<String> {
  2253. var names : Array<String> = [];
  2254. for( s in db.sheets ) {
  2255. var props = getSheetProps(s);
  2256. if(props.categories != null) {
  2257. for(n in props.categories) {
  2258. if(names.indexOf(n) < 0)
  2259. names.push(n);
  2260. }
  2261. }
  2262. for(c in s.columns) {
  2263. var cProps = getColumnProps(c);
  2264. if(cProps.categories != null) {
  2265. for(n in cProps.categories) {
  2266. if(names.indexOf(n) < 0)
  2267. names.push(n);
  2268. }
  2269. }
  2270. }
  2271. }
  2272. names.sort((a, b) -> Reflect.compare(a, b));
  2273. return names;
  2274. }
  2275. }