123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619 |
- package h2d;
- import hxd.Key;
- /**
- The console argument type.
- **/
- enum ConsoleArg {
- /**
- An integer parameter.
- **/
- AInt;
- /**
- A floating-point parameter.
- **/
- AFloat;
- /**
- A text string parameter.
- **/
- AString;
- /**
- A boolean parameter. Can be `true`, `false`, `1` or `0`.
- **/
- ABool;
- /**
- A text string parameter with limitation to only accept the specified list values.
- **/
- AEnum( values : Array<String> );
- /**
- An array of remaining arguments.
- **/
- AArray(t: ConsoleArg);
- }
- /**
- A descriptor for an argument of a console command.
- **/
- typedef ConsoleArgDesc = {
- /**
- A human-readable argument name.
- **/
- name : String,
- /**
- The type of the argument.
- **/
- t : ConsoleArg,
- /**
- When set, argument is considered optional and command callback will receive `null` if argument was omitted.
- Inserting optional arguments between non-optional arguments leads to an undefined behavior.
- **/
- ?opt : Bool,
- }
- /**
- A simple debug console integration.
- Console can be focused manually through `Console.show` and `Console.hide` methods
- as well as by pressing the key defined by `Console.shortKeyChar`.
- It's possible to log messages to console via `Console.log` method.
- By default comes with 2 commands: `help` and `cls`, which print help message
- describing all commands and clears the console logs respectively.
- To add custom commands, use `Console.add` and `Console.addCommand` methods.
- **/
- class Console #if !macro extends h2d.Object #end {
- #if !macro
- /**
- The timeout in seconds before log will automatically hide after the last message.
- **/
- public static var HIDE_LOG_TIMEOUT = 3.;
- var width : Int;
- var height : Int;
- var bg : h2d.Bitmap;
- var tf : h2d.TextInput;
- var hintTxt: h2d.Text;
- var logTxt : h2d.HtmlText;
- var lastLogTime : Float;
- var commands : Map < String, { help : String, args : Array<ConsoleArgDesc>, callb : Dynamic } > ;
- var aliases : Map<String,String>;
- var logDY : Float = 0;
- var logs : Array<String>;
- var logIndex:Int;
- var curCmd:String;
- var errorColor = 0xC00000;
- /**
- The text character which should be pressed in order to automatically show console input.
- **/
- public var shortKeyChar : Int = "/".code;
- /**
- Provide an auto-complete on Enter/Tab key and command completion hints.
- **/
- public var autoComplete : Bool = true;
- /**
- Create a new Console instance using the provided font and parent.
- @param font The font to use for console text input and log.
- @param parent An optional parent `h2d.Object` instance to which Console adds itself if set.
- **/
- public function new(font:h2d.Font,?parent) {
- super(parent);
- height = Math.ceil(font.lineHeight) + 2;
- logTxt = new h2d.HtmlText(font, this);
- logTxt.x = 2;
- logTxt.dropShadow = { dx : 0, dy : 1, color : 0, alpha : 0.5 };
- logTxt.visible = false;
- logs = [];
- logIndex = -1;
- bg = new h2d.Bitmap(h2d.Tile.fromColor(0,1,1,0.5), this);
- bg.visible = false;
- hintTxt = new h2d.Text(font, bg);
- hintTxt.x = 2;
- hintTxt.y = 1;
- hintTxt.textColor = 0xFFFFFFFF;
- hintTxt.alpha = 0.5;
- tf = new h2d.TextInput(font, bg);
- tf.onKeyDown = handleKey;
- tf.onChange = handleCmdChange;
- tf.onFocusLost = function(_) hide();
- tf.x = 2;
- tf.y = 1;
- tf.textColor = 0xFFFFFFFF;
- resetCommands();
- }
- /**
- * Reset all commands and aliases to default
- */
- public function resetCommands() {
- commands = new Map();
- aliases = new Map();
- addCommand("help", "Show help", [ { name : "command", t : AString, opt : true } ], showHelp);
- addCommand("cls", "Clear console", [], function() {
- logs = [];
- logTxt.text = "";
- });
- addAlias("?", "help");
- }
- /**
- Add a new command to console.
- @param name Command name.
- @param help Optional command description text.
- @param args An array of command arguments.
- @param callb The callback method taking the arguments listed in `args`.
- **/
- public function addCommand( name, ?help, args : Array<ConsoleArgDesc>, callb : Dynamic ) {
- commands.set(name, { help : help == null ? "" : help, args:args, callb:callb } );
- }
- #end
- /**
- Add a new command to console. <span class="label">Macro method</span>
- The `callb` method arguments are used to determine console argument type and names. Due to that,
- only the following callback argument types are supported: `Int`, `Float`, `String` and `Bool`.
- Another limitation is that commands added via macro do not contain description.
- For example:
- ```haxe
- function addItem(id:Int, ?amount:Int) {
- var item = findItemById(id)
- if (amount == null) amount = 1;
- player.giveItem(item, amount);
- console.log('Added $amount x ${item.name} to player!');
- }
- // Macro call automatically takes addItem arguments.
- console.add("additem", addItem);
- // And is equivalent to using addCommand describing each argument manually:
- console.addCommand("additem", null, [{ name: "id", t: AInt }, { name: "amount", t: AInt, opt: true }], addItem);
- ```
- @param name A String expression of the command name.
- @param callb An expression that points at the callback method.
- **/
- public macro function add( ethis, name, callb ) {
- var args = [];
- var et = haxe.macro.Context.typeExpr(callb);
- switch( haxe.macro.Context.follow(et.t) ) {
- case TFun(fargs, _):
- for( a in fargs ) {
- var t = haxe.macro.Context.followWithAbstracts(a.t);
- var tstr = haxe.macro.TypeTools.toString(t);
- var tval = switch( tstr ) {
- case "Int": AInt;
- case "Float": AFloat;
- case "String": AString;
- case "Bool": ABool;
- default: haxe.macro.Context.error("Unsupported parameter type "+tstr+" for argument "+a.name, callb.pos);
- }
- var tname = ""+tval;
- args.push(macro { name : $v{a.name}, t : h2d.Console.ConsoleArg.$tname, opt : $v{a.opt} });
- }
- default:
- haxe.macro.Context.error(haxe.macro.TypeTools.toString(et.t)+" should be a function", callb.pos);
- }
- return macro $ethis.addCommand($name,null,$a{args},$callb);
- }
- #if !macro
- /**
- Add an alias to an existing command.
- @param name Command alias.
- @param command Full command name to alias.
- **/
- public function addAlias( name, command ) {
- aliases.set(name, command);
- }
- /**
- Executes `commandLine` the same way the user would execute it.
- **/
- public function runCommand( commandLine : String ) {
- handleCommand(commandLine);
- }
- override function onAdd() {
- super.onAdd();
- @:privateAccess getScene().window.addEventTarget(onEvent);
- }
- override function onRemove() {
- @:privateAccess getScene().window.removeEventTarget(onEvent);
- super.onRemove();
- }
- function onEvent( e : hxd.Event ) {
- switch( e.kind ) {
- case EWheel:
- if( logTxt.visible ) {
- logDY -= tf.font.lineHeight * e.wheelDelta * 3;
- if( logDY < 0 ) logDY = 0;
- if( logDY > logTxt.textHeight ) logDY = logTxt.textHeight;
- e.propagate = false;
- }
- case ETextInput:
- if( e.charCode == shortKeyChar && !bg.visible )
- show();
- default:
- }
- }
- function showHelp( ?command : String ) {
- var all;
- if( command == null ) {
- all = Lambda.array( { iterator : function() return commands.keys() } );
- all.sort(Reflect.compare);
- all.remove("help");
- all.push("help");
- } else {
- if( aliases.exists(command) ) command = aliases.get(command);
- if( !commands.exists(command) )
- throw 'Command not found "$command"';
- all = [command];
- }
- for( cmdName in all ) {
- var c = commands.get(cmdName);
- var str = String.fromCharCode(shortKeyChar) + cmdName;
- for( a in aliases.keys() )
- if( aliases.get(a) == cmdName )
- str += "|" + a;
- for( a in c.args ) {
- var astr = a.name;
- switch( a.t ) {
- case AInt, AFloat, AArray(_):
- astr += ":"+a.t.getName().substr(1);
- case AString:
- // nothing
- case AEnum(values):
- astr += "=" + values.join("|");
- case ABool:
- astr += "=0|1";
- }
- str += " " + (a.opt?"["+astr+"]":astr);
- }
- if( c.help != "" )
- str += " : " + c.help;
- log(str);
- }
- }
- /**
- Checks if the Console is currently shown.
- **/
- public function isActive() {
- return bg.visible;
- }
- /**
- Hides the Console.
- **/
- public function hide() {
- bg.visible = false;
- tf.text = "";
- hintTxt.text = "";
- tf.cursorIndex = -1;
- }
- /**
- Shows and focuses the Console.
- **/
- public function show() {
- bg.visible = true;
- tf.focus();
- tf.cursorIndex = tf.text.length;
- logIndex = -1;
- }
- function getCommandSuggestion(cmd : String) : String {
- var hadShortKey = false;
- if (cmd.charCodeAt(0) == shortKeyChar) {
- hadShortKey = true;
- cmd = cmd.substr(1);
- }
- if (cmd == "") {
- return "";
- }
- var lowCmd = cmd.toLowerCase();
- var closestCommand = "";
- var commandNames = commands.keys();
- for (command in commandNames) {
- if (command.toLowerCase().indexOf(lowCmd) == 0) {
- if (closestCommand == "" || closestCommand.length > command.length) {
- closestCommand = command;
- }
- }
- }
- if( aliases.exists(cmd) )
- closestCommand = cmd;
- if (hadShortKey && closestCommand != "")
- closestCommand = String.fromCharCode(shortKeyChar) + closestCommand;
- return closestCommand;
- }
- function handleKey( e : hxd.Event ) {
- if( !bg.visible )
- return;
- switch( e.keyCode ) {
- case Key.ENTER, Key.NUMPAD_ENTER:
- var cmd = tf.text;
- tf.text = "";
- hintTxt.text = "";
- if (autoComplete) {
- var suggestion = getCommandSuggestion(cmd);
- if (suggestion != "") {
- cmd = suggestion;
- }
- }
- handleCommand(cmd);
- if( !logTxt.visible ) bg.visible = false;
- e.cancel = true;
- return;
- case Key.TAB:
- if (autoComplete) {
- if (hintTxt.text != "") {
- tf.text = hintTxt.text + " ";
- tf.cursorIndex = tf.text.length;
- }
- }
- case Key.ESCAPE:
- hide();
- case Key.UP:
- if(logs.length == 0 || logIndex == 0) return;
- if(logIndex == -1) {
- curCmd = tf.text;
- logIndex = logs.length - 1;
- }
- else {
- var curLog = logs[logIndex];
- while(curLog == logs[logIndex] && logIndex > 0)
- logIndex--;
- }
- tf.text = logs[logIndex];
- tf.cursorIndex = tf.text.length;
- case Key.DOWN:
- if(tf.text == curCmd) return;
- var curLog = logs[logIndex];
- while(curLog == logs[logIndex] && logIndex < logs.length - 1)
- logIndex++;
- if(logIndex == logs.length - 1) {
- tf.text = curCmd == null ? "" : curCmd;
- tf.cursorIndex = tf.text.length;
- logIndex = -1;
- return;
- }
- tf.text = logs[logIndex];
- tf.cursorIndex = tf.text.length;
- }
- }
- function handleCmdChange() {
- hintTxt.visible = autoComplete;
- if (autoComplete) {
- hintTxt.text = getCommandSuggestion(tf.text);
- } else {
- hintTxt.text = "";
- }
- }
- function handleCommand( command : String ) {
- command = StringTools.trim(command);
- if( command.charCodeAt(0) == shortKeyChar ) command = command.substr(1);
- if( command == "" ) {
- hide();
- return;
- }
- logs.push(command);
- logIndex = -1;
- var args = [];
- var c = '';
- var i = 0;
- function readString(endChar:String) {
- var string = '';
- while (i < command.length) {
- c = command.charAt(++i);
- if (c == endChar) {
- ++i;
- return string;
- }
- string += c;
- }
- return null;
- }
- inline function skipSpace() {
- c = command.charAt(i);
- while (c == ' ' || c == '\t') {
- c = command.charAt(++i);
- }
- --i;
- }
- var last = '';
- while (i < command.length) {
- c = command.charAt(i);
- switch (c) {
- case ' ' | '\t':
- skipSpace();
- args.push(last);
- last = '';
- case "'" | '"':
- var string = readString(c);
- if (string == null) {
- log('Bad formated string', errorColor);
- return;
- }
- last = string;
- if (i < command.length - 1) {
- args.push(string);
- last = '';
- }
- skipSpace();
- default:
- last += c;
- }
- ++i;
- }
- args.push(last);
- var cmdName = args[0];
- if( aliases.exists(cmdName) ) cmdName = aliases.get(cmdName);
- var cmd = commands.get(cmdName);
- if( cmd == null ) {
- log('Unknown command "${cmdName}"',errorColor);
- return;
- }
- function parseArgument( v : String, t: ConsoleArg, name : String, loneArg = false ): Dynamic {
- switch( t ) {
- case AInt:
- var i = Std.parseInt(v);
- if( i == null ) {
- log('$v should be Int for argument $name',errorColor);
- return null;
- }
- return i;
- case AFloat:
- var f = Std.parseFloat(v);
- if( Math.isNaN(f) ) {
- log('$v should be Float for argument $name',errorColor);
- return null;
- }
- return f;
- case ABool:
- switch( v ) {
- case "true", "1": return true;
- case "false", "0": return false;
- default:
- log('$v should be Bool for argument $name',errorColor);
- return null;
- }
- case AString:
- // if we take a single string, let's pass the whole args (allows spaces)
- return loneArg ? StringTools.trim(command.substr(args[0].length)) : v;
- case AEnum(values):
- var found = false;
- for( v2 in values ) {
- if( v == v2 )
- return v2;
- }
- log('$v should be [${values.join("|")}] for argument $name', errorColor);
- return null;
- case AArray(t):
- log('Cannot have nested arrays for argument $name');
- return null;
- }
- return null;
- }
- var vargs = new Array<Dynamic>();
- for( i in 0...cmd.args.length ) {
- var a = cmd.args[i];
- switch( a.t ) {
- case AArray(t):
- if (i != cmd.args.length - 1) {
- log('Array ${a.name} should be last argument',errorColor);
- return;
- }
- var arr = [];
- for (j in (i + 1)...args.length) {
- var v = args[j];
- var parsed = parseArgument(v, t, a.name);
- if (parsed == null)
- return;
- arr.push(parsed);
- }
- vargs.push(arr);
- default:
- var v = args[i + 1];
- if( v == null ) {
- if( a.opt ) {
- vargs.push(null);
- continue;
- }
- log('Missing argument ${a.name}',errorColor);
- return;
- }
- var parsed = parseArgument(v, a.t, a.name, cmd.args.length == 1);
- if (parsed == null)
- return;
- vargs.push(parsed);
- }
- }
- doCall(cmd.callb, vargs);
- }
- function doCall( callb : Dynamic, vargs : Array<Dynamic> ) {
- try {
- Reflect.callMethod(null, callb, vargs);
- } catch( e : String ) {
- log('ERROR $e', errorColor);
- }
- }
- /**
- Print to the console log.
- @param text The text to show in the log message.
- @param color Optional custom text color.
- **/
- public function log( text : String, ?color ) {
- if( color == null ) color = tf.textColor;
- var oldH = logTxt.textHeight;
- logTxt.text = logTxt.text + '<font color="#${StringTools.hex(color&0xFFFFFF,6)}">${StringTools.htmlEscape(text)}</font><br/>';
- if( logDY != 0 ) logDY += logTxt.textHeight - oldH;
- logTxt.alpha = 1;
- logTxt.visible = true;
- lastLogTime = haxe.Timer.stamp();
- }
- override function sync(ctx:h2d.RenderContext) {
- var scene = ctx.scene;
- if( scene != null ) {
- x = 0;
- y = scene.height - height*scaleY;
- width = scene.width;
- tf.maxWidth = width;
- bg.tile.scaleToSize(width, height);
- }
- var log = logTxt;
- if( log.visible ) {
- log.y = bg.y - log.textHeight + logDY;
- var dt = haxe.Timer.stamp() - lastLogTime;
- if( dt > HIDE_LOG_TIMEOUT && !bg.visible ) {
- log.alpha -= ctx.elapsedTime * 4;
- if( log.alpha <= 0 )
- log.visible = false;
- }
- }
- super.sync(ctx);
- }
- #end
- }
|