gui_utils.js 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656
  1. /* This file is part of AToMPM - A Tool for Multi-Paradigm Modelling
  2. * Copyright 2011 by the AToMPM team and licensed under the LGPL
  3. * See COPYING.lesser and README.md in the root of this project for full details
  4. */
  5. GUIUtils = function(){
  6. /**
  7. * This is the old getElementById function
  8. */
  9. this.$$ = function( id ){
  10. return document.getElementById( id );
  11. };
  12. /**
  13. * Converts from page centric X coordinates to canvas centric X coordinates
  14. */
  15. this.convertToCanvasX = function(pageX){
  16. return pageX + $('#div_container').scrollLeft() - $('#contentDiv').offset().left;
  17. };
  18. /**
  19. * Converts from page centric Y coordinates to canvas centric Y coordinates
  20. */
  21. this.convertToCanvasY = function(pageY){
  22. return pageY + $('#div_container').scrollTop() - $('#contentDiv').offset().top;
  23. };
  24. /**
  25. * Disables the dock bar
  26. */
  27. this.disableDock = function(){
  28. $('#div_dock').attr('class', 'dock disabled_dock');
  29. };
  30. /**
  31. * Enables the dock bar
  32. */
  33. this.enableDock = function(){
  34. $('#div_dock').attr('class', 'dock');
  35. };
  36. /**
  37. * Constructs and returns a checked checkbox
  38. */
  39. this.getCheckbox = function(checked){
  40. var cb = $('<input>');
  41. cb.attr("type", 'checkbox');
  42. cb.prop("checked", checked);
  43. return cb;
  44. };
  45. // TODO: replace the bundled function with an actual object generation. The
  46. // current method is sloppy.
  47. /**
  48. * Constructs and returns an input field given an attribute's type and value,
  49. * bundled in the return value is a function to retrieve the input fields
  50. * content as well as its initial value
  51. *
  52. * For Maps and Lists, onfocus/onblur toggle a default entry to be shown.
  53. */
  54. this.getInputField = function (type,value){
  55. /* recursively expand specialTypes, if any */
  56. function explodeType(t)
  57. {
  58. var exploded = __specialTypes[t] || t;
  59. while( exploded.match(/\$/) )
  60. exploded = exploded.replace(
  61. /(\$.+?)([,\]>])/g,
  62. function(s,p1,p2){return __specialTypes[p1]+p2;});
  63. return exploded;
  64. }
  65. /* return a default value for the given exploded type string */
  66. function defaultEntry(et)
  67. {
  68. if( et == 'string' || et == 'code' )
  69. return "";
  70. else if( et == 'int' )
  71. return 0;
  72. else if( et == 'boolean' )
  73. return true;
  74. else if( et == 'double' )
  75. return 0.0;
  76. else if( et.match(/^ENUM/) )
  77. return et;
  78. else if( (matches=et.match(/^list<(.*)>$/)) )
  79. return [defaultEntry(matches[1])];
  80. else if( (matches=et.match(/^map<\[(.*?)\],\[(.*)\]>$/)) )
  81. {
  82. var m = {},
  83. keys = matches[1].split(','),
  84. types = [],
  85. depth = 0,
  86. index = 0;
  87. for( var i=0; i<matches[2].length; i++ )
  88. {
  89. if( matches[2].charAt(i) == '(' ||
  90. matches[2].charAt(i) == '<' )
  91. depth++;
  92. else if( matches[2].charAt(i) == ')' ||
  93. matches[2].charAt(i) == '>' )
  94. depth--;
  95. if( matches[2].charAt(i) == ',' && depth == 0 )
  96. {
  97. types.push( matches[2].substring(index,i) );
  98. index = i+1;
  99. }
  100. }
  101. types.push( matches[2].substring(index) );
  102. for( var i=0; i<keys.length; i++ )
  103. m[keys[i]] = defaultEntry(types[i]);
  104. return m;
  105. }
  106. else if( (matches=et.match(/^map<(.*),(.*)>$/)) )
  107. {
  108. var m = {};
  109. m[defaultEntry(matches[1])] = defaultEntry(matches[2]);
  110. return m;
  111. }
  112. }
  113. if( type == 'code' )
  114. var input = GUIUtils.getTextInput(utils.jsond(value,'\n')),
  115. getinput = function(_){return _.val();};
  116. else if( type.match(/^map/) ||
  117. type.match(/^list/) )
  118. {
  119. var input = GUIUtils.getTextInput(
  120. utils.jsond(utils.jsons(value,undefined,' '))),
  121. getinput = function(_){
  122. return utils.jsonp(utils.jsone(_.val()));
  123. };
  124. var de = defaultEntry( explodeType(type) ),
  125. isMap = type.match(/^map/),
  126. matches = undefined;
  127. input.focus(
  128. (isMap ?
  129. function()
  130. {
  131. var newVal = utils.mergeDicts([getinput(input),de]);
  132. input.val( utils.jsond(utils.jsons(newVal,undefined,' ')));
  133. } :
  134. function()
  135. {
  136. var newVal = getinput(input).concat(de);
  137. input.val( utils.jsond(utils.jsons(newVal,undefined,' ')));
  138. }));
  139. input.blur(
  140. (isMap ?
  141. function()
  142. {
  143. var val = getinput(input);
  144. utils.splitDict(val,utils.keys(de));
  145. input.val( utils.jsond(utils.jsons(val,undefined,' ')));
  146. } :
  147. function()
  148. {
  149. var val = getinput(input);
  150. if( utils.jsons(utils.tail(val)) == utils.jsons(de[0]) )
  151. val.pop();
  152. input.val( utils.jsond(utils.jsons(val,undefined,' ')));
  153. }));
  154. }
  155. else if( type.match(/^ENUM/) )
  156. {
  157. var vals = type.match(/^ENUM\((.*)\)$/)[1],
  158. input = GUIUtils.getSelector(vals.split(','),false,[value]),
  159. getinput =
  160. function(_){return HttpUtils.getSelectorSelection(_)[0];};
  161. }
  162. else if( type.match(/^boolean$/) )
  163. {
  164. var input = GUIUtils.getCheckbox(value),
  165. getinput =
  166. function(_){return _.prop("checked");};
  167. }
  168. else if( type.match(/^\$/) )
  169. return GUIUtils.getInputField(__specialTypes[type],value);
  170. else if ((matches = type.match("^file<(.*)>"))) {
  171. var input = GUIUtils.getFileInput(value,matches[1],"code_style string_input",1),
  172. getinput = function(_){return _.val();};
  173. }
  174. else
  175. var input = GUIUtils.getTextInput(value,"code_style string_input",1),
  176. getinput = (type == 'string' ?
  177. function(_){return _.val();} :
  178. function(_){return utils.jsonp(_.val());});
  179. input.title = explodeType(type);
  180. return {'input':input, 'getinput':getinput, 'oldVal':getinput(input)};
  181. };
  182. /**
  183. * Constructs and returns a <select> element with the choices list converted into
  184. * a list of <option> elements.
  185. *
  186. * @param choices - the choices for the <select> element
  187. * @param multipleChoice - if true, allows for multiple options to be selected
  188. * @param defaultSelect - sets the default selection for the list
  189. * @param numVisibleOptions - sets the number of visible options
  190. */
  191. this.getSelector = function(choices,multipleChoice,defaultSelection,numVisibleOptions){
  192. var select = $('<select>');
  193. select.attr("class", 'default_style');
  194. select.attr("size", Math.min(choices.length,numVisibleOptions || __MAX_SELECT_OPTIONS));
  195. select.attr("multiple", multipleChoice);
  196. choices.forEach(
  197. function(choice)
  198. {
  199. var option = $('<option>');
  200. option.val( choice );
  201. option.html( choice );
  202. select.append(option);
  203. if( defaultSelection != undefined &&
  204. utils.contains(defaultSelection,choice) )
  205. option.prop("selected", true);
  206. });
  207. return select;
  208. };
  209. /* constructs an INPUT given some text and a CSS class */
  210. /**
  211. * Constructs an <input> element
  212. *
  213. * @param text - the default text for the input element
  214. * @param className - the default class name for the input element
  215. * @param width - the default width of the element. If this is omitted
  216. * then the <input> defaults to 400px wide.
  217. */
  218. this.getStringInput = function(text,className,width){
  219. var input = $('<input>');
  220. input.attr("type", 'text');
  221. input.attr("class", className || 'default_style');
  222. input.val( text );
  223. input.css("width", width || '400px');
  224. return input;
  225. };
  226. this.getFileInput = function(code,pattern,className,rows){
  227. var string_input = this.getTextInput(code, className, rows);
  228. var extra_el = $('<button>');
  229. extra_el.attr("width", 16);
  230. extra_el.attr("height", 16);
  231. extra_el.html("...");
  232. extra_el.click(function(event) {
  233. var options = {'extensions':[pattern],
  234. 'multipleChoice':false,
  235. 'manualInput':false,
  236. 'title':'choose a rule model',
  237. 'startDir':'model'},
  238. callback =
  239. function(fnames)
  240. {
  241. string_input.val(fnames[0]);
  242. };
  243. WindowManagement.openDialog(_FILE_BROWSER,options,callback);
  244. event.stopPropagation();
  245. event.preventDefault();
  246. });
  247. string_input.extra_el = extra_el;
  248. return string_input;
  249. };
  250. /**
  251. * Constructs a <textarea> element. In this element, Alt + Right Arrow
  252. * is treated as a tab.
  253. *
  254. * @param code - the default code for this text area
  255. * @param className - the default class name for the <textarea>
  256. * @param rows - the default number of rows for this <textarea>
  257. */
  258. this.getTextInput = function(code,className,rows){
  259. var input = $('<textarea>');
  260. input.attr("cols", 80);
  261. rows = rows || 7;
  262. input.attr("rows", (rows || 7));
  263. input.val(code);
  264. input.attr("class", className || 'code_style');
  265. input.keydown( function(event) {
  266. if( event.keyCode == KEY_RIGHT_ARROW /* This is to simulate a tab press */ ) {
  267. if( currentKeys[ KEY_ALT ] == 1 && currentKeys[ KEY_CTRL ] != 1){
  268. var cursorPos = event.target.selectionStart,
  269. lineStart = input.val().lastIndexOf('\n',cursorPos-1)+1,
  270. tabBy = __TAB_WIDTH - (cursorPos-lineStart)%__TAB_WIDTH,
  271. tab = '';
  272. for(var i=0; i<tabBy; i++) tab += ' ';
  273. input.val(
  274. input.val().substring(0,cursorPos)+tab+
  275. input.val().substring(cursorPos));
  276. input.get(0).setSelectionRange(cursorPos+tabBy,cursorPos+tabBy);
  277. return true;
  278. }
  279. return true;
  280. }
  281. // https://media.giphy.com/media/12XMGIWtrHBl5e/giphy.gif
  282. else if( event.keyCode == KEY_ENTER )
  283. {
  284. if (rows > 1) {
  285. // only for multi-line input fields
  286. var cursorPos = event.target.selectionStart;
  287. input.val(
  288. input.val().substring(0,cursorPos)+'\r\n'+
  289. input.val().substring(cursorPos));
  290. input.get(0).setSelectionRange(cursorPos+1,cursorPos+1);
  291. }
  292. event.stopPropagation();
  293. event.preventDefault();
  294. return true;
  295. }
  296. });
  297. input.keyup( function (event) {
  298. if( event.keyCode == KEY_ENTER )
  299. {
  300. event.stopPropagation();
  301. event.preventDefault();
  302. }
  303. });
  304. return input;
  305. };
  306. /**
  307. * Constructs a <span> element.
  308. *
  309. * @param text - the default text to be displayed
  310. * @param className - the default class name
  311. */
  312. this.getTextSpan = function(text,className)
  313. {
  314. var span = $('<span>');
  315. span.html( text.replace(/\n/g,'<br/>') );
  316. span.attr("class", className || 'default_style');
  317. return span;
  318. };
  319. /**
  320. * Finds and removes the specified toolbar, if present
  321. *
  322. * @param tb - the toolbar to be removed
  323. */
  324. this.removeToolbar = function(tb){
  325. tb_fixed = tb.replace(/\//g, "\\/");
  326. if( $('#div_toolbar_'+tb_fixed) )
  327. {
  328. //Find the toolbar in the dock bar and remove it
  329. //from the DOM
  330. $("#div_dock").children("div").each( function() {
  331. if( this.id == "div_toolbar_" + tb ){
  332. $(this).remove();
  333. }
  334. });
  335. // Now delete it from the list of loaded toolbars
  336. delete __loadedToolbars[tb];
  337. }
  338. };
  339. /**
  340. * Throw out the current canvas, replace it with a new one.
  341. * 1. Unselect any current selection
  342. * 2. Clear canvas
  343. * 3. Clear icon and edge data
  344. * 4. reset canvas statechart
  345. */
  346. this.resetCanvas = function (){
  347. __select();
  348. __canvas.clear();
  349. __icons = {};
  350. __edges = {};
  351. __canvasBehaviourStatechart.init();
  352. };
  353. /**
  354. * Sets up a model popup window and displays it.
  355. *
  356. * @param elements - DOM elements to be displayed in order
  357. * @param getinput - function that retrieves the desired user-input
  358. * from "elements".
  359. * @param type - Can be __NO_BUTTONS, __ONE_BUTTON,
  360. * or __TWO_BUTTONS and controls the number of displayed buttons
  361. * @param title - the dialog title
  362. * @param callback - called with the result of getinput when the ok
  363. * button is clicked
  364. */
  365. this.setupAndShowDialog = function(elements,getinput,type,title,callback){
  366. // BehaviorManager.handleUserEvent(__EVENT_CANCELED_DIALOG);
  367. var dialog = $('#div_dialog'),
  368. the_id = __dialog_stack.length;
  369. dialog = dialog.clone().attr("id", 'div_dialog_'+the_id);
  370. dim_bg = $('#div_dim_bg'),
  371. div_title = $('<div>');
  372. __dialog_stack.push(dialog);
  373. dialog.appendTo(document.body);
  374. div_title.attr("class", 'dialog_title');
  375. div_title.append(GUIUtils.getTextSpan(title || ''));
  376. dialog.append(div_title);
  377. elements.forEach(
  378. function(e)
  379. {
  380. dialog.append(e);
  381. dialog.append( $('<br>') );
  382. });
  383. dialog.append( $('<br>') );
  384. if( type != __NO_BUTTONS )
  385. {
  386. var ok = $('<button class="okbutton">'); // HUSEYIN-ENTER
  387. ok.click( function(ev) {
  388. if( getinput == undefined )
  389. {
  390. BehaviorManager.handleUserEvent(__EVENT_OKAYED_DIALOG);
  391. if( callback != undefined )
  392. callback();
  393. }
  394. else
  395. {
  396. try{
  397. var input = getinput();
  398. } catch(err) {
  399. console.error('failed to retrieve dialog input :: '+err);
  400. return;
  401. }
  402. input = (utils.isArray(input) ?
  403. input.map(function(i) {return String(i);}) :
  404. input);
  405. BehaviorManager.handleUserEvent(__EVENT_OKAYED_DIALOG);
  406. callback(input);
  407. }
  408. });
  409. ok.attr("id", "dialog_btn");
  410. ok.html('ok');
  411. dialog.append(ok);
  412. }
  413. if( type == __TWO_BUTTONS )
  414. {
  415. var cancel = $('<button>');
  416. cancel.click(function(ev) {
  417. BehaviorManager.handleUserEvent(__EVENT_CANCELED_DIALOG);
  418. });
  419. cancel.html('cancel');
  420. dialog.append(cancel);
  421. }
  422. BehaviorManager.setActiveBehaviourStatechart(__SC_DIALOG);
  423. BehaviorManager.handleUserEvent(__EVENT_SHOW_DIALOG);
  424. };
  425. /*
  426. NOTE:: the sortedButtonNames() function sorts icon definition metamodels and
  427. button models s.t. buttons appear in an order that reflects their
  428. model... to be perfectly clean, this should be absorbed into icon
  429. definition and button model compilers, but since button models aren't
  430. compiled, it was simpler to just let this relatively simple logic live
  431. here
  432. */
  433. /**
  434. * Sets up and shows a toolbar according to the given button model or metamodel.
  435. *
  436. * This method:
  437. * 1. Removes any old instance of the toolbar if it currently exists
  438. * 2. Creates a <div> to hold the buttons
  439. * 3. Creates each button and appends it to the <div>
  440. * 4. Add the <div> to the dock
  441. * 5. Map the toolbar to its data
  442. *
  443. * @param tb - the toolbar to setup and show
  444. * @param data - the data to bind the toolbar to
  445. * @param type - the toolbar type, can be __BUTTON_TOOLBAR or __METAMODEL_TOOLBAR
  446. */
  447. this.setupAndShowToolbar = function(tb,data,type)
  448. {
  449. var imgSrc =
  450. function(name)
  451. {
  452. return (type == __BUTTON_TOOLBAR ?
  453. tb.substring(0,tb.lastIndexOf('/')+1)+name+'.icon.png' :
  454. '/Formalisms/default.icon.png');
  455. },
  456. className =
  457. function()
  458. {return (type == __BUTTON_TOOLBAR ? 'toolbar_bm' : 'toolbar_mm');},
  459. buttons =
  460. (type == __BUTTON_TOOLBAR ? data.asm.nodes : data.types),
  461. sortedButtons =
  462. function()
  463. {
  464. return (type == __BUTTON_TOOLBAR ?
  465. /* sort button names according to their position in their
  466. associated buttons model */
  467. utils.sortDict(data.csm.nodes,
  468. function(b1,b2)
  469. {
  470. var pos1 = b1['position']['value'],
  471. pos2 = b2['position']['value'];
  472. if( (pos1[1] < pos2[1]) ||
  473. (pos1[1] == pos2[1] && pos1[0] < pos2[0]) )
  474. return -1;
  475. return 1;
  476. }) :
  477. utils.sortDict(data.types,
  478. /* sort type names according to their IconIcon's position in
  479. the associated icon definition model */
  480. function(b1,b2)
  481. {
  482. var pos1 = undefined,
  483. pos2 = undefined;
  484. b1.some( function(attr)
  485. {
  486. if(attr['name'] == 'position')
  487. pos1 = attr['default'];
  488. return pos1;
  489. });
  490. b2.some( function(attr)
  491. {
  492. if(attr['name'] == 'position')
  493. pos2 = attr['default'];
  494. return pos2;
  495. });
  496. if( (pos1[1] < pos2[1]) ||
  497. (pos1[1] == pos2[1] && pos1[0] < pos2[0]) )
  498. return -1;
  499. return 1;
  500. }) );
  501. },
  502. createButton =
  503. function(name,tooltip,code)
  504. {
  505. var div = $('<div>'),
  506. img = $('<img>');
  507. div.addClass( 'toolbar_button' );
  508. div.attr("id", tb+'/'+name);
  509. div.attr("title", tooltip);
  510. div.click( function(ev){
  511. var res = HttpUtils.safeEval(code);
  512. if( res['$uerr'] )
  513. WindowManagement.openDialog(
  514. _ERROR,
  515. 'unexpected error in button code ::\n '+res['$uerr']);
  516. else if( res['$err'] )
  517. WindowManagement.openDialog(
  518. _ERROR,
  519. 'error in button code ::\n '+res['$err']);
  520. });
  521. var url = HttpUtils.url(imgSrc(name),__NO_WID);
  522. img.attr("src", url);
  523. //handle missing icon
  524. let defaultUrl = HttpUtils.url("/Formalisms/default.icon.png");
  525. let missingMsg = "Warning: The icon \"" + url + "\" is missing! The default icon has been used.";
  526. let onerrorStr = "this.onerror = ''; this.src = '" + defaultUrl + "'; console.log('" + missingMsg + "');";
  527. img.attr('onerror', onerrorStr);
  528. div.append(img);
  529. return div;
  530. };
  531. GUIUtils.removeToolbar(tb);
  532. var tb_div = $('<div>');
  533. tb_div.attr("id", 'div_toolbar_'+tb);
  534. tb_div.attr("class", className()+' toolbar unselectable' );
  535. tb_div.attr("title", tb);
  536. sortedButtons().forEach(
  537. function(b)
  538. {
  539. if( type == __METAMODEL_TOOLBAR && b.match(/(.*)Link$/) )
  540. return;
  541. var spc1 = $('<span>'),
  542. spc2 = $('<span>');
  543. // spc1.className = spc2.className = 'toolbar_space';
  544. spc1.attr("class", "toolbar_space" );
  545. spc2.attr("class", "toolbar_space" );
  546. tb_div.append(spc1);
  547. if( type == __BUTTON_TOOLBAR )
  548. tb_div.append(
  549. createButton(
  550. buttons[b]['name']['value'],
  551. buttons[b]['tooltip']['value'],
  552. buttons[b]['code']['value']) );
  553. else if( (matches = b.match(/(.*)Icon/)) )
  554. tb_div.append(
  555. createButton(
  556. b,
  557. 'create instance(s) of '+b.match(/(.*)Icon/)[1],
  558. '_setTypeToCreate("'+
  559. tb.substring(0,tb.length-'.metamodel'.length)+
  560. '/'+b+'");') );
  561. tb_div.append(spc2);
  562. } );
  563. if( tb_div.children().length == 0 )
  564. tb_div.append( GUIUtils.getTextSpan(tb,'toolbar_alt') );
  565. //get the toolbar
  566. let dock = $('#div_dock');
  567. //create an array and add the new toolbar
  568. let items = Array.from(dock[0].childNodes);
  569. items.push(tb_div[0]);
  570. //sort the dock
  571. items.sort(function(a, b) {
  572. //main menu comes first
  573. if (a.id.includes("MainMenu")){
  574. return -1;
  575. }
  576. //toolbars come first
  577. if (a.id.includes("Toolbars") && !(b.id.includes("Toolbars"))){
  578. return -1;
  579. }
  580. if (b.id.includes("Toolbars") && !(a.id.includes("Toolbars"))){
  581. return 1;
  582. }
  583. //otherwise, sort by name
  584. return a.id == b.id? 0 : (a.id > b.id ? 1 : -1);
  585. });
  586. //add the elements back into the dock
  587. for (let i = 0; i < items.length; ++i) {
  588. dock.append(items[i]);
  589. }
  590. __loadedToolbars[tb] = data;
  591. };
  592. return this;
  593. }();