Editor.hx 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680
  1. package hide.comp.cdb;
  2. import hxd.Key in K;
  3. typedef UndoState = {
  4. var data : Any;
  5. var cursor : { sheet : String, x : Int, y : Int, select : Null<{ x : Int, y : Int }> };
  6. var tables : Array<{ sheet : String, parent : { sheet : String, line : Int, column : Int } }>;
  7. }
  8. typedef EditorApi = {
  9. function load( data : Any ) : Void;
  10. function copy() : Any;
  11. function save() : Void;
  12. var ?currentValue : Any;
  13. var ?undo : hide.ui.UndoHistory;
  14. var ?undoState : Array<UndoState>;
  15. var ?editor : Editor;
  16. }
  17. @:allow(hide.comp.cdb)
  18. class Editor extends Component {
  19. var base : cdb.Database;
  20. var sheet : cdb.Sheet;
  21. var existsCache : Map<String,{ t : Float, r : Bool }> = new Map();
  22. var tables : Array<Table> = [];
  23. var searchBox : Element;
  24. var displayMode : Table.DisplayMode;
  25. var clipboard : {
  26. text : String,
  27. data : Array<{}>,
  28. schema : Array<cdb.Data.Column>,
  29. };
  30. var changesDepth : Int = 0;
  31. var api : EditorApi;
  32. public var config : hide.Config;
  33. public var cursor : Cursor;
  34. public var keys : hide.ui.Keys;
  35. public var undo : hide.ui.UndoHistory;
  36. public function new(sheet,config,api,?parent) {
  37. super(parent,null);
  38. this.api = api;
  39. this.config = config;
  40. this.sheet = sheet;
  41. if( api.undoState == null ) api.undoState = [];
  42. if( api.editor == null ) api.editor = this;
  43. if( api.currentValue == null ) api.currentValue = api.copy();
  44. this.undo = api.undo == null ? new hide.ui.UndoHistory() : api.undo;
  45. api.undo = undo;
  46. init();
  47. }
  48. function init() {
  49. element.attr("tabindex", 0);
  50. element.on("focus", function(_) onFocus());
  51. element.on("blur", function(_) cursor.hide());
  52. element.on("keypress", function(e) {
  53. if( e.target.nodeName == "INPUT" )
  54. return;
  55. var cell = cursor.getCell();
  56. if( cell != null && cell.isTextInput() && !e.ctrlKey )
  57. cell.edit();
  58. });
  59. keys = new hide.ui.Keys(element);
  60. keys.addListener(onKey);
  61. keys.register("search", function() {
  62. searchBox.show();
  63. searchBox.find("input").focus().select();
  64. });
  65. keys.register("copy", onCopy);
  66. keys.register("paste", onPaste);
  67. keys.register("delete", onDelete);
  68. keys.register("cdb.showReferences", showReferences);
  69. keys.register("undo", function() undo.undo());
  70. keys.register("redo", function() undo.redo());
  71. keys.register("cdb.insertLine", function() { insertLine(cursor.table,cursor.y); cursor.move(0,1,false,false); });
  72. for( k in ["cdb.editCell","rename"] )
  73. keys.register(k, function() {
  74. var c = cursor.getCell();
  75. if( c != null ) c.edit();
  76. });
  77. keys.register("cdb.closeList", function() {
  78. var c = cursor.getCell();
  79. var sub = Std.instance(c == null ? cursor.table : c.table, SubTable);
  80. if( sub != null ) {
  81. sub.cell.element.click();
  82. return;
  83. }
  84. if( cursor.select != null ) {
  85. cursor.select = null;
  86. cursor.update();
  87. }
  88. });
  89. keys.register("cdb.gotoReference", gotoReference);
  90. base = sheet.base;
  91. cursor = new Cursor(this);
  92. if( displayMode == null ) displayMode = Table;
  93. refresh();
  94. }
  95. function onKey( e : js.jquery.Event ) {
  96. switch( e.keyCode ) {
  97. case K.LEFT:
  98. cursor.move( -1, 0, e.shiftKey, e.ctrlKey);
  99. return true;
  100. case K.RIGHT:
  101. cursor.move( 1, 0, e.shiftKey, e.ctrlKey);
  102. return true;
  103. case K.UP:
  104. cursor.move( 0, -1, e.shiftKey, e.ctrlKey);
  105. return true;
  106. case K.DOWN:
  107. cursor.move( 0, 1, e.shiftKey, e.ctrlKey);
  108. return true;
  109. case K.TAB:
  110. cursor.move( e.shiftKey ? -1 : 1, 0, false, false);
  111. return true;
  112. case K.SPACE:
  113. e.preventDefault(); // prevent scroll
  114. }
  115. return false;
  116. }
  117. function searchFilter( filter : String ) {
  118. if( filter == "" ) filter = null;
  119. if( filter != null ) filter = filter.toLowerCase();
  120. var lines = element.find("table.cdb-sheet > tr").not(".head");
  121. lines.removeClass("filtered");
  122. if( filter != null ) {
  123. for( t in lines ) {
  124. if( t.textContent.toLowerCase().indexOf(filter) < 0 )
  125. t.classList.add("filtered");
  126. }
  127. while( lines.length > 0 ) {
  128. lines = lines.filter(".list").not(".filtered").prev();
  129. lines.removeClass("filtered");
  130. }
  131. }
  132. }
  133. function onCopy() {
  134. var sel = cursor.getSelection();
  135. if( sel == null )
  136. return;
  137. var data = [];
  138. for( y in sel.y1...sel.y2+1 ) {
  139. var obj = cursor.table.lines[y].obj;
  140. var out = {};
  141. for( x in sel.x1...sel.x2+1 ) {
  142. var c = cursor.table.sheet.columns[x];
  143. var v = Reflect.field(obj, c.name);
  144. if( v != null )
  145. Reflect.setField(out, c.name, v);
  146. }
  147. data.push(out);
  148. }
  149. clipboard = {
  150. data : data,
  151. text : Std.string([for( o in data ) cursor.table.sheet.objToString(o,true)]),
  152. schema : [for( x in sel.x1...sel.x2+1 ) cursor.table.sheet.columns[x]],
  153. };
  154. ide.setClipboard(clipboard.text);
  155. }
  156. function onPaste() {
  157. var text = ide.getClipboard();
  158. if( clipboard == null || text != clipboard.text ) {
  159. // TODO : edit and copy text
  160. return;
  161. }
  162. beginChanges();
  163. var sheet = cursor.table.sheet;
  164. var posX = cursor.x < 0 ? 0 : cursor.x;
  165. var posY = cursor.y < 0 ? 0 : cursor.y;
  166. for( obj1 in clipboard.data ) {
  167. if( posY == sheet.lines.length )
  168. sheet.newLine();
  169. var obj2 = sheet.lines[posY];
  170. for( cid in 0...clipboard.schema.length ) {
  171. var c1 = clipboard.schema[cid];
  172. var c2 = sheet.columns[cid + posX];
  173. if( c2 == null ) continue;
  174. var f = base.getConvFunction(c1.type, c2.type);
  175. var v : Dynamic = Reflect.field(obj1, c1.name);
  176. if( f == null )
  177. v = base.getDefault(c2);
  178. else {
  179. // make a deep copy to erase references
  180. if( v != null ) v = haxe.Json.parse(haxe.Json.stringify(v));
  181. if( f.f != null )
  182. v = f.f(v);
  183. }
  184. if( v == null && !c2.opt )
  185. v = base.getDefault(c2);
  186. if( v == null )
  187. Reflect.deleteField(obj2, c2.name);
  188. else
  189. Reflect.setField(obj2, c2.name, v);
  190. }
  191. posY++;
  192. }
  193. endChanges();
  194. sheet.sync();
  195. refreshAll();
  196. }
  197. function onDelete() {
  198. var sel = cursor.getSelection();
  199. if( sel == null )
  200. return;
  201. var hasChanges = false;
  202. beginChanges();
  203. if( cursor.x < 0 ) {
  204. // delete lines
  205. var y = sel.y2;
  206. while( y >= sel.y1 ) {
  207. var line = cursor.table.lines[y];
  208. line.table.sheet.deleteLine(line.index);
  209. hasChanges = true;
  210. y--;
  211. }
  212. cursor.set(cursor.table, -1, sel.y1, null, false);
  213. } else {
  214. // delete cells
  215. for( y in sel.y1...sel.y2+1 ) {
  216. var line = cursor.table.lines[y];
  217. for( x in sel.x1...sel.x2+1 ) {
  218. var c = line.columns[x];
  219. var old = Reflect.field(line.obj, c.name);
  220. var def = base.getDefault(c,false);
  221. if( old == def )
  222. continue;
  223. changeObject(line,c,def);
  224. hasChanges = true;
  225. }
  226. }
  227. }
  228. endChanges();
  229. if( hasChanges )
  230. refreshAll();
  231. }
  232. public function changeObject( line : Line, column : cdb.Data.Column, value : Dynamic ) {
  233. beginChanges();
  234. var prev = Reflect.field(line.obj, column.name);
  235. if( value == null )
  236. Reflect.deleteField(line.obj, column.name);
  237. else
  238. Reflect.setField(line.obj, column.name, value);
  239. line.table.sheet.updateValue(column, line.index, prev);
  240. endChanges();
  241. }
  242. /**
  243. Call before modifying the database, allow to group several changes together.
  244. Allow recursion, only last endChanges() will trigger db save and undo point creation.
  245. **/
  246. public function beginChanges() {
  247. if( changesDepth == 0 )
  248. api.undoState.unshift(getState());
  249. changesDepth++;
  250. }
  251. function getState() : UndoState {
  252. return {
  253. data : api.currentValue,
  254. cursor : cursor.table == null ? null : {
  255. sheet : cursor.table.sheet.name,
  256. x : cursor.x,
  257. y : cursor.y,
  258. select : cursor.select == null ? null : { x : cursor.select.x, y : cursor.select.y }
  259. },
  260. tables : [for( i in 1...tables.length ) {
  261. var t = tables[i];
  262. var tp = t.sheet.parent;
  263. { sheet : t.sheet.name, parent : { sheet : tp.sheet.name, line : tp.line, column : tp.column } }
  264. }],
  265. };
  266. }
  267. function setState( state : UndoState ) {
  268. var cur = state.cursor;
  269. for( t in state.tables ) {
  270. var tparent = null;
  271. for( tp in tables )
  272. if( tp.sheet.name == t.parent.sheet ) {
  273. tparent = tp;
  274. break;
  275. }
  276. if( tparent != null )
  277. tparent.lines[t.parent.line].cells[t.parent.column].open(true);
  278. }
  279. if( cur != null ) {
  280. var table = null;
  281. for( t in tables )
  282. if( t.sheet.name == cur.sheet ) {
  283. table = t;
  284. break;
  285. }
  286. if( table != null )
  287. focus();
  288. cursor.set(table, cur.x, cur.y, cur.select == null ? null : { x : cur.select.x, y : cur.select.y } );
  289. } else
  290. cursor.set();
  291. }
  292. /**
  293. Call when changes are done, after endChanges.
  294. **/
  295. public function endChanges() {
  296. changesDepth--;
  297. if( changesDepth == 0 ) {
  298. var f = makeCustom(api);
  299. if( f != null ) undo.change(Custom(f));
  300. }
  301. }
  302. // do not reference "this" editor in undo state !
  303. static function makeCustom( api : EditorApi ) {
  304. var newValue = api.copy();
  305. if( newValue == api.currentValue )
  306. return null;
  307. var state = api.undoState[0];
  308. api.currentValue = newValue;
  309. api.save();
  310. return function(undo) api.editor.handleUndo(state, newValue, undo);
  311. }
  312. function handleUndo( state : UndoState, newValue : Any, undo : Bool ) {
  313. if( undo ) {
  314. api.undoState.shift();
  315. api.currentValue = state.data;
  316. } else {
  317. api.undoState.unshift(state);
  318. api.currentValue = newValue;
  319. }
  320. api.load(api.currentValue);
  321. refreshAll(state);
  322. api.save();
  323. }
  324. function showReferences() {
  325. if( cursor.table == null ) return;
  326. // todo : port from old cdb
  327. }
  328. function gotoReference() {
  329. var c = cursor.getCell();
  330. if( c == null || c.value == null ) return;
  331. switch( c.column.type ) {
  332. case TRef(s):
  333. var sd = base.getSheet(s);
  334. if( sd == null ) return;
  335. var k = sd.index.get(c.value);
  336. if( k == null ) return;
  337. var index = sd.lines.indexOf(k.obj);
  338. if( index >= 0 ) openReference(sd, index, 0);
  339. default:
  340. }
  341. }
  342. function openReference( s : cdb.Sheet, line : Int, column : Int ) {
  343. ide.open("hide.view.CdbTable", { path : s.name }, function(view) @:privateAccess Std.instance(view,hide.view.CdbTable).editor.cursor.setDefault(line,column));
  344. }
  345. public function syncSheet( ?base ) {
  346. if( base == null ) base = this.base;
  347. this.base = base;
  348. // swap sheet if it was modified
  349. for( s in base.sheets )
  350. if( s.name == this.sheet.name ) {
  351. this.sheet = s;
  352. break;
  353. }
  354. }
  355. function refreshAll( ?state : UndoState ) {
  356. api.editor.refresh(state);
  357. }
  358. public function refresh( ?state : UndoState ) {
  359. if( state == null )
  360. state = getState();
  361. base.sync();
  362. element.empty();
  363. element.addClass('cdb');
  364. searchBox = new Element("<div>").addClass("searchBox").appendTo(element);
  365. new Element("<input type='text'>").appendTo(searchBox).keydown(function(e) {
  366. if( e.keyCode == 27 ) {
  367. searchBox.find("i").click();
  368. return;
  369. }
  370. }).keyup(function(e) {
  371. searchFilter(e.getThis().val());
  372. });
  373. new Element("<i>").addClass("fa fa-times-circle").appendTo(searchBox).click(function(_) {
  374. searchFilter(null);
  375. searchBox.toggle();
  376. });
  377. if( sheet.columns.length == 0 ) {
  378. new Element("<a>Add a column</a>").appendTo(element).click(function(_) {
  379. newColumn(sheet);
  380. });
  381. return;
  382. }
  383. var content = new Element("<table>");
  384. tables = [];
  385. new Table(this, sheet, content, displayMode);
  386. content.appendTo(element);
  387. if( state != null )
  388. setState(state);
  389. if( cursor.table != null ) {
  390. for( t in tables )
  391. if( t.sheet.name == cursor.table.sheet.name )
  392. cursor.table = t;
  393. cursor.update();
  394. }
  395. }
  396. function quickExists(path) {
  397. var c = existsCache.get(path);
  398. if( c == null ) {
  399. c = { t : -1e9, r : false };
  400. existsCache.set(path, c);
  401. }
  402. var t = haxe.Timer.stamp();
  403. if( c.t < t - 10 ) { // cache result for 10s
  404. c.r = sys.FileSystem.exists(path);
  405. c.t = t;
  406. }
  407. return c.r;
  408. }
  409. function getLine( sheet : cdb.Sheet, index : Int ) {
  410. for( t in tables )
  411. if( t.sheet == sheet )
  412. return t.lines[index];
  413. return null;
  414. }
  415. public function newColumn( sheet : cdb.Sheet, ?index : Int ) {
  416. var modal = new hide.comp.cdb.ModalColumnForm(base, null, element);
  417. modal.setCallback(function() {
  418. var c = modal.getColumn(base, sheet, null);
  419. if (c == null) {
  420. return;
  421. }
  422. var err = newColumn_save(sheet, c, index + 1);
  423. if (err != null) {
  424. modal.error(err);
  425. } else {
  426. modal.closeModal();
  427. }
  428. });
  429. }
  430. function newColumn_save( sheet : cdb.Sheet, c : cdb.Data.Column, ?index : Int ) {
  431. beginChanges();
  432. var err = sheet.addColumn(c, index);
  433. endChanges();
  434. if (err != null) {
  435. return err;
  436. }
  437. for( t in tables )
  438. if( t.sheet == sheet )
  439. t.refresh();
  440. return null;
  441. }
  442. public function editColumn( sheet : cdb.Sheet, col : cdb.Data.Column ) {
  443. var modal = new hide.comp.cdb.ModalColumnForm(base, col, element);
  444. modal.setCallback(function() {
  445. var c = modal.getColumn(base, sheet, col);
  446. if (c == null) {
  447. return;
  448. }
  449. var err = editColumn_save(base, sheet, col, c);
  450. if (err != null) {
  451. modal.error(err);
  452. } else {
  453. modal.closeModal();
  454. }
  455. });
  456. }
  457. function editColumn_save( base : cdb.Database, sheet : cdb.Sheet, colOld : cdb.Data.Column, colNew : cdb.Data.Column ) {
  458. beginChanges();
  459. var err = base.updateColumn(sheet, colOld, colNew);
  460. endChanges();
  461. for( t in tables )
  462. if( t.sheet == sheet )
  463. t.refresh();
  464. if (err != null) {
  465. return err;
  466. }
  467. return null;
  468. }
  469. public function deleteColumn( sheet : cdb.Sheet, cname : String ) {
  470. beginChanges();
  471. sheet.deleteColumn(cname);
  472. endChanges();
  473. }
  474. public function moveColumnLeft( sheet : cdb.Sheet, index : Int ) {
  475. beginChanges();
  476. var c = sheet.columns[index];
  477. if( index > 0 ) {
  478. sheet.columns.remove(c);
  479. sheet.columns.insert(index - 1, c);
  480. }
  481. endChanges();
  482. }
  483. public function moveColumnRight( sheet : cdb.Sheet, index : Int ) {
  484. beginChanges();
  485. var c = sheet.columns[index];
  486. if( index > 0 ) {
  487. sheet.columns.remove(c);
  488. sheet.columns.insert(index + 1, c);
  489. }
  490. endChanges();
  491. }
  492. public function insertLine( table : Table, index = 0 ) {
  493. if( table.displayMode == Properties ) {
  494. var ins = table.element.find("select.insertField");
  495. var options = [for( o in ins.find("option").elements() ) o.val()];
  496. ins.attr("size", options.length);
  497. options.shift();
  498. ins.focus();
  499. var index = 0;
  500. ins.val(options[0]);
  501. ins.off();
  502. ins.blur(function(_) table.refresh());
  503. ins.keydown(function(e) {
  504. switch( e.keyCode ) {
  505. case K.ESCAPE:
  506. element.focus();
  507. case K.UP if( index > 0 ):
  508. ins.val(options[--index]);
  509. case K.DOWN if( index < options.length - 1 ):
  510. ins.val(options[++index]);
  511. case K.ENTER:
  512. table.insertProperty(ins.val());
  513. default:
  514. }
  515. e.stopPropagation();
  516. e.preventDefault();
  517. });
  518. return;
  519. }
  520. beginChanges();
  521. table.sheet.newLine(index);
  522. endChanges();
  523. table.refresh();
  524. }
  525. public function popupColumn( table : Table, col : cdb.Data.Column ) {
  526. var indexColumn = 0;
  527. for (c in table.sheet.columns) {
  528. if (c == col) {
  529. break;
  530. }
  531. indexColumn++;
  532. }
  533. var menu : Array<hide.comp.ContextMenu.ContextMenuItem> = [];
  534. if( col.type == TString && col.kind == Script )
  535. menu.push({ label : "Edit all", click : function() editScripts(table,col) });
  536. menu.push({ label : "Edit", click : function () {
  537. editColumn(table.sheet, col);
  538. }
  539. });
  540. menu.push({ label : "Add Column", click : function () {
  541. newColumn(table.sheet, indexColumn);
  542. }
  543. });
  544. menu.push({ label: "sep", isSeparator: true });
  545. menu.push({ label : "Move Left", enabled: (indexColumn > 0), click : function () {
  546. moveColumnLeft(table.sheet, indexColumn);
  547. table.refresh();
  548. }
  549. });
  550. menu.push({ label : "Move Right", enabled: (indexColumn < table.sheet.columns.length - 1), click : function () {
  551. moveColumnRight(table.sheet, indexColumn);
  552. table.refresh();
  553. }
  554. });
  555. menu.push({ label: "sep", isSeparator: true });
  556. menu.push({ label : "Delete", click : function () {
  557. deleteColumn(table.sheet, col.name);
  558. table.refresh();
  559. }
  560. });
  561. new hide.comp.ContextMenu(menu);
  562. }
  563. function editScripts( table : Table, col : cdb.Data.Column ) {
  564. }
  565. function moveLine( line : Line, delta : Int ) {
  566. beginChanges();
  567. var index = sheet.moveLine(line.index, delta);
  568. if( index != null ) {
  569. cursor.set(cursor.table, -1, index);
  570. refresh();
  571. }
  572. endChanges();
  573. }
  574. public function popupLine( line : Line ) {
  575. var sheet = line.table.sheet;
  576. var sepIndex = sheet.separators.indexOf(line.index);
  577. new hide.comp.ContextMenu([
  578. { label : "Move Up", click : moveLine.bind(line,-1) },
  579. { label : "Move Down", click : moveLine.bind(line,1) },
  580. { label : "Insert", click : function() {
  581. insertLine(line.table,line.index);
  582. cursor.move(0,1,false,false);
  583. } },
  584. { label : "Delete", click : function() {
  585. beginChanges();
  586. sheet.deleteLine(line.index);
  587. endChanges();
  588. refreshAll();
  589. } },
  590. { label : "Separator", enabled : !sheet.props.hide, checked : sepIndex >= 0, click : function() {
  591. beginChanges();
  592. if( sepIndex >= 0 ) {
  593. sheet.separators.splice(sepIndex, 1);
  594. if( sheet.props.separatorTitles != null ) sheet.props.separatorTitles.splice(sepIndex, 1);
  595. } else {
  596. sepIndex = sheet.separators.length;
  597. for( i in 0...sheet.separators.length )
  598. if( sheet.separators[i] > line.index ) {
  599. sepIndex = i;
  600. break;
  601. }
  602. sheet.separators.insert(sepIndex, line.index);
  603. if( sheet.props.separatorTitles != null && sheet.props.separatorTitles.length > sepIndex )
  604. sheet.props.separatorTitles.insert(sepIndex, null);
  605. }
  606. endChanges();
  607. refresh();
  608. } }
  609. ]);
  610. }
  611. public function close() {
  612. for( t in tables.copy() )
  613. t.dispose();
  614. }
  615. public function focus() {
  616. if( element.is(":focus") ) return;
  617. element.focus();
  618. onFocus();
  619. }
  620. public dynamic function onFocus() {
  621. }
  622. }