Console.hx 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619
  1. package h2d;
  2. import hxd.Key;
  3. /**
  4. The console argument type.
  5. **/
  6. enum ConsoleArg {
  7. /**
  8. An integer parameter.
  9. **/
  10. AInt;
  11. /**
  12. A floating-point parameter.
  13. **/
  14. AFloat;
  15. /**
  16. A text string parameter.
  17. **/
  18. AString;
  19. /**
  20. A boolean parameter. Can be `true`, `false`, `1` or `0`.
  21. **/
  22. ABool;
  23. /**
  24. A text string parameter with limitation to only accept the specified list values.
  25. **/
  26. AEnum( values : Array<String> );
  27. /**
  28. An array of remaining arguments.
  29. **/
  30. AArray(t: ConsoleArg);
  31. }
  32. /**
  33. A descriptor for an argument of a console command.
  34. **/
  35. typedef ConsoleArgDesc = {
  36. /**
  37. A human-readable argument name.
  38. **/
  39. name : String,
  40. /**
  41. The type of the argument.
  42. **/
  43. t : ConsoleArg,
  44. /**
  45. When set, argument is considered optional and command callback will receive `null` if argument was omitted.
  46. Inserting optional arguments between non-optional arguments leads to an undefined behavior.
  47. **/
  48. ?opt : Bool,
  49. }
  50. /**
  51. A simple debug console integration.
  52. Console can be focused manually through `Console.show` and `Console.hide` methods
  53. as well as by pressing the key defined by `Console.shortKeyChar`.
  54. It's possible to log messages to console via `Console.log` method.
  55. By default comes with 2 commands: `help` and `cls`, which print help message
  56. describing all commands and clears the console logs respectively.
  57. To add custom commands, use `Console.add` and `Console.addCommand` methods.
  58. **/
  59. class Console #if !macro extends h2d.Object #end {
  60. #if !macro
  61. /**
  62. The timeout in seconds before log will automatically hide after the last message.
  63. **/
  64. public static var HIDE_LOG_TIMEOUT = 3.;
  65. var width : Int;
  66. var height : Int;
  67. var bg : h2d.Bitmap;
  68. var tf : h2d.TextInput;
  69. var hintTxt: h2d.Text;
  70. var logTxt : h2d.HtmlText;
  71. var lastLogTime : Float;
  72. var commands : Map < String, { help : String, args : Array<ConsoleArgDesc>, callb : Dynamic } > ;
  73. var aliases : Map<String,String>;
  74. var logDY : Float = 0;
  75. var logs : Array<String>;
  76. var logIndex:Int;
  77. var curCmd:String;
  78. var errorColor = 0xC00000;
  79. /**
  80. The text character which should be pressed in order to automatically show console input.
  81. **/
  82. public var shortKeyChar : Int = "/".code;
  83. /**
  84. Provide an auto-complete on Enter/Tab key and command completion hints.
  85. **/
  86. public var autoComplete : Bool = true;
  87. /**
  88. Create a new Console instance using the provided font and parent.
  89. @param font The font to use for console text input and log.
  90. @param parent An optional parent `h2d.Object` instance to which Console adds itself if set.
  91. **/
  92. public function new(font:h2d.Font,?parent) {
  93. super(parent);
  94. height = Math.ceil(font.lineHeight) + 2;
  95. logTxt = new h2d.HtmlText(font, this);
  96. logTxt.x = 2;
  97. logTxt.dropShadow = { dx : 0, dy : 1, color : 0, alpha : 0.5 };
  98. logTxt.visible = false;
  99. logs = [];
  100. logIndex = -1;
  101. bg = new h2d.Bitmap(h2d.Tile.fromColor(0,1,1,0.5), this);
  102. bg.visible = false;
  103. hintTxt = new h2d.Text(font, bg);
  104. hintTxt.x = 2;
  105. hintTxt.y = 1;
  106. hintTxt.textColor = 0xFFFFFFFF;
  107. hintTxt.alpha = 0.5;
  108. tf = new h2d.TextInput(font, bg);
  109. tf.onKeyDown = handleKey;
  110. tf.onChange = handleCmdChange;
  111. tf.onFocusLost = function(_) hide();
  112. tf.x = 2;
  113. tf.y = 1;
  114. tf.textColor = 0xFFFFFFFF;
  115. resetCommands();
  116. }
  117. /**
  118. * Reset all commands and aliases to default
  119. */
  120. public function resetCommands() {
  121. commands = new Map();
  122. aliases = new Map();
  123. addCommand("help", "Show help", [ { name : "command", t : AString, opt : true } ], showHelp);
  124. addCommand("cls", "Clear console", [], function() {
  125. logs = [];
  126. logTxt.text = "";
  127. });
  128. addAlias("?", "help");
  129. }
  130. /**
  131. Add a new command to console.
  132. @param name Command name.
  133. @param help Optional command description text.
  134. @param args An array of command arguments.
  135. @param callb The callback method taking the arguments listed in `args`.
  136. **/
  137. public function addCommand( name, ?help, args : Array<ConsoleArgDesc>, callb : Dynamic ) {
  138. commands.set(name, { help : help == null ? "" : help, args:args, callb:callb } );
  139. }
  140. #end
  141. /**
  142. Add a new command to console. <span class="label">Macro method</span>
  143. The `callb` method arguments are used to determine console argument type and names. Due to that,
  144. only the following callback argument types are supported: `Int`, `Float`, `String` and `Bool`.
  145. Another limitation is that commands added via macro do not contain description.
  146. For example:
  147. ```haxe
  148. function addItem(id:Int, ?amount:Int) {
  149. var item = findItemById(id)
  150. if (amount == null) amount = 1;
  151. player.giveItem(item, amount);
  152. console.log('Added $amount x ${item.name} to player!');
  153. }
  154. // Macro call automatically takes addItem arguments.
  155. console.add("additem", addItem);
  156. // And is equivalent to using addCommand describing each argument manually:
  157. console.addCommand("additem", null, [{ name: "id", t: AInt }, { name: "amount", t: AInt, opt: true }], addItem);
  158. ```
  159. @param name A String expression of the command name.
  160. @param callb An expression that points at the callback method.
  161. **/
  162. public macro function add( ethis, name, callb ) {
  163. var args = [];
  164. var et = haxe.macro.Context.typeExpr(callb);
  165. switch( haxe.macro.Context.follow(et.t) ) {
  166. case TFun(fargs, _):
  167. for( a in fargs ) {
  168. var t = haxe.macro.Context.followWithAbstracts(a.t);
  169. var tstr = haxe.macro.TypeTools.toString(t);
  170. var tval = switch( tstr ) {
  171. case "Int": AInt;
  172. case "Float": AFloat;
  173. case "String": AString;
  174. case "Bool": ABool;
  175. default: haxe.macro.Context.error("Unsupported parameter type "+tstr+" for argument "+a.name, callb.pos);
  176. }
  177. var tname = ""+tval;
  178. args.push(macro { name : $v{a.name}, t : h2d.Console.ConsoleArg.$tname, opt : $v{a.opt} });
  179. }
  180. default:
  181. haxe.macro.Context.error(haxe.macro.TypeTools.toString(et.t)+" should be a function", callb.pos);
  182. }
  183. return macro $ethis.addCommand($name,null,$a{args},$callb);
  184. }
  185. #if !macro
  186. /**
  187. Add an alias to an existing command.
  188. @param name Command alias.
  189. @param command Full command name to alias.
  190. **/
  191. public function addAlias( name, command ) {
  192. aliases.set(name, command);
  193. }
  194. /**
  195. Executes `commandLine` the same way the user would execute it.
  196. **/
  197. public function runCommand( commandLine : String ) {
  198. handleCommand(commandLine);
  199. }
  200. override function onAdd() {
  201. super.onAdd();
  202. @:privateAccess getScene().window.addEventTarget(onEvent);
  203. }
  204. override function onRemove() {
  205. @:privateAccess getScene().window.removeEventTarget(onEvent);
  206. super.onRemove();
  207. }
  208. function onEvent( e : hxd.Event ) {
  209. switch( e.kind ) {
  210. case EWheel:
  211. if( logTxt.visible ) {
  212. logDY -= tf.font.lineHeight * e.wheelDelta * 3;
  213. if( logDY < 0 ) logDY = 0;
  214. if( logDY > logTxt.textHeight ) logDY = logTxt.textHeight;
  215. e.propagate = false;
  216. }
  217. case ETextInput:
  218. if( e.charCode == shortKeyChar && !bg.visible )
  219. show();
  220. default:
  221. }
  222. }
  223. function showHelp( ?command : String ) {
  224. var all;
  225. if( command == null ) {
  226. all = Lambda.array( { iterator : function() return commands.keys() } );
  227. all.sort(Reflect.compare);
  228. all.remove("help");
  229. all.push("help");
  230. } else {
  231. if( aliases.exists(command) ) command = aliases.get(command);
  232. if( !commands.exists(command) )
  233. throw 'Command not found "$command"';
  234. all = [command];
  235. }
  236. for( cmdName in all ) {
  237. var c = commands.get(cmdName);
  238. var str = String.fromCharCode(shortKeyChar) + cmdName;
  239. for( a in aliases.keys() )
  240. if( aliases.get(a) == cmdName )
  241. str += "|" + a;
  242. for( a in c.args ) {
  243. var astr = a.name;
  244. switch( a.t ) {
  245. case AInt, AFloat, AArray(_):
  246. astr += ":"+a.t.getName().substr(1);
  247. case AString:
  248. // nothing
  249. case AEnum(values):
  250. astr += "=" + values.join("|");
  251. case ABool:
  252. astr += "=0|1";
  253. }
  254. str += " " + (a.opt?"["+astr+"]":astr);
  255. }
  256. if( c.help != "" )
  257. str += " : " + c.help;
  258. log(str);
  259. }
  260. }
  261. /**
  262. Checks if the Console is currently shown.
  263. **/
  264. public function isActive() {
  265. return bg.visible;
  266. }
  267. /**
  268. Hides the Console.
  269. **/
  270. public function hide() {
  271. bg.visible = false;
  272. tf.text = "";
  273. hintTxt.text = "";
  274. tf.cursorIndex = -1;
  275. }
  276. /**
  277. Shows and focuses the Console.
  278. **/
  279. public function show() {
  280. bg.visible = true;
  281. tf.focus();
  282. tf.cursorIndex = tf.text.length;
  283. logIndex = -1;
  284. }
  285. function getCommandSuggestion(cmd : String) : String {
  286. var hadShortKey = false;
  287. if (cmd.charCodeAt(0) == shortKeyChar) {
  288. hadShortKey = true;
  289. cmd = cmd.substr(1);
  290. }
  291. if (cmd == "") {
  292. return "";
  293. }
  294. var lowCmd = cmd.toLowerCase();
  295. var closestCommand = "";
  296. var commandNames = commands.keys();
  297. for (command in commandNames) {
  298. if (command.toLowerCase().indexOf(lowCmd) == 0) {
  299. if (closestCommand == "" || closestCommand.length > command.length) {
  300. closestCommand = command;
  301. }
  302. }
  303. }
  304. if( aliases.exists(cmd) )
  305. closestCommand = cmd;
  306. if (hadShortKey && closestCommand != "")
  307. closestCommand = String.fromCharCode(shortKeyChar) + closestCommand;
  308. return closestCommand;
  309. }
  310. function handleKey( e : hxd.Event ) {
  311. if( !bg.visible )
  312. return;
  313. switch( e.keyCode ) {
  314. case Key.ENTER, Key.NUMPAD_ENTER:
  315. var cmd = tf.text;
  316. tf.text = "";
  317. hintTxt.text = "";
  318. if (autoComplete) {
  319. var suggestion = getCommandSuggestion(cmd);
  320. if (suggestion != "") {
  321. cmd = suggestion;
  322. }
  323. }
  324. handleCommand(cmd);
  325. if( !logTxt.visible ) bg.visible = false;
  326. e.cancel = true;
  327. return;
  328. case Key.TAB:
  329. if (autoComplete) {
  330. if (hintTxt.text != "") {
  331. tf.text = hintTxt.text + " ";
  332. tf.cursorIndex = tf.text.length;
  333. }
  334. }
  335. case Key.ESCAPE:
  336. hide();
  337. case Key.UP:
  338. if(logs.length == 0 || logIndex == 0) return;
  339. if(logIndex == -1) {
  340. curCmd = tf.text;
  341. logIndex = logs.length - 1;
  342. }
  343. else {
  344. var curLog = logs[logIndex];
  345. while(curLog == logs[logIndex] && logIndex > 0)
  346. logIndex--;
  347. }
  348. tf.text = logs[logIndex];
  349. tf.cursorIndex = tf.text.length;
  350. case Key.DOWN:
  351. if(tf.text == curCmd) return;
  352. var curLog = logs[logIndex];
  353. while(curLog == logs[logIndex] && logIndex < logs.length - 1)
  354. logIndex++;
  355. if(logIndex == logs.length - 1) {
  356. tf.text = curCmd == null ? "" : curCmd;
  357. tf.cursorIndex = tf.text.length;
  358. logIndex = -1;
  359. return;
  360. }
  361. tf.text = logs[logIndex];
  362. tf.cursorIndex = tf.text.length;
  363. }
  364. }
  365. function handleCmdChange() {
  366. hintTxt.visible = autoComplete;
  367. if (autoComplete) {
  368. hintTxt.text = getCommandSuggestion(tf.text);
  369. } else {
  370. hintTxt.text = "";
  371. }
  372. }
  373. function handleCommand( command : String ) {
  374. command = StringTools.trim(command);
  375. if( command.charCodeAt(0) == shortKeyChar ) command = command.substr(1);
  376. if( command == "" ) {
  377. hide();
  378. return;
  379. }
  380. logs.push(command);
  381. logIndex = -1;
  382. var args = [];
  383. var c = '';
  384. var i = 0;
  385. function readString(endChar:String) {
  386. var string = '';
  387. while (i < command.length) {
  388. c = command.charAt(++i);
  389. if (c == endChar) {
  390. ++i;
  391. return string;
  392. }
  393. string += c;
  394. }
  395. return null;
  396. }
  397. inline function skipSpace() {
  398. c = command.charAt(i);
  399. while (c == ' ' || c == '\t') {
  400. c = command.charAt(++i);
  401. }
  402. --i;
  403. }
  404. var last = '';
  405. while (i < command.length) {
  406. c = command.charAt(i);
  407. switch (c) {
  408. case ' ' | '\t':
  409. skipSpace();
  410. args.push(last);
  411. last = '';
  412. case "'" | '"':
  413. var string = readString(c);
  414. if (string == null) {
  415. log('Bad formated string', errorColor);
  416. return;
  417. }
  418. last = string;
  419. if (i < command.length - 1) {
  420. args.push(string);
  421. last = '';
  422. }
  423. skipSpace();
  424. default:
  425. last += c;
  426. }
  427. ++i;
  428. }
  429. args.push(last);
  430. var cmdName = args[0];
  431. if( aliases.exists(cmdName) ) cmdName = aliases.get(cmdName);
  432. var cmd = commands.get(cmdName);
  433. if( cmd == null ) {
  434. log('Unknown command "${cmdName}"',errorColor);
  435. return;
  436. }
  437. function parseArgument( v : String, t: ConsoleArg, name : String, loneArg = false ): Dynamic {
  438. switch( t ) {
  439. case AInt:
  440. var i = Std.parseInt(v);
  441. if( i == null ) {
  442. log('$v should be Int for argument $name',errorColor);
  443. return null;
  444. }
  445. return i;
  446. case AFloat:
  447. var f = Std.parseFloat(v);
  448. if( Math.isNaN(f) ) {
  449. log('$v should be Float for argument $name',errorColor);
  450. return null;
  451. }
  452. return f;
  453. case ABool:
  454. switch( v ) {
  455. case "true", "1": return true;
  456. case "false", "0": return false;
  457. default:
  458. log('$v should be Bool for argument $name',errorColor);
  459. return null;
  460. }
  461. case AString:
  462. // if we take a single string, let's pass the whole args (allows spaces)
  463. return loneArg ? StringTools.trim(command.substr(args[0].length)) : v;
  464. case AEnum(values):
  465. var found = false;
  466. for( v2 in values ) {
  467. if( v == v2 )
  468. return v2;
  469. }
  470. log('$v should be [${values.join("|")}] for argument $name', errorColor);
  471. return null;
  472. case AArray(t):
  473. log('Cannot have nested arrays for argument $name');
  474. return null;
  475. }
  476. return null;
  477. }
  478. var vargs = new Array<Dynamic>();
  479. for( i in 0...cmd.args.length ) {
  480. var a = cmd.args[i];
  481. switch( a.t ) {
  482. case AArray(t):
  483. if (i != cmd.args.length - 1) {
  484. log('Array ${a.name} should be last argument',errorColor);
  485. return;
  486. }
  487. var arr = [];
  488. for (j in (i + 1)...args.length) {
  489. var v = args[j];
  490. var parsed = parseArgument(v, t, a.name);
  491. if (parsed == null)
  492. return;
  493. arr.push(parsed);
  494. }
  495. vargs.push(arr);
  496. default:
  497. var v = args[i + 1];
  498. if( v == null ) {
  499. if( a.opt ) {
  500. vargs.push(null);
  501. continue;
  502. }
  503. log('Missing argument ${a.name}',errorColor);
  504. return;
  505. }
  506. var parsed = parseArgument(v, a.t, a.name, cmd.args.length == 1);
  507. if (parsed == null)
  508. return;
  509. vargs.push(parsed);
  510. }
  511. }
  512. doCall(cmd.callb, vargs);
  513. }
  514. function doCall( callb : Dynamic, vargs : Array<Dynamic> ) {
  515. try {
  516. Reflect.callMethod(null, callb, vargs);
  517. } catch( e : String ) {
  518. log('ERROR $e', errorColor);
  519. }
  520. }
  521. /**
  522. Print to the console log.
  523. @param text The text to show in the log message.
  524. @param color Optional custom text color.
  525. **/
  526. public function log( text : String, ?color ) {
  527. if( color == null ) color = tf.textColor;
  528. var oldH = logTxt.textHeight;
  529. logTxt.text = logTxt.text + '<font color="#${StringTools.hex(color&0xFFFFFF,6)}">${StringTools.htmlEscape(text)}</font><br/>';
  530. if( logDY != 0 ) logDY += logTxt.textHeight - oldH;
  531. logTxt.alpha = 1;
  532. logTxt.visible = true;
  533. lastLogTime = haxe.Timer.stamp();
  534. }
  535. override function sync(ctx:h2d.RenderContext) {
  536. var scene = ctx.scene;
  537. if( scene != null ) {
  538. x = 0;
  539. y = scene.height - height*scaleY;
  540. width = scene.width;
  541. tf.maxWidth = width;
  542. bg.tile.scaleToSize(width, height);
  543. }
  544. var log = logTxt;
  545. if( log.visible ) {
  546. log.y = bg.y - log.textHeight + logDY;
  547. var dt = haxe.Timer.stamp() - lastLogTime;
  548. if( dt > HIDE_LOG_TIMEOUT && !bg.visible ) {
  549. log.alpha -= ctx.elapsedTime * 4;
  550. if( log.alpha <= 0 )
  551. log.visible = false;
  552. }
  553. }
  554. super.sync(ctx);
  555. }
  556. #end
  557. }