My Marlin configs for Fabrikator Mini and CTC i3 Pro B
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

configurator.js 49KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432
  1. /**
  2. * configurator.js
  3. *
  4. * Marlin Configuration Utility
  5. * - Web form for entering configuration options
  6. * - A reprap calculator to calculate movement values
  7. * - Uses HTML5 to generate downloadables in Javascript
  8. * - Reads and parses standard configuration files from local folders
  9. *
  10. * Supporting functions
  11. * - Parser to read Marlin Configuration.h and Configuration_adv.h files
  12. * - Utilities to replace values in configuration files
  13. */
  14. "use strict";
  15. $(function(){
  16. /**
  17. * Github API useful GET paths. (Start with "https://api.github.com/repos/:owner/:repo/")
  18. *
  19. * contributors Get a list of contributors
  20. * tags Get a list of tags
  21. * contents/[path]?ref=branch/tag/commit Get the contents of a file
  22. */
  23. // GitHub
  24. // Warning! Limited to 60 requests per hour!
  25. var config = {
  26. type: 'github',
  27. host: 'https://api.github.com',
  28. owner: 'MarlinFirmware',
  29. repo: 'Marlin',
  30. ref: 'Development',
  31. path: 'Marlin/configurator/config'
  32. };
  33. /**/
  34. /* // Remote
  35. var config = {
  36. type: 'remote',
  37. host: 'http://www.thinkyhead.com',
  38. path: '_marlin/config'
  39. };
  40. /**/
  41. /* // Local
  42. var config = {
  43. type: 'local',
  44. path: 'config'
  45. };
  46. /**/
  47. function github_command(conf, command, path) {
  48. var req = conf.host+'/repos/'+conf.owner+'/'+conf.repo+'/'+command;
  49. if (path) req += '/' + path;
  50. return req;
  51. }
  52. function config_path(item) {
  53. var path = '', ref = '';
  54. switch(config.type) {
  55. case 'github':
  56. path = github_command(config, 'contents', config.path);
  57. if (config.ref !== undefined) ref = '?ref=' + config.ref;
  58. break;
  59. case 'remote':
  60. path = config.host + '/' + config.path + '/';
  61. break;
  62. case 'local':
  63. path = config.path + '/';
  64. break;
  65. }
  66. return path + '/' + item + ref;
  67. }
  68. // Extend builtins
  69. String.prototype.lpad = function(len, chr) {
  70. if (chr === undefined) { chr = ' '; }
  71. var s = this+'', need = len - s.length;
  72. if (need > 0) { s = new Array(need+1).join(chr) + s; }
  73. return s;
  74. };
  75. String.prototype.prePad = function(len, chr) { return len ? this.lpad(len, chr) : this; };
  76. String.prototype.zeroPad = function(len) { return this.prePad(len, '0'); };
  77. String.prototype.toHTML = function() { return jQuery('<div>').text(this).html(); };
  78. String.prototype.regEsc = function() { return this.replace(/[.?*+^$[\]\\(){}|-]/g, "\\$&"); }
  79. String.prototype.lineCount = function(ind) { var len = (ind === undefined ? this : this.substr(0,ind*1)).split(/\r?\n|\r/).length; return len > 0 ? len - 1 : 0; };
  80. String.prototype.line = function(num) { var arr = this.split(/\r?\n|\r/); return num < arr.length ? arr[1*num] : ''; };
  81. String.prototype.replaceLine = function(num,txt) { var arr = this.split(/\r?\n|\r/); if (num < arr.length) { arr[num] = txt; return arr.join('\n'); } else return this; }
  82. String.prototype.toLabel = function() { return this.replace(/[\[\]]/g, '').replace(/_/g, ' ').toTitleCase(); }
  83. String.prototype.toTitleCase = function() { return this.replace(/([A-Z])(\w+)/gi, function(m,p1,p2) { return p1.toUpperCase() + p2.toLowerCase(); }); }
  84. Number.prototype.limit = function(m1, m2) {
  85. if (m2 == null) return this > m1 ? m1 : this;
  86. return this < m1 ? m1 : this > m2 ? m2 : this;
  87. };
  88. Date.prototype.fileStamp = function(filename) {
  89. var fs = this.getFullYear()
  90. + ((this.getMonth()+1)+'').zeroPad(2)
  91. + (this.getDate()+'').zeroPad(2)
  92. + (this.getHours()+'').zeroPad(2)
  93. + (this.getMinutes()+'').zeroPad(2)
  94. + (this.getSeconds()+'').zeroPad(2);
  95. if (filename !== undefined)
  96. return filename.replace(/^(.+)(\.\w+)$/g, '$1-['+fs+']$2');
  97. return fs;
  98. }
  99. /**
  100. * selectField.addOptions takes an array or keyed object
  101. */
  102. $.fn.extend({
  103. addOptions: function(arrObj) {
  104. return this.each(function() {
  105. var sel = $(this);
  106. var isArr = Object.prototype.toString.call(arrObj) == "[object Array]";
  107. $.each(arrObj, function(k, v) {
  108. sel.append( $('<option>',{value:isArr?v:k}).text(v) );
  109. });
  110. });
  111. },
  112. noSelect: function() {
  113. return this
  114. .attr('unselectable', 'on')
  115. .css('user-select', 'none')
  116. .on('selectstart', false);
  117. },
  118. unblock: function(on) {
  119. on ? this.removeClass('blocked') : this.addClass('blocked');
  120. return this;
  121. }
  122. });
  123. // The app is a singleton
  124. window.configuratorApp = (function(){
  125. // private variables and functions go here
  126. var self,
  127. pi2 = Math.PI * 2,
  128. has_boards = false, has_config = false, has_config_adv = false,
  129. boards_file = 'boards.h',
  130. config_file = 'Configuration.h',
  131. config_adv_file = 'Configuration_adv.h',
  132. $msgbox = $('#message'),
  133. $form = $('#config_form'),
  134. $tooltip = $('#tooltip'),
  135. $cfg = $('#config_text'), $adv = $('#config_adv_text'),
  136. $config = $cfg.find('pre'), $config_adv = $adv.find('pre'),
  137. config_file_list = [ boards_file, config_file, config_adv_file ],
  138. config_list = [ $config, $config_adv ],
  139. define_info = {}, // info for all defines, by name
  140. define_list = [[],[]], // arrays with all define names
  141. define_occur = [{},{}], // lines where defines occur in each file
  142. define_groups = [{},{}], // similarly-named defines that group in the form
  143. define_section = {}, // the section of each define
  144. dependent_groups = {},
  145. boards_list = {},
  146. therms_list = {},
  147. total_config_lines,
  148. total_config_adv_lines,
  149. hover_timer,
  150. pulse_offset = 0;
  151. // Return this anonymous object as configuratorApp
  152. return {
  153. my_public_var: 4,
  154. logging: 1,
  155. init: function() {
  156. self = this; // a 'this' for use when 'this' is something else
  157. // Set up the form, creating fields and fieldsets as-needed
  158. this.initConfigForm();
  159. // Make tabs for all the fieldsets
  160. this.makeTabsForFieldsets();
  161. // No selection on errors
  162. // $msgbox.noSelect();
  163. // Make a droppable file uploader, if possible
  164. var $uploader = $('#file-upload');
  165. var fileUploader = new BinaryFileUploader({
  166. element: $uploader[0],
  167. onFileLoad: function(file) { self.handleFileLoad(file, $uploader); }
  168. });
  169. if (!fileUploader.hasFileUploaderSupport())
  170. this.setMessage("Your browser doesn't support the file reading API.", 'error');
  171. // Make the disclosure items work
  172. $('.disclose').click(function(){
  173. var $dis = $(this), $pre = $dis.nextAll('pre:first');
  174. var didAnim = function() {$dis.toggleClass('closed almost');};
  175. $dis.addClass('almost').hasClass('closed')
  176. ? $pre.slideDown(200, didAnim)
  177. : $pre.slideUp(200, didAnim);
  178. });
  179. // Adjust the form layout for the window size
  180. $(window).bind('scroll resize', this.adjustFormLayout).trigger('resize');
  181. // Read boards.h, Configuration.h, Configuration_adv.h
  182. var ajax_count = 0, success_count = 0;
  183. var loaded_items = {};
  184. var isGithub = config.type == 'github';
  185. var rateLimit = 0;
  186. $.each(config_file_list, function(i,fname){
  187. var url = config_path(fname);
  188. $.ajax({
  189. url: url,
  190. type: 'GET',
  191. dataType: isGithub ? 'jsonp' : undefined,
  192. async: true,
  193. cache: false,
  194. error: function(req, stat, err) {
  195. self.log(req, 1);
  196. if (req.status == 200) {
  197. if (typeof req.responseText === 'string') {
  198. var txt = req.responseText;
  199. loaded_items[fname] = function(){ self.fileLoaded(fname, txt, true); };
  200. success_count++;
  201. // self.setMessage('The request for "'+fname+'" may be malformed.', 'error');
  202. }
  203. }
  204. else {
  205. self.setRequestError(req.status ? req.status : '(Access-Control-Allow-Origin?)', url);
  206. }
  207. },
  208. success: function(txt) {
  209. if (isGithub && typeof txt.meta.status !== undefined && txt.meta.status != 200) {
  210. self.setRequestError(txt.meta.status, url);
  211. }
  212. else {
  213. // self.log(txt, 1);
  214. if (isGithub) {
  215. rateLimit = {
  216. quota: 1 * txt.meta['X-RateLimit-Remaining'],
  217. timeLeft: Math.floor(txt.meta['X-RateLimit-Reset'] - Date.now()/1000),
  218. };
  219. }
  220. loaded_items[fname] = function(){ self.fileLoaded(fname, isGithub ? decodeURIComponent(escape(atob(txt.data.content))) : txt, true); };
  221. success_count++;
  222. }
  223. },
  224. complete: function() {
  225. ajax_count++;
  226. if (ajax_count >= config_file_list.length) {
  227. // If not all files loaded set an error
  228. if (success_count < ajax_count)
  229. self.setMessage('Unable to load configurations. Try the upload field.', 'error');
  230. // Is the request near the rate limit? Set an error.
  231. var r;
  232. if (r = rateLimit) {
  233. if (r.quota < 20) {
  234. self.setMessage(
  235. 'Approaching request limit (' +
  236. r.quota + ' remaining.' +
  237. ' Reset in ' + Math.floor(r.timeLeft/60) + ':' + (r.timeLeft%60+'').zeroPad(2) + ')',
  238. 'warning'
  239. );
  240. }
  241. }
  242. // Post-process all the loaded files
  243. $.each(config_file_list, function(){ if (loaded_items[this]) loaded_items[this](); });
  244. }
  245. }
  246. });
  247. });
  248. },
  249. /**
  250. * Make a download link visible and active
  251. */
  252. activateDownloadLink: function(cindex) {
  253. var filename = config_file_list[cindex+1];
  254. var $c = config_list[cindex], txt = $c.text();
  255. $c.prevAll('.download:first')
  256. .unbind('mouseover click')
  257. .mouseover(function() {
  258. var d = new Date(), fn = d.fileStamp(filename);
  259. $(this).attr({ download:fn, href:'download:'+fn, title:'download:'+fn });
  260. })
  261. .click(function(){
  262. var $button = $(this);
  263. $(this).attr({ href:'data:text/plain;charset=utf-8,' + encodeURIComponent($c.text()) });
  264. setTimeout(function(){
  265. $button.attr({ href:$button.attr('title') });
  266. }, 100);
  267. return true;
  268. })
  269. .css({visibility:'visible'});
  270. },
  271. /**
  272. * Make the download-all link visible and active
  273. */
  274. activateDownloadAllLink: function() {
  275. $('.download-all')
  276. .unbind('mouseover click')
  277. .mouseover(function() {
  278. var d = new Date(), fn = d.fileStamp('MarlinConfig.zip');
  279. $(this).attr({ download:fn, href:'download:'+fn, title:'download:'+fn });
  280. })
  281. .click(function(){
  282. var $button = $(this);
  283. var zip = new JSZip();
  284. for (var e in [0,1]) zip.file(config_file_list[e+1], config_list[e].text());
  285. var zipped = zip.generate({type:'blob'});
  286. saveAs(zipped, $button.attr('download'));
  287. return false;
  288. })
  289. .css({visibility:'visible'});
  290. },
  291. /**
  292. * Init the boards array from a boards.h file
  293. */
  294. initBoardsFromText: function(txt) {
  295. boards_list = {};
  296. var r, findDef = new RegExp('[ \\t]*#define[ \\t]+(BOARD_[\\w_]+)[ \\t]+(\\d+)[ \\t]*(//[ \\t]*)?(.+)?', 'gm');
  297. while((r = findDef.exec(txt)) !== null) {
  298. boards_list[r[1]] = r[2].prePad(3, '  ') + " — " + r[4].replace(/\).*/, ')');
  299. }
  300. this.log("Loaded boards:\n" + Object.keys(boards_list).join('\n'), 3);
  301. has_boards = true;
  302. },
  303. /**
  304. * Init the thermistors array from the Configuration.h file
  305. */
  306. initThermistorList: function(txt) {
  307. // Get all the thermistors and save them into an object
  308. var r, s, findDef = new RegExp('(//.*\n)+\\s+(#define[ \\t]+TEMP_SENSOR_0)', 'g');
  309. r = findDef.exec(txt);
  310. findDef = new RegExp('^//[ \\t]*([-\\d]+)[ \\t]+is[ \\t]+(.*)[ \\t]*$', 'gm');
  311. while((s = findDef.exec(r[0])) !== null) {
  312. therms_list[s[1]] = s[1].prePad(4, '  ') + " — " + s[2];
  313. }
  314. },
  315. /**
  316. * Get all the unique define names, building lists that will be used
  317. * when gathering info about each define.
  318. *
  319. * define_list[c][j] : Define names in each config (in order of occurrence)
  320. * define_section[name] : Section where define should appear in the form
  321. * define_occur[c][name][i] : Lines in each config where the same define occurs
  322. * .cindex Config file index
  323. * .lineNum Line number of the occurrence
  324. * .line The occurrence line
  325. */
  326. initDefineList: function(cindex) {
  327. var section = 'hidden',
  328. leave_out_defines = ['CONFIGURATION_H', 'CONFIGURATION_ADV_H'],
  329. define_sect = {},
  330. occ_list = {},
  331. txt = config_list[cindex].text(),
  332. r, findDef = new RegExp('^.*(@section|#define)[ \\t]+(\\w+).*$', 'gm');
  333. while((r = findDef.exec(txt)) !== null) {
  334. var name = r[2];
  335. if (r[1] == '@section') {
  336. section = name;
  337. }
  338. else if ($.inArray(name, leave_out_defines) < 0) {
  339. var lineNum = txt.lineCount(r.index),
  340. inst = { cindex:cindex, lineNum:lineNum, line:r[0] },
  341. in_sect = (name in define_sect);
  342. if (!in_sect) {
  343. occ_list[name] = [ inst ];
  344. }
  345. if (!(name in define_section) && !in_sect) {
  346. define_sect[name] = section; // new first-time define
  347. }
  348. else {
  349. occ_list[name].push(inst);
  350. }
  351. }
  352. }
  353. define_list[cindex] = Object.keys(define_sect);
  354. define_occur[cindex] = occ_list;
  355. $.extend(define_section, define_sect);
  356. this.log(define_list[cindex], 2);
  357. this.log(occ_list, 2);
  358. this.log(define_sect, 2);
  359. },
  360. /**
  361. * Find the defines in one of the configs that are just variants.
  362. * Group them together for form-building and other uses.
  363. *
  364. * define_groups[c][name]
  365. * .pattern regexp matching items in the group
  366. * .title title substitution
  367. * .count number of items in the group
  368. */
  369. refreshDefineGroups: function(cindex) {
  370. var findDef = /^(|.*_)(([XYZE](MAX|MIN))|(E[0-3]|[XYZE01234])|MAX|MIN|(bed)?K[pid]|HOTEND|HPB|JAPAN|WESTERN|CYRILLIC|LEFT|RIGHT|BACK|FRONT|[XYZ]_POINT)(_.*|)$/i;
  371. var match_prev, patt, title, nameList, groups = {}, match_section;
  372. $.each(define_list[cindex], function(i, name) {
  373. if (match_prev) {
  374. if (match_prev.exec(name) && define_section[name] == match_section) {
  375. nameList.push(name);
  376. }
  377. else {
  378. if (nameList.length > 1) {
  379. $.each(nameList, function(i,n){
  380. groups[n] = {
  381. pattern: patt,
  382. title: title,
  383. count: nameList.length
  384. };
  385. });
  386. }
  387. match_prev = null;
  388. }
  389. }
  390. if (!match_prev) {
  391. var r = findDef.exec(name);
  392. if (r != null) {
  393. title = '';
  394. switch(r[2].toUpperCase()) {
  395. case '0':
  396. patt = '([0123])';
  397. title = 'N';
  398. break;
  399. case 'X':
  400. patt = '([XYZE])';
  401. title = 'AXIS';
  402. break;
  403. case 'E0':
  404. patt = 'E([0-3])';
  405. title = 'E';
  406. break;
  407. case 'BEDKP':
  408. patt = 'bed(K[pid])';
  409. title = 'BED_PID';
  410. break;
  411. case 'KP':
  412. patt = '(K[pid])';
  413. title = 'PID';
  414. break;
  415. case 'LEFT':
  416. case 'RIGHT':
  417. case 'BACK':
  418. case 'FRONT':
  419. patt = '([LRBF])(EFT|IGHT|ACK|RONT)';
  420. break;
  421. case 'MAX':
  422. case 'MIN':
  423. patt = '(MAX|MIN)';
  424. break;
  425. case 'HOTEND':
  426. case 'HPB':
  427. patt = '(HOTEND|HPB)';
  428. break;
  429. case 'JAPAN':
  430. case 'WESTERN':
  431. case 'CYRILLIC':
  432. patt = '(JAPAN|WESTERN|CYRILLIC)';
  433. break;
  434. case 'XMIN':
  435. case 'XMAX':
  436. patt = '([XYZ])'+r[4];
  437. title = 'XYZ_'+r[4];
  438. break;
  439. default:
  440. patt = null;
  441. break;
  442. }
  443. if (patt) {
  444. patt = '^' + r[1] + patt + r[7] + '$';
  445. title = r[1] + title + r[7];
  446. match_prev = new RegExp(patt, 'i');
  447. match_section = define_section[name];
  448. nameList = [ name ];
  449. }
  450. }
  451. }
  452. });
  453. define_groups[cindex] = groups;
  454. this.log(define_groups[cindex], 2);
  455. },
  456. /**
  457. * Get all conditional blocks and their line ranges
  458. * and store them in the dependent_groups list.
  459. *
  460. * dependent_groups[condition][i]
  461. *
  462. * .cindex config file index
  463. * .start starting line
  464. * .end ending line
  465. *
  466. */
  467. initDependentGroups: function() {
  468. var findBlock = /^[ \t]*#(ifn?def|if|else|endif)[ \t]*(.*)([ \t]*\/\/[^\n]+)?$/gm,
  469. leave_out_defines = ['CONFIGURATION_H', 'CONFIGURATION_ADV_H'];
  470. dependent_groups = {};
  471. $.each(config_list, function(i, $v) {
  472. var ifStack = [];
  473. var r, txt = $v.text();
  474. while((r = findBlock.exec(txt)) !== null) {
  475. var lineNum = txt.lineCount(r.index);
  476. var code = r[2].replace(/[ \t]*\/\/.*$/, '');
  477. switch(r[1]) {
  478. case 'if':
  479. var code = code
  480. .replace(/([A-Z][A-Z0-9_]+)/g, 'self.defineValue("$1")')
  481. .replace(/defined[ \t]*\(?[ \t]*self.defineValue\(("[A-Z][A-Z0-9_]+")\)[ \t]*\)?/g, 'self.defineIsEnabled($1)');
  482. ifStack.push(['('+code+')', lineNum]); // #if starts on next line
  483. self.log("push if " + code, 4);
  484. break;
  485. case 'ifdef':
  486. if ($.inArray(code, leave_out_defines) < 0) {
  487. ifStack.push(['self.defineIsEnabled("' + code + '")', lineNum]);
  488. self.log("push ifdef " + code, 4);
  489. }
  490. else {
  491. ifStack.push(0);
  492. }
  493. break;
  494. case 'ifndef':
  495. if ($.inArray(code, leave_out_defines) < 0) {
  496. ifStack.push(['!self.defineIsEnabled("' + code + '")', lineNum]);
  497. self.log("push ifndef " + code, 4);
  498. }
  499. else {
  500. ifStack.push(0);
  501. }
  502. break;
  503. case 'else':
  504. case 'endif':
  505. var c = ifStack.pop();
  506. if (c) {
  507. var cond = c[0], line = c[1];
  508. self.log("pop " + c[0], 4);
  509. if (dependent_groups[cond] === undefined) dependent_groups[cond] = [];
  510. dependent_groups[cond].push({cindex:i,start:line,end:lineNum});
  511. if (r[1] == 'else') {
  512. // Reverse the condition
  513. cond = (cond.indexOf('!') === 0) ? cond.substr(1) : ('!'+cond);
  514. ifStack.push([cond, lineNum]);
  515. self.log("push " + cond, 4);
  516. }
  517. }
  518. else {
  519. if (r[1] == 'else') ifStack.push(0);
  520. }
  521. break;
  522. }
  523. }
  524. }); // text blobs loop
  525. },
  526. /**
  527. * Init all the defineInfo structures after reload
  528. * The "enabled" field may need an update for newly-loaded dependencies
  529. */
  530. initDefineInfo: function() {
  531. $.each(define_list, function(e,def_list){
  532. $.each(def_list, function(i, name) {
  533. define_info[name] = self.getDefineInfo(name, e);
  534. });
  535. });
  536. },
  537. /**
  538. * Create fields for defines in a config that have none
  539. * Use define_groups data to group fields together
  540. */
  541. createFieldsForDefines: function(cindex) {
  542. // var n = 0;
  543. var grouping = false, group = define_groups[cindex],
  544. g_pattern, g_regex, g_subitem, g_section, g_class,
  545. fail_list = [];
  546. $.each(define_list[cindex], function(i, name) {
  547. var section = define_section[name];
  548. if (section != 'hidden' && !$('#'+name).length) {
  549. var inf = define_info[name];
  550. if (inf) {
  551. var label_text = name, sublabel;
  552. // Is this field in a sequence?
  553. // Then see if it's the second or after
  554. if (grouping) {
  555. if (name in group && g_pattern == group[name].pattern && g_section == section) {
  556. g_subitem = true;
  557. sublabel = g_regex.exec(name)[1];
  558. }
  559. else
  560. grouping = false;
  561. }
  562. // Start grouping?
  563. if (!grouping && name in group) {
  564. grouping = true;
  565. g_subitem = false;
  566. var grp = group[name];
  567. g_section = section;
  568. g_class = 'one_of_' + grp.count;
  569. g_pattern = grp.pattern;
  570. g_regex = new RegExp(g_pattern, 'i');
  571. label_text = grp.title;
  572. sublabel = g_regex.exec(name)[1];
  573. }
  574. var $ff = $('#'+section), $newfield,
  575. avail = eval(inf.enabled);
  576. if (!(grouping && g_subitem)) {
  577. var $newlabel = $('<label>',{for:name,class:'added'}).text(label_text.toLabel());
  578. $newlabel.unblock(avail);
  579. // if (!(++n % 3))
  580. $newlabel.addClass('newline');
  581. $ff.append($newlabel);
  582. }
  583. // Multiple fields?
  584. if (inf.type == 'list') {
  585. for (var i=0; i<inf.size; i++) {
  586. var fieldname = i > 0 ? name+'-'+i : name;
  587. $newfield = $('<input>',{type:'text',size:6,maxlength:10,id:fieldname,name:fieldname,class:'subitem added',disabled:!avail}).unblock(avail);
  588. if (grouping) $newfield.addClass(g_class);
  589. $ff.append($newfield);
  590. }
  591. }
  592. else {
  593. // Items with options, either toggle or select
  594. // TODO: Radio buttons for other values
  595. if (inf.options !== undefined) {
  596. if (inf.type == 'toggle') {
  597. $newfield = $('<input>',{type:'checkbox'});
  598. }
  599. else {
  600. // Otherwise selectable
  601. $newfield = $('<select>');
  602. }
  603. // ...Options added when field initialized
  604. }
  605. else {
  606. $newfield = inf.type == 'switch' ? $('<input>',{type:'checkbox'}) : $('<input>',{type:'text',size:10,maxlength:40});
  607. }
  608. $newfield.attr({id:name,name:name,class:'added',disabled:!avail}).unblock(avail);
  609. if (grouping) {
  610. $newfield.addClass(g_class);
  611. if (sublabel) {
  612. $ff.append($('<label>',{class:'added sublabel',for:name}).text(sublabel.toTitleCase()).unblock(avail));
  613. }
  614. }
  615. // Add the new field to the form
  616. $ff.append($newfield);
  617. }
  618. }
  619. else
  620. fail_list.push(name);
  621. }
  622. });
  623. if (fail_list.length) this.log('Unable to parse:\n' + fail_list.join('\n'), 2);
  624. },
  625. /**
  626. * Handle a file being dropped on the file field
  627. */
  628. handleFileLoad: function(txt, $uploader) {
  629. txt += '';
  630. var filename = $uploader.val().replace(/.*[\/\\](.*)$/, '$1');
  631. if ($.inArray(filename, config_file_list))
  632. this.fileLoaded(filename, txt);
  633. else
  634. this.setMessage("Can't parse '"+filename+"'!");
  635. },
  636. /**
  637. * Process a file after it's been successfully loaded
  638. */
  639. fileLoaded: function(filename, txt, wait) {
  640. this.log("fileLoaded:"+filename,4);
  641. var err, cindex;
  642. switch(filename) {
  643. case boards_file:
  644. this.initBoardsFromText(txt);
  645. $('#MOTHERBOARD').html('').addOptions(boards_list);
  646. if (has_config) this.initField('MOTHERBOARD');
  647. break;
  648. case config_file:
  649. if (has_boards) {
  650. $config.text(txt);
  651. total_config_lines = txt.lineCount();
  652. // this.initThermistorList(txt);
  653. if (!wait) cindex = 0;
  654. has_config = true;
  655. if (has_config_adv) {
  656. this.activateDownloadAllLink();
  657. if (wait) cindex = 2;
  658. }
  659. }
  660. else {
  661. err = boards_file;
  662. }
  663. break;
  664. case config_adv_file:
  665. if (has_config) {
  666. $config_adv.text(txt);
  667. total_config_adv_lines = txt.lineCount();
  668. if (!wait) cindex = 1;
  669. has_config_adv = true;
  670. if (has_config) {
  671. this.activateDownloadAllLink();
  672. if (wait) cindex = 2;
  673. }
  674. }
  675. else {
  676. err = config_file;
  677. }
  678. break;
  679. }
  680. // When a config file loads defines need update
  681. if (cindex != null) this.prepareConfigData(cindex);
  682. this.setMessage(err
  683. ? 'Please upload a "' + boards_file + '" file first!'
  684. : '"' + filename + '" loaded successfully.', err ? 'error' : 'message'
  685. );
  686. },
  687. prepareConfigData: function(cindex) {
  688. var inds = (cindex == 2) ? [ 0, 1 ] : [ cindex ];
  689. $.each(inds, function(i,e){
  690. // Purge old fields from the form, clear the define list
  691. self.purgeAddedFields(e);
  692. // Build the define_list
  693. self.initDefineList(e);
  694. // TODO: Find sequential names and group them
  695. // Allows related settings to occupy one line in the form
  696. self.refreshDefineGroups(e);
  697. });
  698. // Build the dependent defines list
  699. this.initDependentGroups(); // all config text
  700. // Get define_info for all known defines
  701. this.initDefineInfo(); // all config text
  702. $.each(inds, function(i,e){
  703. // Create new fields
  704. self.createFieldsForDefines(e); // create new fields
  705. // Init the fields, set values, etc
  706. self.refreshConfigForm(e);
  707. self.activateDownloadLink(e);
  708. });
  709. },
  710. /**
  711. * Add initial enhancements to the existing form
  712. */
  713. initConfigForm: function() {
  714. // Modify form fields and make the form responsive.
  715. // As values change on the form, we could update the
  716. // contents of text areas containing the configs, for
  717. // example.
  718. // while(!$config_adv.text() == null) {}
  719. // while(!$config.text() == null) {}
  720. // Go through all form items with names
  721. $form.find('[name]').each(function() {
  722. // Set its id to its name
  723. var name = $(this).attr('name');
  724. $(this).attr({id: name});
  725. // Attach its label sibling
  726. var $label = $(this).prev('label');
  727. if ($label.length) $label.attr('for',name);
  728. });
  729. // Get all 'switchable' class items and add a checkbox
  730. // $form.find('.switchable').each(function(){
  731. // $(this).after(
  732. // $('<input>',{type:'checkbox',value:'1',class:'enabler added'})
  733. // .prop('checked',true)
  734. // .attr('id',this.id + '-switch')
  735. // .change(self.handleSwitch)
  736. // );
  737. // });
  738. // Add options to the popup menus
  739. // $('#SERIAL_PORT').addOptions([0,1,2,3,4,5,6,7]);
  740. // $('#BAUDRATE').addOptions([2400,9600,19200,38400,57600,115200,250000]);
  741. // $('#EXTRUDERS').addOptions([1,2,3,4]);
  742. // $('#POWER_SUPPLY').addOptions({'1':'ATX','2':'Xbox 360'});
  743. // Replace the Serial popup menu with a stepper control
  744. /*
  745. $('#serial_stepper').jstepper({
  746. min: 0,
  747. max: 3,
  748. val: $('#SERIAL_PORT').val(),
  749. arrowWidth: '18px',
  750. arrowHeight: '15px',
  751. color: '#FFF',
  752. acolor: '#F70',
  753. hcolor: '#FF0',
  754. id: 'select-me',
  755. textStyle: {width:'1.5em',fontSize:'120%',textAlign:'center'},
  756. onChange: function(v) { $('#SERIAL_PORT').val(v).trigger('change'); }
  757. });
  758. */
  759. },
  760. /**
  761. * Make tabs to switch between fieldsets
  762. */
  763. makeTabsForFieldsets: function() {
  764. // Make tabs for the fieldsets
  765. var $fset = $form.find('fieldset'),
  766. $tabs = $('<ul>',{class:'tabs'}),
  767. ind = 1;
  768. $fset.each(function(){
  769. var tabID = 'TAB'+ind;
  770. $(this).addClass(tabID);
  771. var $leg = $(this).find('legend');
  772. var $link = $('<a>',{href:'#'+ind,id:tabID}).text($leg.text());
  773. $tabs.append($('<li>').append($link));
  774. $link.click(function(e){
  775. e.preventDefault;
  776. var ind = this.id;
  777. $tabs.find('.active').removeClass('active');
  778. $(this).addClass('active');
  779. $fset.hide();
  780. $fset.filter('.'+this.id).show();
  781. return false;
  782. });
  783. ind++;
  784. });
  785. $('#tabs').html('').append($tabs);
  786. $('<br>',{class:'clear'}).appendTo('#tabs');
  787. $tabs.find('a:first').trigger('click');
  788. },
  789. /**
  790. * Update all fields on the form after loading a configuration
  791. */
  792. refreshConfigForm: function(cindex) {
  793. /**
  794. * Any manually-created form elements will remain
  795. * where they are. Unknown defines (currently most)
  796. * are added to tabs based on section
  797. *
  798. * Specific exceptions can be managed by applying
  799. * classes to the associated form fields.
  800. * Sorting and arrangement can come from an included
  801. * Javascript file that describes the configuration
  802. * in JSON, or using information added to the config
  803. * files.
  804. *
  805. */
  806. // Refresh the motherboard menu with new options
  807. $('#MOTHERBOARD').html('').addOptions(boards_list);
  808. // Init all existing fields, getting define info for those that need it
  809. // refreshing the options and updating their current values
  810. $.each(define_list[cindex], function(i, name) {
  811. if ($('#'+name).length) {
  812. self.initField(name);
  813. self.initFieldValue(name);
  814. }
  815. else
  816. self.log(name + " is not on the page yet.", 2);
  817. });
  818. // Set enabled state based on dependencies
  819. // this.enableForDependentConditions();
  820. },
  821. /**
  822. * Enable / disable fields in dependent groups
  823. * based on their dependencies.
  824. */
  825. refreshDependentFields: function() {
  826. $.each(define_list, function(e,def_list){
  827. $.each(def_list, function(i, name) {
  828. var inf = define_info[name];
  829. if (inf && inf.enabled != 'true') {
  830. var $elm = $('#'+name), ena = eval(inf.enabled);
  831. var isEnabled = (inf.type == 'switch') || self.defineIsEnabled(name);
  832. $('#'+name+'-switch').attr('disabled', !ena);
  833. $elm.attr('disabled', !(ena && isEnabled)).unblock(ena);
  834. $('label[for="'+name+'"]').unblock(ena);
  835. }
  836. });
  837. });
  838. },
  839. /**
  840. * Make a field responsive, tooltip its label(s), add enabler if needed
  841. */
  842. initField: function(name) {
  843. this.log("initField:"+name,4);
  844. var $elm = $('#'+name), inf = define_info[name];
  845. $elm[0].defineInfo = inf;
  846. // Create a tooltip on the label if there is one
  847. if (inf.tooltip) {
  848. // label for the item
  849. var $tipme = $('label[for="'+name+'"]');
  850. if ($tipme.length) {
  851. $tipme.unbind('mouseenter mouseleave');
  852. $tipme.hover(
  853. function() {
  854. if ($('#tipson input').prop('checked')) {
  855. var pos = $tipme.position(), px = $tipme.width()/2;
  856. $tooltip.html(inf.tooltip)
  857. .append('<span>')
  858. .css({bottom:($tooltip.parent().outerHeight()-pos.top+10)+'px',left:(pos.left+px)+'px'})
  859. .show();
  860. if (hover_timer) {
  861. clearTimeout(hover_timer);
  862. hover_timer = null;
  863. }
  864. }
  865. },
  866. function() {
  867. hover_timer = setTimeout(function(){
  868. hover_timer = null;
  869. $tooltip.fadeOut(400);
  870. }, 400);
  871. }
  872. );
  873. }
  874. }
  875. // Make the element(s) respond to events
  876. if (inf.type == 'list') {
  877. // Multiple fields need to respond
  878. for (var i=0; i<inf.size; i++) {
  879. if (i > 0) $elm = $('#'+name+'-'+i);
  880. $elm.unbind('input');
  881. $elm.on('input', this.handleChange);
  882. }
  883. }
  884. else {
  885. var elmtype = $elm.attr('type');
  886. // Set options on single fields if there are any
  887. if (inf.options !== undefined && elmtype === undefined)
  888. $elm.html('').addOptions(inf.options);
  889. $elm.unbind('input change');
  890. $elm.on(elmtype == 'text' ? 'input' : 'change', this.handleChange);
  891. }
  892. // Add an enabler checkbox if it needs one
  893. if (inf.switchable && $('#'+name+'-switch').length == 0) {
  894. // $elm = the last element added
  895. $elm.after(
  896. $('<input>',{type:'checkbox',value:'1',class:'enabler added'})
  897. .prop('checked',self.defineIsEnabled(name))
  898. .attr({id: name+'-switch'})
  899. .change(self.handleSwitch)
  900. );
  901. }
  902. },
  903. /**
  904. * Handle any value field being changed
  905. * this = the field
  906. */
  907. handleChange: function() {
  908. self.updateDefineFromField(this.id);
  909. self.refreshDependentFields();
  910. },
  911. /**
  912. * Handle a switch checkbox being changed
  913. * this = the switch checkbox
  914. */
  915. handleSwitch: function() {
  916. var $elm = $(this),
  917. name = $elm[0].id.replace(/-.+/,''),
  918. inf = define_info[name],
  919. on = $elm.prop('checked') || false;
  920. self.setDefineEnabled(name, on);
  921. if (inf.type == 'list') {
  922. // Multiple fields?
  923. for (var i=0; i<inf.size; i++) {
  924. $('#'+name+(i?'-'+i:'')).attr('disabled', !on);
  925. }
  926. }
  927. else {
  928. $elm.prev().attr('disabled', !on);
  929. }
  930. },
  931. /**
  932. * Get the current value of a #define
  933. */
  934. defineValue: function(name) {
  935. this.log('defineValue:'+name,4);
  936. var inf = define_info[name];
  937. if (inf == null) return 'n/a';
  938. var r = inf.regex.exec(inf.line), val = r[inf.val_i];
  939. this.log(r,2);
  940. return (inf.type == 'switch') ? (val === undefined || val.trim() != '//') : val;
  941. },
  942. /**
  943. * Get the current enabled state of a #define
  944. */
  945. defineIsEnabled: function(name) {
  946. this.log('defineIsEnabled:'+name,4);
  947. var inf = define_info[name];
  948. if (inf == null) return false;
  949. var r = inf.regex.exec(inf.line);
  950. this.log(r,2);
  951. var on = r[1] != null ? r[1].trim() != '//' : true;
  952. this.log(name + ' = ' + on, 2);
  953. return on;
  954. },
  955. /**
  956. * Set a #define enabled or disabled by altering the config text
  957. */
  958. setDefineEnabled: function(name, val) {
  959. this.log('setDefineEnabled:'+name,4);
  960. var inf = define_info[name];
  961. if (inf) {
  962. var slash = val ? '' : '//';
  963. var newline = inf.line
  964. .replace(/^([ \t]*)(\/\/)([ \t]*)/, '$1$3') // remove slashes
  965. .replace(inf.pre+inf.define, inf.pre+slash+inf.define); // add them back
  966. this.setDefineLine(name, newline);
  967. }
  968. },
  969. /**
  970. * Update a #define (from the form) by altering the config text
  971. */
  972. updateDefineFromField: function(name) {
  973. this.log('updateDefineFromField:'+name,4);
  974. // Drop the suffix on sub-fields
  975. name = name.replace(/-\d+$/, '');
  976. var $elm = $('#'+name), inf = define_info[name];
  977. if (inf == null) return;
  978. var isCheck = $elm.attr('type') == 'checkbox',
  979. val = isCheck ? $elm.prop('checked') : $elm.val().trim();
  980. var newline;
  981. switch(inf.type) {
  982. case 'switch':
  983. var slash = val ? '' : '//';
  984. newline = inf.line.replace(inf.repl, '$1'+slash+'$3');
  985. break;
  986. case 'list':
  987. case 'quoted':
  988. case 'plain':
  989. if (isCheck) this.setMessage(name + ' should not be a checkbox!', 'error');
  990. case 'toggle':
  991. if (isCheck) {
  992. val = val ? inf.options[1] : inf.options[0];
  993. }
  994. else {
  995. if (inf.type == 'list')
  996. for (var i=1; i<inf.size; i++) val += ', ' + $('#'+name+'-'+i).val().trim();
  997. }
  998. newline = inf.line.replace(inf.repl, '$1'+(''+val).replace('$','\\$')+'$3');
  999. break;
  1000. }
  1001. this.setDefineLine(name, newline);
  1002. },
  1003. /**
  1004. * Set the define's line in the text to a new line,
  1005. * then update, highlight, and scroll to the line
  1006. */
  1007. setDefineLine: function(name, newline) {
  1008. this.log('setDefineLine:'+name+'\n'+newline,4);
  1009. var inf = define_info[name];
  1010. var $c = $(inf.field), txt = $c.text();
  1011. var hilite_token = '[HIGHLIGHTER-TOKEN]';
  1012. txt = txt.replaceLine(inf.lineNum, hilite_token + newline); // for override line and lineNum would be changed
  1013. inf.line = newline;
  1014. // Convert txt into HTML before storing
  1015. var html = txt.toHTML().replace(hilite_token, '<span></span>');
  1016. // Set the final text including the highlighter
  1017. $c.html(html);
  1018. // Scroll to reveal the define
  1019. if ($c.is(':visible')) this.scrollToDefine(name);
  1020. },
  1021. /**
  1022. * Scroll a pre box to reveal a #define
  1023. */
  1024. scrollToDefine: function(name, always) {
  1025. this.log('scrollToDefine:'+name,4);
  1026. var inf = define_info[name], $c = $(inf.field);
  1027. // Scroll to the altered text if it isn't visible
  1028. var halfHeight = $c.height()/2, scrollHeight = $c.prop('scrollHeight'),
  1029. lineHeight = scrollHeight/[total_config_lines, total_config_adv_lines][inf.cindex],
  1030. textScrollY = (inf.lineNum * lineHeight - halfHeight).limit(0, scrollHeight - 1);
  1031. if (always || Math.abs($c.prop('scrollTop') - textScrollY) > halfHeight) {
  1032. $c.find('span').height(lineHeight);
  1033. $c.animate({ scrollTop: textScrollY });
  1034. }
  1035. },
  1036. /**
  1037. * Set a form field to the current #define value in the config text
  1038. */
  1039. initFieldValue: function(name) {
  1040. var $elm = $('#'+name), inf = define_info[name],
  1041. val = this.defineValue(name);
  1042. this.log('initFieldValue:' + name + ' to ' + val, 2);
  1043. // If the item is switchable then set enabled state too
  1044. var $cb = $('#'+name+'-switch'), avail = eval(inf.enabled), on = true;
  1045. if ($cb.length) {
  1046. on = self.defineIsEnabled(name);
  1047. $cb.prop('checked', on);
  1048. }
  1049. if (inf.type == 'list') {
  1050. $.each(val.split(','),function(i,v){
  1051. var $e = i > 0 ? $('#'+name+'-'+i) : $elm;
  1052. $e.val(v.trim());
  1053. $e.attr('disabled', !(on && avail)).unblock(avail);
  1054. });
  1055. }
  1056. else {
  1057. if (inf.type == 'toggle') val = val == inf.options[1];
  1058. $elm.attr('type') == 'checkbox' ? $elm.prop('checked', val) : $elm.val(''+val);
  1059. $elm.attr('disabled', !(on && avail)).unblock(avail); // enable/disable the form field (could also dim it)
  1060. }
  1061. $('label[for="'+name+'"]').unblock(avail);
  1062. },
  1063. /**
  1064. * Purge added fields and all their define info
  1065. */
  1066. purgeAddedFields: function(cindex) {
  1067. $.each(define_list[cindex], function(i, name){
  1068. $('#'+name + ",[id^='"+name+"-'],label[for='"+name+"']").filter('.added').remove();
  1069. });
  1070. define_list[cindex] = [];
  1071. },
  1072. /**
  1073. * Get information about a #define from configuration file text:
  1074. *
  1075. * - Pre-examine the #define for its prefix, value position, suffix, etc.
  1076. * - Construct RegExp's for the #define to find and replace values.
  1077. * - Store the existing #define line as a fast key to finding it later.
  1078. * - Determine the line number of the #define
  1079. * - Gather nearby comments to be used as tooltips.
  1080. * - Look for JSON in nearby comments for use as select options.
  1081. *
  1082. * define_info[name]
  1083. * .type type of define: switch, list, quoted, plain, or toggle
  1084. * .size the number of items in a "list" type
  1085. * .options select options, if any
  1086. * .cindex config index
  1087. * .field pre containing the config text (config_list[cindex][0])
  1088. * .line the full line from the config text
  1089. * .pre any text preceding #define
  1090. * .define the "#define NAME" text (may have leading spaces)
  1091. * .post the text following the "#define NAME val" part
  1092. * .regex regexp to get the value from the line
  1093. * .repl regexp to replace the value in the line
  1094. * .val_i the value's index in the .regex result
  1095. */
  1096. getDefineInfo: function(name, cindex) {
  1097. if (cindex === undefined) cindex = 0;
  1098. this.log('getDefineInfo:'+name,4);
  1099. var $c = config_list[cindex], txt = $c.text(),
  1100. info = { type:0, cindex:cindex, field:$c[0], val_i:2 }, post;
  1101. // a switch line with no value
  1102. var find = new RegExp('^([ \\t]*//)?([ \\t]*#define[ \\t]+' + name + ')([ \\t]*(/[*/].*)?)$', 'm'),
  1103. r = find.exec(txt);
  1104. if (r !== null) {
  1105. post = r[3] == null ? '' : r[3];
  1106. $.extend(info, {
  1107. type: 'switch',
  1108. val_i: 1,
  1109. regex: new RegExp('([ \\t]*//)?([ \\t]*' + r[2].regEsc() + post.regEsc() + ')', 'm'),
  1110. repl: new RegExp('([ \\t]*)(\/\/)?([ \\t]*' + r[2].regEsc() + post.regEsc() + ')', 'm')
  1111. });
  1112. }
  1113. else {
  1114. // a define with curly braces
  1115. find = new RegExp('^(.*//)?(.*#define[ \\t]+' + name + '[ \\t]+)(\{[^\}]*\})([ \\t]*(/[*/].*)?)$', 'm');
  1116. r = find.exec(txt);
  1117. if (r !== null) {
  1118. post = r[4] == null ? '' : r[4];
  1119. $.extend(info, {
  1120. type: 'list',
  1121. size: r[3].split(',').length,
  1122. regex: new RegExp('([ \\t]*//)?[ \\t]*' + r[2].regEsc() + '\{([^\}]*)\}' + post.regEsc(), 'm'),
  1123. repl: new RegExp('(([ \\t]*//)?[ \\t]*' + r[2].regEsc() + '\{)[^\}]*(\}' + post.regEsc() + ')', 'm')
  1124. });
  1125. }
  1126. else {
  1127. // a define with quotes
  1128. find = new RegExp('^(.*//)?(.*#define[ \\t]+' + name + '[ \\t]+)("[^"]*")([ \\t]*(/[*/].*)?)$', 'm');
  1129. r = find.exec(txt);
  1130. if (r !== null) {
  1131. post = r[4] == null ? '' : r[4];
  1132. $.extend(info, {
  1133. type: 'quoted',
  1134. regex: new RegExp('([ \\t]*//)?[ \\t]*' + r[2].regEsc() + '"([^"]*)"' + post.regEsc(), 'm'),
  1135. repl: new RegExp('(([ \\t]*//)?[ \\t]*' + r[2].regEsc() + '")[^"]*("' + post.regEsc() + ')', 'm')
  1136. });
  1137. }
  1138. else {
  1139. // a define with no quotes
  1140. find = new RegExp('^([ \\t]*//)?([ \\t]*#define[ \\t]+' + name + '[ \\t]+)(\\S*)([ \\t]*(/[*/].*)?)$', 'm');
  1141. r = find.exec(txt);
  1142. if (r !== null) {
  1143. post = r[4] == null ? '' : r[4];
  1144. $.extend(info, {
  1145. type: 'plain',
  1146. regex: new RegExp('([ \\t]*//)?[ \\t]*' + r[2].regEsc() + '(\\S*)' + post.regEsc(), 'm'),
  1147. repl: new RegExp('(([ \\t]*//)?[ \\t]*' + r[2].regEsc() + ')\\S*(' + post.regEsc() + ')', 'm')
  1148. });
  1149. if (r[3].match(/false|true/)) {
  1150. info.type = 'toggle';
  1151. info.options = ['false','true'];
  1152. }
  1153. }
  1154. }
  1155. }
  1156. }
  1157. // Success?
  1158. if (info.type) {
  1159. $.extend(info, {
  1160. line: r[0],
  1161. pre: r[1] == null ? '' : r[1].replace('//',''),
  1162. define: r[2],
  1163. post: post
  1164. });
  1165. // Get the end-of-line comment, if there is one
  1166. var tooltip = '', eoltip = '';
  1167. find = new RegExp('.*#define[ \\t].*/[/*]+[ \\t]*(.*)');
  1168. if (info.line.search(find) >= 0)
  1169. eoltip = tooltip = info.line.replace(find, '$1');
  1170. // Get all the comments immediately before the item, also include #define lines preceding it
  1171. var s;
  1172. // find = new RegExp('(([ \\t]*(//|#)[^\n]+\n){1,4})' + info.line.regEsc(), 'g');
  1173. find = new RegExp('(([ \\t]*//+[^\n]+\n)+([ \\t]*(//)?#define[^\n]+\n)*)' + info.line.regEsc(), 'g');
  1174. if (r = find.exec(txt)) {
  1175. var temp = [], tips = [];
  1176. // Find each line in forward order, store in reverse
  1177. find = new RegExp('^[ \\t]*//+[ \\t]*(.*)[ \\t]*$', 'gm');
  1178. while((s = find.exec(r[1])) !== null) temp.unshift(s[1]);
  1179. this.log(name+":\n"+temp.join('\n'), 2);
  1180. // Go through the reversed lines and add comment lines on
  1181. $.each(temp, function(i,v) {
  1182. // @ annotation breaks the comment chain
  1183. if (v.match(/^[ \\t]*\/\/+[ \\t]*@/)) return false;
  1184. // A #define breaks the chain, after a good tip
  1185. if (v.match(/^[ \\t]*(\/\/+)?[ \\t]*#define/)) return (tips.length < 1);
  1186. // Skip unwanted lines
  1187. if (v.match(/^[ \\t]*(={5,}|#define[ \\t]+.*)/g)) return true;
  1188. tips.unshift(v);
  1189. });
  1190. // Build the final tooltip, extract embedded options
  1191. $.each(tips, function(i,tip) {
  1192. // if (tip.match(/^#define[ \\t]/) != null) tooltip = eoltip;
  1193. // JSON data? Save as select options
  1194. if (!info.options && tip.match(/:[\[{]/) != null) {
  1195. // TODO
  1196. // :[1-6] = value limits
  1197. var o; eval('o=' + tip.substr(1));
  1198. info.options = o;
  1199. if (Object.prototype.toString.call(o) == "[object Array]" && o.length == 2 && !eval(''+o[0]))
  1200. info.type = 'toggle';
  1201. }
  1202. else {
  1203. // Other lines added to the tooltip
  1204. tooltip += ' ' + tip + '\n';
  1205. }
  1206. });
  1207. // Add .tooltip and .lineNum properties to the info
  1208. find = new RegExp('^'+name); // Strip the name from the tooltip
  1209. var lineNum = this.getLineNumberOfText(info.line, txt);
  1210. // See if this define is enabled conditionally
  1211. var enable_cond = '';
  1212. $.each(dependent_groups, function(cond,dat){
  1213. $.each(dat, function(i,o){
  1214. if (o.cindex == cindex && lineNum > o.start && lineNum < o.end) {
  1215. if (enable_cond != '') enable_cond += ' && ';
  1216. enable_cond += '(' + cond + ')';
  1217. }
  1218. });
  1219. });
  1220. $.extend(info, {
  1221. tooltip: '<strong>'+name+'</strong> '+tooltip.trim().replace(find,'').toHTML(),
  1222. lineNum: lineNum,
  1223. switchable: (info.type != 'switch' && info.line.match(/^[ \t]*\/\//)) || false, // Disabled? Mark as "switchable"
  1224. enabled: enable_cond ? enable_cond : 'true'
  1225. });
  1226. } // found comments
  1227. } // if info.type
  1228. else
  1229. info = null;
  1230. this.log(info, 2);
  1231. return info;
  1232. },
  1233. /**
  1234. * Count the number of lines before a match, return -1 on fail
  1235. */
  1236. getLineNumberOfText: function(line, txt) {
  1237. var pos = txt.indexOf(line);
  1238. return (pos < 0) ? pos : txt.lineCount(pos);
  1239. },
  1240. /**
  1241. * Add a temporary message to the page
  1242. */
  1243. setMessage: function(msg,type) {
  1244. if (msg) {
  1245. if (type === undefined) type = 'message';
  1246. var $err = $('<p class="'+type+'">'+msg+'<span>x</span></p>').appendTo($msgbox), err = $err[0];
  1247. var baseColor = $err.css('color').replace(/rgba?\(([^),]+,[^),]+,[^),]+).*/, 'rgba($1,');
  1248. err.pulse_offset = (pulse_offset += 200);
  1249. err.startTime = Date.now() + pulse_offset;
  1250. err.pulser = setInterval(function(){
  1251. var pulse_time = Date.now() + err.pulse_offset;
  1252. var opac = 0.5+Math.sin(pulse_time/200)*0.4;
  1253. $err.css({color:baseColor+(opac)+')'});
  1254. if (pulse_time - err.startTime > 2500 && opac > 0.899) {
  1255. clearInterval(err.pulser);
  1256. }
  1257. }, 50);
  1258. $err.click(function(e) {
  1259. $(this).remove();
  1260. self.adjustFormLayout();
  1261. return false;
  1262. }).css({cursor:'pointer'});
  1263. }
  1264. else {
  1265. $msgbox.find('p.error, p.warning').each(function() {
  1266. if (this.pulser !== undefined && this.pulser)
  1267. clearInterval(this.pulser);
  1268. $(this).remove();
  1269. });
  1270. }
  1271. self.adjustFormLayout();
  1272. },
  1273. adjustFormLayout: function() {
  1274. var wtop = $(window).scrollTop(),
  1275. ctop = $cfg.offset().top,
  1276. thresh = $form.offset().top+100;
  1277. if (ctop < thresh) {
  1278. var maxhi = $form.height(); // pad plus heights of config boxes can't be more than this
  1279. var pad = wtop > ctop ? wtop-ctop : 0; // pad the top box to stay in view
  1280. var innerpad = Math.ceil($cfg.height() - $cfg.find('pre').height());
  1281. // height to use for the inner boxes
  1282. var hi = ($(window).height() - ($cfg.offset().top - pad) + wtop - innerpad)/2;
  1283. if (hi < 200) hi = 200;
  1284. $cfg.css({ paddingTop: pad });
  1285. var $pre = $('pre.config');
  1286. $pre.css({ height: Math.floor(hi) - $pre.position().top });
  1287. }
  1288. else {
  1289. $cfg.css({ paddingTop: wtop > ctop ? wtop-ctop : 0, height: '' });
  1290. }
  1291. },
  1292. setRequestError: function(stat, path) {
  1293. self.setMessage('Error '+stat+' – ' + path.replace(/^(https:\/\/[^\/]+\/)?.+(\/[^\/]+)$/, '$1...$2'), 'error');
  1294. },
  1295. log: function(o,l) {
  1296. if (l === undefined) l = 0;
  1297. if (this.logging>=l*1) console.log(o);
  1298. },
  1299. logOnce: function(o) {
  1300. if (o.didLogThisObject === undefined) {
  1301. this.log(o);
  1302. o.didLogThisObject = true;
  1303. }
  1304. },
  1305. EOF: null
  1306. };
  1307. })();
  1308. // Typically the app would be in its own file, but this would be here
  1309. window.configuratorApp.init();
  1310. });