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 48KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414
  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: 'thinkyhead',
  29. repo: 'Marlin',
  30. ref: 'marlin_configurator',
  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|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. patt = '(JAPAN|WESTERN)';
  432. break;
  433. case 'XMIN':
  434. case 'XMAX':
  435. patt = '([XYZ])'+r[4];
  436. title = 'XYZ_'+r[4];
  437. break;
  438. default:
  439. patt = null;
  440. break;
  441. }
  442. if (patt) {
  443. patt = '^' + r[1] + patt + r[7] + '$';
  444. title = r[1] + title + r[7];
  445. match_prev = new RegExp(patt, 'i');
  446. match_section = define_section[name];
  447. nameList = [ name ];
  448. }
  449. }
  450. }
  451. });
  452. define_groups[cindex] = groups;
  453. this.log(define_groups[cindex], 2);
  454. },
  455. /**
  456. * Get all conditional blocks and their line ranges
  457. * and store them in the dependent_groups list.
  458. *
  459. * dependent_groups[condition][i]
  460. *
  461. * .cindex config file index
  462. * .start starting line
  463. * .end ending line
  464. *
  465. */
  466. initDependentGroups: function() {
  467. var findBlock = /^[ \t]*#(ifn?def|if|else|endif)[ \t]*(.*)([ \t]*\/\/[^\n]+)?$/gm,
  468. leave_out_defines = ['CONFIGURATION_H', 'CONFIGURATION_ADV_H'];
  469. dependent_groups = {};
  470. $.each(config_list, function(i, $v) {
  471. var ifStack = [];
  472. var r, txt = $v.text();
  473. while((r = findBlock.exec(txt)) !== null) {
  474. var lineNum = txt.lineCount(r.index);
  475. var code = r[2].replace(/[ \t]*\/\/.*$/, '');
  476. switch(r[1]) {
  477. case 'if':
  478. var code = code
  479. .replace(/([A-Z][A-Z0-9_]+)/g, 'self.defineValue("$1")')
  480. .replace(/defined[ \t]*\(?[ \t]*self.defineValue\(("[A-Z][A-Z0-9_]+")\)[ \t]*\)?/g, 'self.defineIsEnabled($1)');
  481. ifStack.push(['('+code+')', lineNum]); // #if starts on next line
  482. self.log("push if " + code, 4);
  483. break;
  484. case 'ifdef':
  485. if ($.inArray(code, leave_out_defines) < 0) {
  486. ifStack.push(['self.defineIsEnabled("' + code + '")', lineNum]);
  487. self.log("push ifdef " + code, 4);
  488. }
  489. else {
  490. ifStack.push(0);
  491. }
  492. break;
  493. case 'ifndef':
  494. if ($.inArray(code, leave_out_defines) < 0) {
  495. ifStack.push(['!self.defineIsEnabled("' + code + '")', lineNum]);
  496. self.log("push ifndef " + code, 4);
  497. }
  498. else {
  499. ifStack.push(0);
  500. }
  501. break;
  502. case 'else':
  503. case 'endif':
  504. var c = ifStack.pop();
  505. if (c) {
  506. var cond = c[0], line = c[1];
  507. self.log("pop " + c[0], 4);
  508. if (dependent_groups[cond] === undefined) dependent_groups[cond] = [];
  509. dependent_groups[cond].push({cindex:i,start:line,end:lineNum});
  510. if (r[1] == 'else') {
  511. // Reverse the condition
  512. cond = (cond.indexOf('!') === 0) ? cond.substr(1) : ('!'+cond);
  513. ifStack.push([cond, lineNum]);
  514. self.log("push " + cond, 4);
  515. }
  516. }
  517. else {
  518. if (r[1] == 'else') ifStack.push(0);
  519. }
  520. break;
  521. }
  522. }
  523. }); // text blobs loop
  524. },
  525. /**
  526. * Init all the defineInfo structures after reload
  527. * The "enabled" field may need an update for newly-loaded dependencies
  528. */
  529. initDefineInfo: function() {
  530. $.each(define_list, function(e,def_list){
  531. $.each(def_list, function(i, name) {
  532. define_info[name] = self.getDefineInfo(name, e);
  533. });
  534. });
  535. },
  536. /**
  537. * Create fields for defines in a config that have none
  538. * Use define_groups data to group fields together
  539. */
  540. createFieldsForDefines: function(cindex) {
  541. // var n = 0;
  542. var grouping = false, group = define_groups[cindex],
  543. g_pattern, g_regex, g_subitem, g_section, g_class,
  544. fail_list = [];
  545. $.each(define_list[cindex], function(i, name) {
  546. var section = define_section[name];
  547. if (section != 'hidden' && !$('#'+name).length) {
  548. var inf = define_info[name];
  549. if (inf) {
  550. var label_text = name, sublabel;
  551. // Is this field in a sequence?
  552. // Then see if it's the second or after
  553. if (grouping) {
  554. if (name in group && g_pattern == group[name].pattern && g_section == section) {
  555. g_subitem = true;
  556. sublabel = g_regex.exec(name)[1];
  557. }
  558. else
  559. grouping = false;
  560. }
  561. // Start grouping?
  562. if (!grouping && name in group) {
  563. grouping = true;
  564. g_subitem = false;
  565. var grp = group[name];
  566. g_section = section;
  567. g_class = 'one_of_' + grp.count;
  568. g_pattern = grp.pattern;
  569. g_regex = new RegExp(g_pattern, 'i');
  570. label_text = grp.title;
  571. sublabel = g_regex.exec(name)[1];
  572. }
  573. var $ff = $('#'+section), $newfield,
  574. avail = eval(inf.enabled);
  575. if (!(grouping && g_subitem)) {
  576. var $newlabel = $('<label>',{for:name,class:'added'}).text(label_text.toLabel());
  577. $newlabel.unblock(avail);
  578. // if (!(++n % 3))
  579. $newlabel.addClass('newline');
  580. $ff.append($newlabel);
  581. }
  582. // Multiple fields?
  583. if (inf.type == 'list') {
  584. for (var i=0; i<inf.size; i++) {
  585. var fieldname = i > 0 ? name+'-'+i : name;
  586. $newfield = $('<input>',{type:'text',size:6,maxlength:10,id:fieldname,name:fieldname,class:'subitem added',disabled:!avail}).unblock(avail);
  587. if (grouping) $newfield.addClass(g_class);
  588. $ff.append($newfield);
  589. }
  590. }
  591. else {
  592. // Items with options, either toggle or select
  593. // TODO: Radio buttons for other values
  594. if (inf.options !== undefined) {
  595. if (inf.type == 'toggle') {
  596. $newfield = $('<input>',{type:'checkbox'});
  597. }
  598. else {
  599. // Otherwise selectable
  600. $newfield = $('<select>');
  601. }
  602. // ...Options added when field initialized
  603. }
  604. else {
  605. $newfield = inf.type == 'switch' ? $('<input>',{type:'checkbox'}) : $('<input>',{type:'text',size:10,maxlength:40});
  606. }
  607. $newfield.attr({id:name,name:name,class:'added',disabled:!avail}).unblock(avail);
  608. if (grouping) {
  609. $newfield.addClass(g_class);
  610. if (sublabel) {
  611. $ff.append($('<label>',{class:'added sublabel',for:name}).text(sublabel.toTitleCase()).unblock(avail));
  612. }
  613. }
  614. // Add the new field to the form
  615. $ff.append($newfield);
  616. }
  617. }
  618. else
  619. fail_list.push(name);
  620. }
  621. });
  622. if (fail_list.length) this.log('Unable to parse:\n' + fail_list.join('\n'), 2);
  623. },
  624. /**
  625. * Handle a file being dropped on the file field
  626. */
  627. handleFileLoad: function(txt, $uploader) {
  628. txt += '';
  629. var filename = $uploader.val().replace(/.*[\/\\](.*)$/, '$1');
  630. if ($.inArray(filename, config_file_list))
  631. this.fileLoaded(filename, txt);
  632. else
  633. this.setMessage("Can't parse '"+filename+"'!");
  634. },
  635. /**
  636. * Process a file after it's been successfully loaded
  637. */
  638. fileLoaded: function(filename, txt, wait) {
  639. this.log("fileLoaded:"+filename,4);
  640. var err, cindex;
  641. switch(filename) {
  642. case boards_file:
  643. this.initBoardsFromText(txt);
  644. $('#MOTHERBOARD').html('').addOptions(boards_list);
  645. if (has_config) this.initField('MOTHERBOARD');
  646. break;
  647. case config_file:
  648. if (has_boards) {
  649. $config.text(txt);
  650. total_config_lines = txt.lineCount();
  651. // this.initThermistorList(txt);
  652. if (!wait) cindex = 0;
  653. has_config = true;
  654. if (has_config_adv) {
  655. this.activateDownloadAllLink();
  656. if (wait) cindex = 2;
  657. }
  658. }
  659. else {
  660. err = boards_file;
  661. }
  662. break;
  663. case config_adv_file:
  664. if (has_config) {
  665. $config_adv.text(txt);
  666. total_config_adv_lines = txt.lineCount();
  667. if (!wait) cindex = 1;
  668. has_config_adv = true;
  669. if (has_config) {
  670. this.activateDownloadAllLink();
  671. if (wait) cindex = 2;
  672. }
  673. }
  674. else {
  675. err = config_file;
  676. }
  677. break;
  678. }
  679. // When a config file loads defines need update
  680. if (cindex != null) this.prepareConfigData(cindex);
  681. this.setMessage(err
  682. ? 'Please upload a "' + boards_file + '" file first!'
  683. : '"' + filename + '" loaded successfully.', err ? 'error' : 'message'
  684. );
  685. },
  686. prepareConfigData: function(cindex) {
  687. var inds = (cindex == 2) ? [ 0, 1 ] : [ cindex ];
  688. $.each(inds, function(i,e){
  689. // Purge old fields from the form, clear the define list
  690. self.purgeAddedFields(e);
  691. // Build the define_list
  692. self.initDefineList(e);
  693. // TODO: Find sequential names and group them
  694. // Allows related settings to occupy one line in the form
  695. self.refreshDefineGroups(e);
  696. });
  697. // Build the dependent defines list
  698. this.initDependentGroups(); // all config text
  699. // Get define_info for all known defines
  700. this.initDefineInfo(); // all config text
  701. $.each(inds, function(i,e){
  702. // Create new fields
  703. self.createFieldsForDefines(e); // create new fields
  704. // Init the fields, set values, etc
  705. self.refreshConfigForm(e);
  706. self.activateDownloadLink(e);
  707. });
  708. },
  709. /**
  710. * Add initial enhancements to the existing form
  711. */
  712. initConfigForm: function() {
  713. // Modify form fields and make the form responsive.
  714. // As values change on the form, we could update the
  715. // contents of text areas containing the configs, for
  716. // example.
  717. // while(!$config_adv.text() == null) {}
  718. // while(!$config.text() == null) {}
  719. // Go through all form items with names
  720. $form.find('[name]').each(function() {
  721. // Set its id to its name
  722. var name = $(this).attr('name');
  723. $(this).attr({id: name});
  724. // Attach its label sibling
  725. var $label = $(this).prev('label');
  726. if ($label.length) $label.attr('for',name);
  727. });
  728. // Get all 'switchable' class items and add a checkbox
  729. // $form.find('.switchable').each(function(){
  730. // $(this).after(
  731. // $('<input>',{type:'checkbox',value:'1',class:'enabler added'})
  732. // .prop('checked',true)
  733. // .attr('id',this.id + '-switch')
  734. // .change(self.handleSwitch)
  735. // );
  736. // });
  737. // Add options to the popup menus
  738. // $('#SERIAL_PORT').addOptions([0,1,2,3,4,5,6,7]);
  739. // $('#BAUDRATE').addOptions([2400,9600,19200,38400,57600,115200,250000]);
  740. // $('#EXTRUDERS').addOptions([1,2,3,4]);
  741. // $('#POWER_SUPPLY').addOptions({'1':'ATX','2':'Xbox 360'});
  742. // Replace the Serial popup menu with a stepper control
  743. /*
  744. $('#serial_stepper').jstepper({
  745. min: 0,
  746. max: 3,
  747. val: $('#SERIAL_PORT').val(),
  748. arrowWidth: '18px',
  749. arrowHeight: '15px',
  750. color: '#FFF',
  751. acolor: '#F70',
  752. hcolor: '#FF0',
  753. id: 'select-me',
  754. textStyle: {width:'1.5em',fontSize:'120%',textAlign:'center'},
  755. onChange: function(v) { $('#SERIAL_PORT').val(v).trigger('change'); }
  756. });
  757. */
  758. },
  759. /**
  760. * Make tabs to switch between fieldsets
  761. */
  762. makeTabsForFieldsets: function() {
  763. // Make tabs for the fieldsets
  764. var $fset = $form.find('fieldset'),
  765. $tabs = $('<ul>',{class:'tabs'}),
  766. ind = 1;
  767. $fset.each(function(){
  768. var tabID = 'TAB'+ind;
  769. $(this).addClass(tabID);
  770. var $leg = $(this).find('legend');
  771. var $link = $('<a>',{href:'#'+ind,id:tabID}).text($leg.text());
  772. $tabs.append($('<li>').append($link));
  773. $link.click(function(e){
  774. e.preventDefault;
  775. var ind = this.id;
  776. $tabs.find('.active').removeClass('active');
  777. $(this).addClass('active');
  778. $fset.hide();
  779. $fset.filter('.'+this.id).show();
  780. return false;
  781. });
  782. ind++;
  783. });
  784. $('#tabs').html('').append($tabs);
  785. $('<br>',{class:'clear'}).appendTo('#tabs');
  786. $tabs.find('a:first').trigger('click');
  787. },
  788. /**
  789. * Update all fields on the form after loading a configuration
  790. */
  791. refreshConfigForm: function(cindex) {
  792. /**
  793. * Any manually-created form elements will remain
  794. * where they are. Unknown defines (currently most)
  795. * are added to tabs based on section
  796. *
  797. * Specific exceptions can be managed by applying
  798. * classes to the associated form fields.
  799. * Sorting and arrangement can come from an included
  800. * Javascript file that describes the configuration
  801. * in JSON, or using information added to the config
  802. * files.
  803. *
  804. */
  805. // Refresh the motherboard menu with new options
  806. $('#MOTHERBOARD').html('').addOptions(boards_list);
  807. // Init all existing fields, getting define info for those that need it
  808. // refreshing the options and updating their current values
  809. $.each(define_list[cindex], function(i, name) {
  810. if ($('#'+name).length) {
  811. self.initField(name);
  812. self.initFieldValue(name);
  813. }
  814. else
  815. self.log(name + " is not on the page yet.", 2);
  816. });
  817. // Set enabled state based on dependencies
  818. // this.enableForDependentConditions();
  819. },
  820. /**
  821. * Enable / disable fields in dependent groups
  822. * based on their dependencies.
  823. */
  824. refreshDependentFields: function() {
  825. $.each(define_list, function(e,def_list){
  826. $.each(def_list, function(i, name) {
  827. var inf = define_info[name];
  828. if (inf && inf.enabled != 'true') {
  829. var $elm = $('#'+name), ena = eval(inf.enabled);
  830. var isEnabled = (inf.type == 'switch') || self.defineIsEnabled(name);
  831. $('#'+name+'-switch').attr('disabled', !ena);
  832. $elm.attr('disabled', !(ena && isEnabled)).unblock(ena);
  833. $('label[for="'+name+'"]').unblock(ena);
  834. }
  835. });
  836. });
  837. },
  838. /**
  839. * Make a field responsive, tooltip its label(s), add enabler if needed
  840. */
  841. initField: function(name) {
  842. this.log("initField:"+name,4);
  843. var $elm = $('#'+name), inf = define_info[name];
  844. $elm[0].defineInfo = inf;
  845. // Create a tooltip on the label if there is one
  846. if (inf.tooltip) {
  847. // label for the item
  848. var $tipme = $('label[for="'+name+'"]');
  849. if ($tipme.length) {
  850. $tipme.unbind('mouseenter mouseleave');
  851. $tipme.hover(
  852. function() {
  853. if ($('#tipson input').prop('checked')) {
  854. var pos = $tipme.position(), px = $tipme.width()/2;
  855. $tooltip.html(inf.tooltip)
  856. .append('<span>')
  857. .css({bottom:($tooltip.parent().outerHeight()-pos.top+10)+'px',left:(pos.left+px)+'px'})
  858. .show();
  859. if (hover_timer) {
  860. clearTimeout(hover_timer);
  861. hover_timer = null;
  862. }
  863. }
  864. },
  865. function() {
  866. hover_timer = setTimeout(function(){
  867. hover_timer = null;
  868. $tooltip.fadeOut(400);
  869. }, 400);
  870. }
  871. );
  872. }
  873. }
  874. // Make the element(s) respond to events
  875. if (inf.type == 'list') {
  876. // Multiple fields need to respond
  877. for (var i=0; i<inf.size; i++) {
  878. if (i > 0) $elm = $('#'+name+'-'+i);
  879. $elm.unbind('input');
  880. $elm.on('input', this.handleChange);
  881. }
  882. }
  883. else {
  884. var elmtype = $elm.attr('type');
  885. // Set options on single fields if there are any
  886. if (inf.options !== undefined && elmtype === undefined)
  887. $elm.html('').addOptions(inf.options);
  888. $elm.unbind('input change');
  889. $elm.on(elmtype == 'text' ? 'input' : 'change', this.handleChange);
  890. }
  891. // Add an enabler checkbox if it needs one
  892. if (inf.switchable && $('#'+name+'-switch').length == 0) {
  893. // $elm = the last element added
  894. $elm.after(
  895. $('<input>',{type:'checkbox',value:'1',class:'enabler added'})
  896. .prop('checked',self.defineIsEnabled(name))
  897. .attr({id: name+'-switch'})
  898. .change(self.handleSwitch)
  899. );
  900. }
  901. },
  902. /**
  903. * Handle any value field being changed
  904. * this = the field
  905. */
  906. handleChange: function() {
  907. self.updateDefineFromField(this.id);
  908. self.refreshDependentFields();
  909. },
  910. /**
  911. * Handle a switch checkbox being changed
  912. * this = the switch checkbox
  913. */
  914. handleSwitch: function() {
  915. var $elm = $(this),
  916. name = $elm[0].id.replace(/-.+/,''),
  917. inf = define_info[name],
  918. on = $elm.prop('checked') || false;
  919. self.setDefineEnabled(name, on);
  920. if (inf.type == 'list') {
  921. // Multiple fields?
  922. for (var i=0; i<inf.size; i++) {
  923. $('#'+name+(i?'-'+i:'')).attr('disabled', !on);
  924. }
  925. }
  926. else {
  927. $elm.prev().attr('disabled', !on);
  928. }
  929. },
  930. /**
  931. * Get the current value of a #define
  932. */
  933. defineValue: function(name) {
  934. this.log('defineValue:'+name,4);
  935. var inf = define_info[name];
  936. if (inf == null) return 'n/a';
  937. var r = inf.regex.exec(inf.line), val = r[inf.val_i];
  938. this.log(r,2);
  939. return (inf.type == 'switch') ? (val === undefined || val.trim() != '//') : val;
  940. },
  941. /**
  942. * Get the current enabled state of a #define
  943. */
  944. defineIsEnabled: function(name) {
  945. this.log('defineIsEnabled:'+name,4);
  946. var inf = define_info[name];
  947. if (inf == null) return false;
  948. var r = inf.regex.exec(inf.line);
  949. this.log(r,2);
  950. var on = r[1] != null ? r[1].trim() != '//' : true;
  951. this.log(name + ' = ' + on, 2);
  952. return on;
  953. },
  954. /**
  955. * Set a #define enabled or disabled by altering the config text
  956. */
  957. setDefineEnabled: function(name, val) {
  958. this.log('setDefineEnabled:'+name,4);
  959. var inf = define_info[name];
  960. if (inf) {
  961. var slash = val ? '' : '//';
  962. var newline = inf.line
  963. .replace(/^([ \t]*)(\/\/)([ \t]*)/, '$1$3') // remove slashes
  964. .replace(inf.pre+inf.define, inf.pre+slash+inf.define); // add them back
  965. this.setDefineLine(name, newline);
  966. }
  967. },
  968. /**
  969. * Update a #define (from the form) by altering the config text
  970. */
  971. updateDefineFromField: function(name) {
  972. this.log('updateDefineFromField:'+name,4);
  973. // Drop the suffix on sub-fields
  974. name = name.replace(/-\d+$/, '');
  975. var $elm = $('#'+name), inf = define_info[name];
  976. if (inf == null) return;
  977. var isCheck = $elm.attr('type') == 'checkbox',
  978. val = isCheck ? $elm.prop('checked') : $elm.val().trim();
  979. var newline;
  980. switch(inf.type) {
  981. case 'switch':
  982. var slash = val ? '' : '//';
  983. newline = inf.line.replace(inf.repl, '$1'+slash+'$3');
  984. break;
  985. case 'list':
  986. case 'quoted':
  987. case 'plain':
  988. if (isCheck) this.setMessage(name + ' should not be a checkbox!', 'error');
  989. case 'toggle':
  990. if (isCheck) {
  991. val = val ? inf.options[1] : inf.options[0];
  992. }
  993. else {
  994. if (inf.type == 'list')
  995. for (var i=1; i<inf.size; i++) val += ', ' + $('#'+name+'-'+i).val().trim();
  996. }
  997. newline = inf.line.replace(inf.repl, '$1'+(''+val).replace('$','\\$')+'$3');
  998. break;
  999. }
  1000. this.setDefineLine(name, newline);
  1001. },
  1002. /**
  1003. * Set the define's line in the text to a new line,
  1004. * then update, highlight, and scroll to the line
  1005. */
  1006. setDefineLine: function(name, newline) {
  1007. this.log('setDefineLine:'+name+'\n'+newline,4);
  1008. var inf = define_info[name];
  1009. var $c = $(inf.field), txt = $c.text();
  1010. var hilite_token = '[HIGHLIGHTER-TOKEN]';
  1011. txt = txt.replaceLine(inf.lineNum, hilite_token + newline); // for override line and lineNum would be changed
  1012. inf.line = newline;
  1013. // Convert txt into HTML before storing
  1014. var html = txt.toHTML().replace(hilite_token, '<span></span>');
  1015. // Set the final text including the highlighter
  1016. $c.html(html);
  1017. // Scroll to reveal the define
  1018. if ($c.is(':visible')) this.scrollToDefine(name);
  1019. },
  1020. /**
  1021. * Scroll a pre box to reveal a #define
  1022. */
  1023. scrollToDefine: function(name, always) {
  1024. this.log('scrollToDefine:'+name,4);
  1025. var inf = define_info[name], $c = $(inf.field);
  1026. // Scroll to the altered text if it isn't visible
  1027. var halfHeight = $c.height()/2, scrollHeight = $c.prop('scrollHeight'),
  1028. lineHeight = scrollHeight/[total_config_lines, total_config_adv_lines][inf.cindex],
  1029. textScrollY = (inf.lineNum * lineHeight - halfHeight).limit(0, scrollHeight - 1);
  1030. if (always || Math.abs($c.prop('scrollTop') - textScrollY) > halfHeight) {
  1031. $c.find('span').height(lineHeight);
  1032. $c.animate({ scrollTop: textScrollY });
  1033. }
  1034. },
  1035. /**
  1036. * Set a form field to the current #define value in the config text
  1037. */
  1038. initFieldValue: function(name) {
  1039. var $elm = $('#'+name), inf = define_info[name],
  1040. val = this.defineValue(name);
  1041. this.log('initFieldValue:' + name + ' to ' + val, 2);
  1042. // If the item is switchable then set enabled state too
  1043. var $cb = $('#'+name+'-switch'), avail = eval(inf.enabled), on = true;
  1044. if ($cb.length) {
  1045. on = self.defineIsEnabled(name);
  1046. $cb.prop('checked', on);
  1047. }
  1048. if (inf.type == 'list') {
  1049. $.each(val.split(','),function(i,v){
  1050. var $e = i > 0 ? $('#'+name+'-'+i) : $elm;
  1051. $e.val(v.trim());
  1052. $e.attr('disabled', !(on && avail)).unblock(avail);
  1053. });
  1054. }
  1055. else {
  1056. if (inf.type == 'toggle') val = val == inf.options[1];
  1057. $elm.attr('type') == 'checkbox' ? $elm.prop('checked', val) : $elm.val(''+val);
  1058. $elm.attr('disabled', !(on && avail)).unblock(avail); // enable/disable the form field (could also dim it)
  1059. }
  1060. $('label[for="'+name+'"]').unblock(avail);
  1061. },
  1062. /**
  1063. * Purge added fields and all their define info
  1064. */
  1065. purgeAddedFields: function(cindex) {
  1066. $.each(define_list[cindex], function(i, name){
  1067. $('#'+name + ",[id^='"+name+"-'],label[for='"+name+"']").filter('.added').remove();
  1068. });
  1069. define_list[cindex] = [];
  1070. },
  1071. /**
  1072. * Get information about a #define from configuration file text:
  1073. *
  1074. * - Pre-examine the #define for its prefix, value position, suffix, etc.
  1075. * - Construct RegExp's for the #define to find and replace values.
  1076. * - Store the existing #define line as a fast key to finding it later.
  1077. * - Determine the line number of the #define
  1078. * - Gather nearby comments to be used as tooltips.
  1079. * - Look for JSON in nearby comments for use as select options.
  1080. *
  1081. * define_info[name]
  1082. * .type type of define: switch, list, quoted, plain, or toggle
  1083. * .size the number of items in a "list" type
  1084. * .options select options, if any
  1085. * .cindex config index
  1086. * .field pre containing the config text (config_list[cindex][0])
  1087. * .line the full line from the config text
  1088. * .pre any text preceding #define
  1089. * .define the "#define NAME" text (may have leading spaces)
  1090. * .post the text following the "#define NAME val" part
  1091. * .regex regexp to get the value from the line
  1092. * .repl regexp to replace the value in the line
  1093. * .val_i the value's index in the .regex result
  1094. */
  1095. getDefineInfo: function(name, cindex) {
  1096. if (cindex === undefined) cindex = 0;
  1097. this.log('getDefineInfo:'+name,4);
  1098. var $c = config_list[cindex], txt = $c.text(),
  1099. info = { type:0, cindex:cindex, field:$c[0], val_i:2 }, post;
  1100. // a switch line with no value
  1101. var find = new RegExp('^([ \\t]*//)?([ \\t]*#define[ \\t]+' + name + ')([ \\t]*(/[*/].*)?)$', 'm'),
  1102. r = find.exec(txt);
  1103. if (r !== null) {
  1104. post = r[3] == null ? '' : r[3];
  1105. $.extend(info, {
  1106. type: 'switch',
  1107. val_i: 1,
  1108. regex: new RegExp('([ \\t]*//)?([ \\t]*' + r[2].regEsc() + post.regEsc() + ')', 'm'),
  1109. repl: new RegExp('([ \\t]*)(\/\/)?([ \\t]*' + r[2].regEsc() + post.regEsc() + ')', 'm')
  1110. });
  1111. }
  1112. else {
  1113. // a define with curly braces
  1114. find = new RegExp('^(.*//)?(.*#define[ \\t]+' + name + '[ \\t]+)(\{[^\}]*\})([ \\t]*(/[*/].*)?)$', 'm');
  1115. r = find.exec(txt);
  1116. if (r !== null) {
  1117. post = r[4] == null ? '' : r[4];
  1118. $.extend(info, {
  1119. type: 'list',
  1120. size: r[3].split(',').length,
  1121. regex: new RegExp('([ \\t]*//)?[ \\t]*' + r[2].regEsc() + '\{([^\}]*)\}' + post.regEsc(), 'm'),
  1122. repl: new RegExp('(([ \\t]*//)?[ \\t]*' + r[2].regEsc() + '\{)[^\}]*(\}' + post.regEsc() + ')', 'm')
  1123. });
  1124. }
  1125. else {
  1126. // a define with quotes
  1127. find = new RegExp('^(.*//)?(.*#define[ \\t]+' + name + '[ \\t]+)("[^"]*")([ \\t]*(/[*/].*)?)$', 'm');
  1128. r = find.exec(txt);
  1129. if (r !== null) {
  1130. post = r[4] == null ? '' : r[4];
  1131. $.extend(info, {
  1132. type: 'quoted',
  1133. regex: new RegExp('([ \\t]*//)?[ \\t]*' + r[2].regEsc() + '"([^"]*)"' + post.regEsc(), 'm'),
  1134. repl: new RegExp('(([ \\t]*//)?[ \\t]*' + r[2].regEsc() + '")[^"]*("' + post.regEsc() + ')', 'm')
  1135. });
  1136. }
  1137. else {
  1138. // a define with no quotes
  1139. find = new RegExp('^([ \\t]*//)?([ \\t]*#define[ \\t]+' + name + '[ \\t]+)(\\S*)([ \\t]*(/[*/].*)?)$', 'm');
  1140. r = find.exec(txt);
  1141. if (r !== null) {
  1142. post = r[4] == null ? '' : r[4];
  1143. $.extend(info, {
  1144. type: 'plain',
  1145. regex: new RegExp('([ \\t]*//)?[ \\t]*' + r[2].regEsc() + '(\\S*)' + post.regEsc(), 'm'),
  1146. repl: new RegExp('(([ \\t]*//)?[ \\t]*' + r[2].regEsc() + ')\\S*(' + post.regEsc() + ')', 'm')
  1147. });
  1148. if (r[3].match(/false|true/)) {
  1149. info.type = 'toggle';
  1150. info.options = ['false','true'];
  1151. }
  1152. }
  1153. }
  1154. }
  1155. }
  1156. // Success?
  1157. if (info.type) {
  1158. $.extend(info, {
  1159. line: r[0],
  1160. pre: r[1] == null ? '' : r[1].replace('//',''),
  1161. define: r[2],
  1162. post: post
  1163. });
  1164. // Get the end-of-line comment, if there is one
  1165. var tooltip = '', eoltip = '';
  1166. find = new RegExp('.*#define[ \\t].*/[/*]+[ \\t]*(.*)');
  1167. if (info.line.search(find) >= 0)
  1168. eoltip = tooltip = info.line.replace(find, '$1');
  1169. // Get all the comments immediately before the item
  1170. var s;
  1171. find = new RegExp('(([ \\t]*(//|#)[^\n]+\n){1,4})' + info.line.regEsc(), 'g');
  1172. if (r = find.exec(txt)) {
  1173. // Get the text of the found comments
  1174. find = new RegExp('^[ \\t]*//+[ \\t]*(.*)[ \\t]*$', 'gm');
  1175. while((s = find.exec(r[1])) !== null) {
  1176. var tip = s[1].replace(/[ \\t]*(={5,}|(#define[ \\t]+.*|@section[ \\t]+\w+))[ \\t]*/g, '');
  1177. if (tip.length) {
  1178. if (tip.match(/^#define[ \\t]/) != null) tooltip = eoltip;
  1179. // JSON data? Save as select options
  1180. if (!info.options && tip.match(/:[\[{]/) != null) {
  1181. // TODO
  1182. // :[1-6] = value limits
  1183. var o; eval('o=' + tip.substr(1));
  1184. info.options = o;
  1185. if (Object.prototype.toString.call(o) == "[object Array]" && o.length == 2 && !eval(''+o[0]))
  1186. info.type = 'toggle';
  1187. }
  1188. else {
  1189. // Other lines added to the tooltip
  1190. tooltip += ' ' + tip + '\n';
  1191. }
  1192. }
  1193. }
  1194. }
  1195. // Add .tooltip and .lineNum properties to the info
  1196. find = new RegExp('^'+name); // Strip the name from the tooltip
  1197. var lineNum = this.getLineNumberOfText(info.line, txt);
  1198. // See if this define is enabled conditionally
  1199. var enable_cond = '';
  1200. $.each(dependent_groups, function(cond,dat){
  1201. $.each(dat, function(i,o){
  1202. if (o.cindex == cindex && lineNum > o.start && lineNum < o.end) {
  1203. if (enable_cond != '') enable_cond += ' && ';
  1204. enable_cond += '(' + cond + ')';
  1205. }
  1206. });
  1207. });
  1208. $.extend(info, {
  1209. tooltip: '<strong>'+name+'</strong> '+tooltip.trim().replace(find,'').toHTML(),
  1210. lineNum: lineNum,
  1211. switchable: (info.type != 'switch' && info.line.match(/^[ \t]*\/\//)) || false, // Disabled? Mark as "switchable"
  1212. enabled: enable_cond ? enable_cond : 'true'
  1213. });
  1214. }
  1215. else
  1216. info = null;
  1217. this.log(info, 2);
  1218. return info;
  1219. },
  1220. /**
  1221. * Count the number of lines before a match, return -1 on fail
  1222. */
  1223. getLineNumberOfText: function(line, txt) {
  1224. var pos = txt.indexOf(line);
  1225. return (pos < 0) ? pos : txt.lineCount(pos);
  1226. },
  1227. /**
  1228. * Add a temporary message to the page
  1229. */
  1230. setMessage: function(msg,type) {
  1231. if (msg) {
  1232. if (type === undefined) type = 'message';
  1233. var $err = $('<p class="'+type+'">'+msg+'<span>x</span></p>').appendTo($msgbox), err = $err[0];
  1234. var baseColor = $err.css('color').replace(/rgba?\(([^),]+,[^),]+,[^),]+).*/, 'rgba($1,');
  1235. err.pulse_offset = (pulse_offset += 200);
  1236. err.startTime = Date.now() + pulse_offset;
  1237. err.pulser = setInterval(function(){
  1238. var pulse_time = Date.now() + err.pulse_offset;
  1239. var opac = 0.5+Math.sin(pulse_time/200)*0.4;
  1240. $err.css({color:baseColor+(opac)+')'});
  1241. if (pulse_time - err.startTime > 2500 && opac > 0.899) {
  1242. clearInterval(err.pulser);
  1243. }
  1244. }, 50);
  1245. $err.click(function(e) {
  1246. $(this).remove();
  1247. self.adjustFormLayout();
  1248. return false;
  1249. }).css({cursor:'pointer'});
  1250. }
  1251. else {
  1252. $msgbox.find('p.error, p.warning').each(function() {
  1253. if (this.pulser !== undefined && this.pulser)
  1254. clearInterval(this.pulser);
  1255. $(this).remove();
  1256. });
  1257. }
  1258. self.adjustFormLayout();
  1259. },
  1260. adjustFormLayout: function() {
  1261. var wtop = $(window).scrollTop(),
  1262. ctop = $cfg.offset().top,
  1263. thresh = $form.offset().top+100;
  1264. if (ctop < thresh) {
  1265. var maxhi = $form.height(); // pad plus heights of config boxes can't be more than this
  1266. var pad = wtop > ctop ? wtop-ctop : 0; // pad the top box to stay in view
  1267. var innerpad = Math.ceil($cfg.height() - $cfg.find('pre').height());
  1268. // height to use for the inner boxes
  1269. var hi = ($(window).height() - ($cfg.offset().top - pad) + wtop - innerpad)/2;
  1270. if (hi < 200) hi = 200;
  1271. $cfg.css({ paddingTop: pad });
  1272. var $pre = $('pre.config');
  1273. $pre.css({ height: Math.floor(hi) - $pre.position().top });
  1274. }
  1275. else {
  1276. $cfg.css({ paddingTop: wtop > ctop ? wtop-ctop : 0, height: '' });
  1277. }
  1278. },
  1279. setRequestError: function(stat, path) {
  1280. self.setMessage('Error '+stat+' – ' + path.replace(/^(https:\/\/[^\/]+\/)?.+(\/[^\/]+)$/, '$1...$2'), 'error');
  1281. },
  1282. log: function(o,l) {
  1283. if (l === undefined) l = 0;
  1284. if (this.logging>=l*1) console.log(o);
  1285. },
  1286. logOnce: function(o) {
  1287. if (o.didLogThisObject === undefined) {
  1288. this.log(o);
  1289. o.didLogThisObject = true;
  1290. }
  1291. },
  1292. EOF: null
  1293. };
  1294. })();
  1295. // Typically the app would be in its own file, but this would be here
  1296. window.configuratorApp.init();
  1297. });