SimpleWindow use KissInputText to handle inputtext click for focus

This commit is contained in:
2022-09-18 21:12:35 +00:00
parent 6b568cadf0
commit c547922d87
3 changed files with 1014 additions and 16 deletions

View File

@@ -0,0 +1,986 @@
package kiss_flixel;
import flash.errors.Error;
import flash.events.KeyboardEvent;
import flash.geom.Rectangle;
import flixel.addons.ui.FlxUI.NamedString;
import flixel.FlxG;
import flixel.FlxSprite;
import flixel.math.FlxPoint;
import flixel.math.FlxRect;
import flixel.text.FlxText;
import flixel.util.FlxColor;
import flixel.util.FlxDestroyUtil;
import flixel.util.FlxTimer;
import flixel.FlxCamera;
/**
* KissInputText based on
* FlxInputText v1.11, ported to Haxe
* @author larsiusprime, (Lars Doucet)
* @link http://github.com/haxeflixel/flixel-ui
*
* FlxInputText v1.10, Input text field extension for Flixel
* @author Gama11, Mr_Walrus, nitram_cero (Martín Sebastián Wain)
* @link http://forums.flixel.org/index.php/topic,272.0.html
*
* Copyright (c) 2009 Martín Sebastián Wain
* License: Creative Commons Attribution 3.0 United States
* @link http://creativecommons.org/licenses/by/3.0/us/
*/
class KissInputText extends FlxText
{
public static inline var NO_FILTER:Int = 0;
public static inline var ONLY_ALPHA:Int = 1;
public static inline var ONLY_NUMERIC:Int = 2;
public static inline var ONLY_ALPHANUMERIC:Int = 3;
public static inline var CUSTOM_FILTER:Int = 4;
public static inline var ALL_CASES:Int = 0;
public static inline var UPPER_CASE:Int = 1;
public static inline var LOWER_CASE:Int = 2;
public static inline var BACKSPACE_ACTION:String = "backspace"; // press backspace
public static inline var DELETE_ACTION:String = "delete"; // press delete
public static inline var ENTER_ACTION:String = "enter"; // press enter
public static inline var INPUT_ACTION:String = "input"; // manually edit
/**
* This regular expression will filter out (remove) everything that matches.
* Automatically sets filterMode = FlxInputText.CUSTOM_FILTER.
*/
public var customFilterPattern(default, set):EReg;
function set_customFilterPattern(cfp:EReg)
{
customFilterPattern = cfp;
filterMode = CUSTOM_FILTER;
return customFilterPattern;
}
/**
* A function called whenever the value changes from user input, or enter is pressed
*/
public var callback:String->String->Void;
/**
* Whether or not the textbox has a background
*/
public var background:Bool = false;
/**
* The caret's color. Has the same color as the text by default.
*/
public var caretColor(default, set):Int;
function set_caretColor(i:Int):Int
{
caretColor = i;
dirty = true;
return caretColor;
}
public var caretWidth(default, set):Int = 1;
function set_caretWidth(i:Int):Int
{
caretWidth = i;
dirty = true;
return caretWidth;
}
public var params(default, set):Array<Dynamic>;
/**
* Whether or not the textfield is a password textfield
*/
public var passwordMode(get, set):Bool;
/**
* Whether or not the text box is the active object on the screen.
*/
public var hasFocus(default, set):Bool = false;
/**
* The position of the selection cursor. An index of 0 means the carat is before the character at index 0.
*/
public var caretIndex(default, set):Int = 0;
/**
* callback that is triggered when this text field gets focus
* @since 2.2.0
*/
public var focusGained:Void->Void;
/**
* callback that is triggered when this text field loses focus
* @since 2.2.0
*/
public var focusLost:Void->Void;
/**
* The Case that's being enforced. Either ALL_CASES, UPPER_CASE or LOWER_CASE.
*/
public var forceCase(default, set):Int = ALL_CASES;
/**
* Set the maximum length for the field (e.g. "3"
* for Arcade type hi-score initials). 0 means unlimited.
*/
public var maxLength(default, set):Int = 0;
/**
* Change the amount of lines that are allowed.
*/
public var lines(default, set):Int;
/**
* Defines what text to filter. It can be NO_FILTER, ONLY_ALPHA, ONLY_NUMERIC, ONLY_ALPHA_NUMERIC or CUSTOM_FILTER
* (Remember to append "FlxInputText." as a prefix to those constants)
*/
public var filterMode(default, set):Int = NO_FILTER;
/**
* The color of the fieldBorders
*/
public var fieldBorderColor(default, set):Int = FlxColor.BLACK;
/**
* The thickness of the fieldBorders
*/
public var fieldBorderThickness(default, set):Int = 1;
/**
* The color of the background of the textbox.
*/
public var backgroundColor(default, set):Int = FlxColor.WHITE;
/**
* A FlxSprite representing the background sprite
*/
private var backgroundSprite:FlxSprite;
/**
* A timer for the flashing caret effect.
*/
private var _caretTimer:FlxTimer;
/**
* A FlxSprite representing the flashing caret when editing text.
*/
private var caret:FlxSprite;
/**
* A FlxSprite representing the fieldBorders.
*/
private var fieldBorderSprite:FlxSprite;
/**
* The left- and right- most fully visible character indeces
*/
private var _scrollBoundIndeces:{left:Int, right:Int} = {left: 0, right: 0};
// workaround to deal with non-availability of getCharIndexAtPoint or getCharBoundaries on cpp/neko targets
private var _charBoundaries:Array<FlxRect>;
/**
* Stores last input text scroll.
*/
private var lastScroll:Int;
/**
* @param X The X position of the text.
* @param Y The Y position of the text.
* @param Width The width of the text object (height is determined automatically).
* @param Text The actual text you would like to display initially.
* @param size Initial size of the font
* @param TextColor The color of the text
* @param BackgroundColor The color of the background (FlxColor.TRANSPARENT for no background color)
* @param EmbeddedFont Whether this text field uses embedded fonts or not
*/
public function new(X:Float = 0, Y:Float = 0, Width:Int = 150, ?Text:String, size:Int = 8, TextColor:Int = FlxColor.BLACK,
BackgroundColor:Int = FlxColor.WHITE, EmbeddedFont:Bool = true)
{
super(X, Y, Width, Text, size, EmbeddedFont);
backgroundColor = BackgroundColor;
if (BackgroundColor != FlxColor.TRANSPARENT)
{
background = true;
}
color = TextColor;
caretColor = TextColor;
caret = new FlxSprite();
caret.makeGraphic(caretWidth, Std.int(size + 2));
_caretTimer = new FlxTimer();
caretIndex = 0;
hasFocus = false;
if (background)
{
fieldBorderSprite = new FlxSprite(X, Y);
backgroundSprite = new FlxSprite(X, Y);
}
lines = 1;
FlxG.stage.addEventListener(KeyboardEvent.KEY_DOWN, onKeyDown);
if (Text == null)
{
Text = "";
}
text = Text; // ensure set_text is called to avoid bugs (like not preparing _charBoundaries on sys target, making it impossible to click)
calcFrame();
}
/**
* Clean up memory
*/
override public function destroy():Void
{
FlxG.stage.removeEventListener(KeyboardEvent.KEY_DOWN, onKeyDown);
backgroundSprite = FlxDestroyUtil.destroy(backgroundSprite);
fieldBorderSprite = FlxDestroyUtil.destroy(fieldBorderSprite);
callback = null;
#if sys
if (_charBoundaries != null)
{
while (_charBoundaries.length > 0)
{
_charBoundaries.pop();
}
_charBoundaries = null;
}
#end
super.destroy();
}
/**
* Draw the caret in addition to the text.
*/
override public function draw():Void
{
drawSprite(fieldBorderSprite);
drawSprite(backgroundSprite);
super.draw();
// In case caretColor was changed
if (caretColor != caret.color || caret.height != size + 2)
{
caret.color = caretColor;
}
drawSprite(caret);
}
/**
* Helper function that makes sure sprites are drawn up even though they haven't been added.
* @param Sprite The Sprite to be drawn.
*/
private function drawSprite(Sprite:FlxSprite):Void
{
if (Sprite != null && Sprite.visible)
{
Sprite.scrollFactor = scrollFactor;
Sprite.cameras = cameras;
Sprite.draw();
}
}
/**
* Handles keypresses generated on the stage.
*/
private function onKeyDown(e:KeyboardEvent):Void
{
var key:Int = e.keyCode;
if (hasFocus)
{
// Do nothing for Shift, Ctrl, Esc, and flixel console hotkey
if (key == 16 || key == 17 || key == 220 || key == 27)
{
return;
}
// Left arrow
else if (key == 37)
{
if (caretIndex > 0)
{
caretIndex--;
text = text; // forces scroll update
}
}
// Right arrow
else if (key == 39)
{
if (caretIndex < text.length)
{
caretIndex++;
text = text; // forces scroll update
}
}
// End key
else if (key == 35)
{
caretIndex = text.length;
text = text; // forces scroll update
}
// Home key
else if (key == 36)
{
caretIndex = 0;
text = text;
}
// Backspace
else if (key == 8)
{
if (caretIndex > 0)
{
caretIndex--;
text = text.substring(0, caretIndex) + text.substring(caretIndex + 1);
onChange(BACKSPACE_ACTION);
}
}
// Delete
else if (key == 46)
{
if (text.length > 0 && caretIndex < text.length)
{
text = text.substring(0, caretIndex) + text.substring(caretIndex + 1);
onChange(DELETE_ACTION);
}
}
// Enter
else if (key == 13)
{
onChange(ENTER_ACTION);
}
// Actually add some text
else
{
if (e.charCode == 0) // non-printable characters crash String.fromCharCode
{
return;
}
var newText:String = filter(String.fromCharCode(e.charCode));
if (newText.length > 0 && (maxLength == 0 || (text.length + newText.length) < maxLength))
{
text = insertSubstring(text, newText, caretIndex);
caretIndex++;
onChange(INPUT_ACTION);
}
}
}
}
private function onChange(action:String):Void
{
if (callback != null)
{
callback(text, action);
}
}
/**
* Inserts a substring into a string at a specific index
*
* @param Insert The string to have something inserted into
* @param Insert The string to insert
* @param Index The index to insert at
* @return Returns the joined string for chaining.
*/
private function insertSubstring(Original:String, Insert:String, Index:Int):String
{
if (Index != Original.length)
{
Original = Original.substring(0, Index) + (Insert) + (Original.substring(Index));
}
else
{
Original = Original + (Insert);
}
return Original;
}
var reusablePoint:FlxPoint = new FlxPoint();
var reusableRect:FlxRect = new FlxRect();
/**
* Gets the index of the character in this box under the mouse cursor
* @return The index of the character.
* between 0 and the length of the text
*/
public function getCaretIndex(camera:FlxCamera):Int
{
#if FLX_MOUSE
var bounds = getScreenBounds(reusableRect, camera);
var mousePos = FlxG.mouse.getScreenPosition(camera, reusablePoint);
var hit = FlxPoint.get(mousePos.x - bounds.x, mousePos.y - bounds.y);
return getCharIndexAtPoint(hit.x, hit.y);
#else
return 0;
#end
}
private function getCharBoundaries(charIndex:Int):Rectangle
{
if (_charBoundaries != null && charIndex >= 0 && _charBoundaries.length > 0)
{
var r:Rectangle = new Rectangle();
if (charIndex >= _charBoundaries.length)
{
_charBoundaries[_charBoundaries.length - 1].copyToFlash(r);
}
else
{
_charBoundaries[charIndex].copyToFlash(r);
}
return r;
}
return null;
}
private override function set_text(Text:String):String
{
#if !js
if (textField != null)
{
lastScroll = textField.scrollH;
}
#end
var return_text:String = super.set_text(Text);
if (textField == null)
{
return return_text;
}
var numChars:Int = Text.length;
prepareCharBoundaries(numChars);
textField.text = "";
var textH:Float = 0;
var textW:Float = 0;
var lastW:Float = 0;
// Flash textFields have a "magic number" 2 pixel gutter all around
// It does not seem to vary with font, size, border, etc, and does not seem to be customizable.
// We simply reproduce this behavior here
var magicX:Float = 2;
var magicY:Float = 2;
for (i in 0...numChars)
{
textField.appendText(Text.substr(i, 1)); // add a character
textW = textField.textWidth; // count up total text width
if (i == 0)
{
textH = textField.textHeight; // count height after first char
}
_charBoundaries[i].x = magicX + lastW; // place x at end of last character
_charBoundaries[i].y = magicY; // place y at zero
_charBoundaries[i].width = (textW - lastW); // place width at (width so far) minus (last char's end point)
_charBoundaries[i].height = textH;
lastW = textW;
}
textField.text = Text;
onSetTextCheck();
return return_text;
}
private function getCharIndexAtPoint(X:Float, Y:Float):Int
{
var i:Int = 0;
#if !js
X += textField.scrollH + 2;
#end
// offset X according to text alignment when there is no scroll.
if (_charBoundaries != null && _charBoundaries.length > 0)
{
if (textField.textWidth <= textField.width)
{
switch (getAlignStr())
{
case RIGHT:
X = X - textField.width + textField.textWidth
;
case CENTER:
X = X - textField.width / 2 + textField.textWidth / 2
;
default:
}
}
}
// place caret at matching char position
if (_charBoundaries != null)
{
for (r in _charBoundaries)
{
if (X >= r.left && X <= r.right)
{
return i;
}
i++;
}
}
// place caret at rightmost position
if (_charBoundaries != null && _charBoundaries.length > 0)
{
if (X > textField.textWidth)
{
return _charBoundaries.length;
}
}
// place caret at leftmost position
return 0;
}
private function prepareCharBoundaries(numChars:Int):Void
{
if (_charBoundaries == null)
{
_charBoundaries = [];
}
if (_charBoundaries.length > numChars)
{
var diff:Int = _charBoundaries.length - numChars;
for (i in 0...diff)
{
_charBoundaries.pop();
}
}
for (i in 0...numChars)
{
if (_charBoundaries.length - 1 < i)
{
_charBoundaries.push(FlxRect.get(0, 0, 0, 0));
}
}
}
/**
* Called every time the text is changed (for both flash/cpp) to update scrolling, etc
*/
private function onSetTextCheck():Void
{
#if !js
var boundary:Rectangle = null;
if (caretIndex == -1)
{
boundary = getCharBoundaries(text.length - 1);
}
else
{
boundary = getCharBoundaries(caretIndex);
}
if (boundary != null)
{
// Checks if carret is out of textfield bounds
// if it is update scroll, otherwise maintain the same scroll as last check.
var diffW:Int = 0;
if (boundary.right > lastScroll + textField.width - 2)
{
diffW = -Std.int((textField.width - 2) - boundary.right); // caret to the right of textfield.
}
else if (boundary.left < lastScroll)
{
diffW = Std.int(boundary.left) - 2; // caret to the left of textfield
}
else
{
diffW = lastScroll; // no scroll change
}
#if !js
textField.scrollH = diffW;
#end
calcFrame();
}
#end
}
/**
* Draws the frame of animation for the input text.
*
* @param RunOnCpp Whether the frame should also be recalculated if we're on a non-flash target
*/
private override function calcFrame(RunOnCpp:Bool = false):Void
{
super.calcFrame(RunOnCpp);
if (fieldBorderSprite != null)
{
if (fieldBorderThickness > 0)
{
fieldBorderSprite.makeGraphic(Std.int(width + fieldBorderThickness * 2), Std.int(height + fieldBorderThickness * 2), fieldBorderColor);
fieldBorderSprite.x = x - fieldBorderThickness;
fieldBorderSprite.y = y - fieldBorderThickness;
}
else if (fieldBorderThickness == 0)
{
fieldBorderSprite.visible = false;
}
}
if (backgroundSprite != null)
{
if (background)
{
backgroundSprite.makeGraphic(Std.int(width), Std.int(height), backgroundColor);
backgroundSprite.x = x;
backgroundSprite.y = y;
}
else
{
backgroundSprite.visible = false;
}
}
if (caret != null)
{
// Generate the properly sized caret and also draw a border that matches that of the textfield (if a border style is set)
// borderQuality can be safely ignored since the caret is always a rectangle
var cw:Int = caretWidth; // Basic size of the caret
var ch:Int = Std.int(size + 2);
// Make sure alpha channels are correctly set
var borderC:Int = (0xff000000 | (borderColor & 0x00ffffff));
var caretC:Int = (0xff000000 | (caretColor & 0x00ffffff));
// Generate unique key for the caret so we don't cause weird bugs if someone makes some random flxsprite of this size and color
var caretKey:String = "caret" + cw + "x" + ch + "c:" + caretC + "b:" + borderStyle + "," + borderSize + "," + borderC;
switch (borderStyle)
{
case NONE:
// No border, just make the caret
caret.makeGraphic(cw, ch, caretC, false, caretKey);
caret.offset.x = caret.offset.y = 0;
case SHADOW:
// Shadow offset to the lower-right
cw += Std.int(borderSize);
ch += Std.int(borderSize); // expand canvas on one side for shadow
caret.makeGraphic(cw, ch, FlxColor.TRANSPARENT, false, caretKey); // start with transparent canvas
var r:Rectangle = new Rectangle(borderSize, borderSize, caretWidth, Std.int(size + 2));
caret.pixels.fillRect(r, borderC); // draw shadow
r.x = r.y = 0;
caret.pixels.fillRect(r, caretC); // draw caret
caret.offset.x = caret.offset.y = 0;
case OUTLINE_FAST, OUTLINE:
// Border all around it
cw += Std.int(borderSize * 2);
ch += Std.int(borderSize * 2); // expand canvas on both sides
caret.makeGraphic(cw, ch, borderC, false, caretKey); // start with borderColor canvas
var r = new Rectangle(borderSize, borderSize, caretWidth, Std.int(size + 2));
caret.pixels.fillRect(r, caretC); // draw caret
// we need to offset caret's drawing position since the caret is now larger than normal
caret.offset.x = caret.offset.y = borderSize;
}
// Update width/height so caret's dimensions match its pixels
caret.width = cw;
caret.height = ch;
caretIndex = caretIndex; // force this to update
}
}
/**
* Turns the caret on/off for the caret flashing animation.
*/
private function toggleCaret(timer:FlxTimer):Void
{
caret.visible = !caret.visible;
}
/**
* Checks an input string against the current
* filter and returns a filtered string
*/
private function filter(text:String):String
{
if (forceCase == UPPER_CASE)
{
text = text.toUpperCase();
}
else if (forceCase == LOWER_CASE)
{
text = text.toLowerCase();
}
if (filterMode != NO_FILTER)
{
var pattern:EReg;
switch (filterMode)
{
case ONLY_ALPHA:
pattern = ~/[^a-zA-Z]*/g;
case ONLY_NUMERIC:
pattern = ~/[^0-9]*/g;
case ONLY_ALPHANUMERIC:
pattern = ~/[^a-zA-Z0-9]*/g;
case CUSTOM_FILTER:
pattern = customFilterPattern;
default:
throw new Error("FlxInputText: Unknown filterMode (" + filterMode + ")");
}
text = pattern.replace(text, "");
}
return text;
}
private function set_params(p:Array<Dynamic>):Array<Dynamic>
{
params = p;
if (params == null)
{
params = [];
}
var namedValue:NamedString = {name: "value", value: text};
params.push(namedValue);
return p;
}
private override function set_x(X:Float):Float
{
if ((fieldBorderSprite != null) && fieldBorderThickness > 0)
{
fieldBorderSprite.x = X - fieldBorderThickness;
}
if ((backgroundSprite != null) && background)
{
backgroundSprite.x = X;
}
return super.set_x(X);
}
private override function set_y(Y:Float):Float
{
if ((fieldBorderSprite != null) && fieldBorderThickness > 0)
{
fieldBorderSprite.y = Y - fieldBorderThickness;
}
if ((backgroundSprite != null) && background)
{
backgroundSprite.y = Y;
}
return super.set_y(Y);
}
private function set_hasFocus(newFocus:Bool):Bool
{
if (newFocus)
{
if (hasFocus != newFocus)
{
_caretTimer = new FlxTimer().start(0.5, toggleCaret, 0);
caret.visible = true;
}
}
else
{
// Graphics
caret.visible = false;
if (_caretTimer != null)
{
_caretTimer.cancel();
}
}
if (newFocus != hasFocus)
{
calcFrame();
}
return hasFocus = newFocus;
}
private function getAlignStr():FlxTextAlign
{
var alignStr:FlxTextAlign = LEFT;
if (_defaultFormat != null && _defaultFormat.align != null)
{
alignStr = alignment;
}
return alignStr;
}
private function set_caretIndex(newCaretIndex:Int):Int
{
var offx:Float = 0;
var alignStr:FlxTextAlign = getAlignStr();
switch (alignStr)
{
case RIGHT:
offx = textField.width - 2 - textField.textWidth - 2;
if (offx < 0)
offx = 0; // hack, fix negative offset.
case CENTER:
#if !js
offx = (textField.width - 2 - textField.textWidth) / 2 + textField.scrollH / 2;
#end
if (offx <= 1)
offx = 0; // hack, fix ofset rounding alignment.
default:
offx = 0;
}
caretIndex = newCaretIndex;
// If caret is too far to the right something is wrong
if (caretIndex > (text.length + 1))
{
caretIndex = -1;
}
// Caret is OK, proceed to position
if (caretIndex != -1)
{
var boundaries:Rectangle = null;
// Caret is not to the right of text
if (caretIndex < text.length)
{
boundaries = getCharBoundaries(caretIndex);
if (boundaries != null)
{
caret.x = offx + boundaries.left + x;
caret.y = boundaries.top + y;
}
}
// Caret is to the right of text
else
{
boundaries = getCharBoundaries(caretIndex - 1);
if (boundaries != null)
{
caret.x = offx + boundaries.right + x;
caret.y = boundaries.top + y;
}
// Text box is empty
else if (text.length == 0)
{
// 2 px gutters
caret.x = x + offx + 2;
caret.y = y + 2;
}
}
}
#if !js
caret.x -= textField.scrollH;
#end
// Make sure the caret doesn't leave the textfield on single-line input texts
if ((lines == 1) && (caret.x + caret.width) > (x + width))
{
caret.x = x + width - 2;
}
return caretIndex;
}
private function set_forceCase(Value:Int):Int
{
forceCase = Value;
text = filter(text);
return forceCase;
}
override private function set_size(Value:Int):Int
{
super.size = Value;
caret.makeGraphic(1, Std.int(size + 2));
return Value;
}
private function set_maxLength(Value:Int):Int
{
maxLength = Value;
if (text.length > maxLength)
{
text = text.substring(0, maxLength);
}
return maxLength;
}
private function set_lines(Value:Int):Int
{
if (Value == 0)
return 0;
if (Value > 1)
{
textField.wordWrap = true;
textField.multiline = true;
}
else
{
textField.wordWrap = false;
textField.multiline = false;
}
lines = Value;
calcFrame();
return lines;
}
private function get_passwordMode():Bool
{
return textField.displayAsPassword;
}
private function set_passwordMode(value:Bool):Bool
{
textField.displayAsPassword = value;
calcFrame();
return value;
}
private function set_filterMode(Value:Int):Int
{
filterMode = Value;
text = filter(text);
return filterMode;
}
private function set_fieldBorderColor(Value:Int):Int
{
fieldBorderColor = Value;
calcFrame();
return fieldBorderColor;
}
private function set_fieldBorderThickness(Value:Int):Int
{
fieldBorderThickness = Value;
calcFrame();
return fieldBorderThickness;
}
private function set_backgroundColor(Value:Int):Int
{
backgroundColor = Value;
calcFrame();
return backgroundColor;
}
}