My Marlin configs for Fabrikator Mini and CTC i3 Pro B
Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

configurator.js 47KB

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