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

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