refactoring
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
-main Tests
|
||||||
|
-cp src
|
||||||
|
-cp test
|
||||||
|
-D dump=pretty
|
||||||
|
-neko bin/test.n
|
||||||
|
-lib buddy
|
||||||
+7
-108
@@ -1,129 +1,28 @@
|
|||||||
package bindx;
|
package bindx;
|
||||||
|
|
||||||
#if macro
|
|
||||||
import bindx.GenericError;
|
|
||||||
import haxe.macro.Expr;
|
import haxe.macro.Expr;
|
||||||
import haxe.macro.Type;
|
import bindx.macro.BindMacros;
|
||||||
import haxe.macro.Context;
|
|
||||||
|
|
||||||
using haxe.macro.Tools;
|
@:access(bindx.macro.BindMacros)
|
||||||
using bindx.MetaUtils;
|
|
||||||
using Lambda;
|
|
||||||
#end
|
|
||||||
|
|
||||||
|
|
||||||
@:access(bindx.BindMacros)
|
|
||||||
class Bind {
|
class Bind {
|
||||||
|
|
||||||
@:noUsing macro static public function bind(field:Expr, listener:Expr):Expr {
|
@:noUsing macro static public function bind(field:Expr, listener:Expr):Expr {
|
||||||
return internalBind(field, listener, true);
|
return BindMacros.internalBind(field, listener, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
@:noUsing macro static public function bindTo(field:Expr, target:Expr):Expr {
|
@:noUsing macro static public function bindTo(field:Expr, target:Expr):Expr {
|
||||||
return internalBindTo(field, target);
|
return BindMacros.internalBindTo(field, target);
|
||||||
}
|
}
|
||||||
|
|
||||||
@:noUsing macro static public function unbind(field:Expr, ?listener:Expr):Expr {
|
@:noUsing macro static public function unbind(field:Expr, ?listener:Expr):Expr {
|
||||||
return internalBind(field, listener, false);
|
return BindMacros.internalBind(field, listener, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@:noUsing macro static public function notify(field:Expr, ?oldValue:Expr, ?newValue:Expr):Expr {
|
@:noUsing macro static public function notify(field:Expr, ?oldValue:Expr, ?newValue:Expr):Expr {
|
||||||
return internalNotify(field, oldValue, newValue);
|
return BindMacros.internalNotify(field, oldValue, newValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
@:noUsing macro static public function unbindAll(object:ExprOf<IBindable>):Expr {
|
@:noUsing macro static public function unbindAll(object:ExprOf<IBindable>):Expr {
|
||||||
return internalUnbindAll(object);
|
return BindMacros.internalUnbindAll(object);
|
||||||
}
|
|
||||||
|
|
||||||
#if macro
|
|
||||||
|
|
||||||
static inline function internalBind(field:Expr, listener:Expr, doBind:Bool):Expr {
|
|
||||||
var fieldData = warnCheckField(field);
|
|
||||||
return if (doBind) BindMacros.bindingSignalProvider.getClassFieldBindExpr(fieldData.e, fieldData.field, listener);
|
|
||||||
else BindMacros.bindingSignalProvider.getClassFieldUnbindExpr(fieldData.e, fieldData.field, listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
static inline function internalBindTo(field:Expr, target:Expr):Expr {
|
|
||||||
var fieldData = warnCheckField(field);
|
|
||||||
return BindMacros.bindingSignalProvider.getClassFieldBindToExpr(fieldData.e, fieldData.field, target);
|
|
||||||
}
|
|
||||||
|
|
||||||
static inline function internalNotify(field:Expr, ?oldValue:Expr, ?newValue:Expr):Expr {
|
|
||||||
var fieldData = warnCheckField(field);
|
|
||||||
return BindMacros.bindingSignalProvider.getClassFieldChangedExpr(fieldData.e, fieldData.field, oldValue, newValue);
|
|
||||||
}
|
|
||||||
|
|
||||||
static inline function internalUnbindAll(object:ExprOf<IBindable>):Expr {
|
|
||||||
var type = Context.typeof(object).follow();
|
|
||||||
if (!isBindable(type.getClass())) {
|
|
||||||
Context.error('\'${object.toString()}\' must be bindx.IBindable', object.pos);
|
|
||||||
}
|
|
||||||
return BindMacros.bindingSignalProvider.getUnbindAllExpr(object, type);
|
|
||||||
}
|
|
||||||
|
|
||||||
static inline function warnCheckField(field:Expr):{e:Expr, field:ClassField} {
|
|
||||||
var res = null;
|
|
||||||
try {
|
|
||||||
res = checkField(field);
|
|
||||||
if (res.error != null) res.error.contextError();
|
|
||||||
} catch (e:FatalError) {
|
|
||||||
e.contextFatal();
|
|
||||||
} catch (e:GenericError) {
|
|
||||||
e.contextError();
|
|
||||||
};
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
static function checkField(f:Expr):{e:Expr, field:ClassField, error:GenericError} {
|
|
||||||
var error:GenericError;
|
|
||||||
switch (f.expr) {
|
|
||||||
case EField(e, field):
|
|
||||||
var type = Context.typeof(e);
|
|
||||||
var classType = switch (type) { case TInst(c, _): c.get(); case _: null; };
|
|
||||||
if (classType == null) {
|
|
||||||
error = new FatalError('Type \'${e.toString()}\' is unknown', e.pos);
|
|
||||||
return {e:f, field:null, error:error};
|
|
||||||
}
|
|
||||||
if (!isBindable(classType)) {
|
|
||||||
error = new GenericError('\'${e.toString()}\' must be bindx.IBindable', e.pos);
|
|
||||||
}
|
|
||||||
|
|
||||||
var field = classType.findField(field, null);
|
|
||||||
if (field == null) {
|
|
||||||
throw new FatalError('\'${e.toString()}.${field.name}\' expected', field.pos);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!field.hasBindableMeta()) {
|
|
||||||
error = new GenericError('\'${e.toString()}.${field.name}\' is not bindable', field.pos);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {e:e, field:field, error:error};
|
|
||||||
|
|
||||||
case EConst(CIdent(_)):
|
|
||||||
return {e:f, field:null, error:new GenericError('Can\'t bind \'${f.toString()}\'. Please use \'this.${f.toString()}\'', f.pos)};
|
|
||||||
|
|
||||||
case _:
|
|
||||||
}
|
|
||||||
return {e:f, field:null, error:new GenericError('\'${f.toString()}\' is not bindable', f.pos)};
|
|
||||||
}
|
|
||||||
|
|
||||||
static inline function isBindable(classType:ClassType):Bool {
|
|
||||||
var check = [classType];
|
|
||||||
var res = false;
|
|
||||||
while (check.length > 0 && !res) {
|
|
||||||
var t = check.shift();
|
|
||||||
while (t != null) {
|
|
||||||
if (t.module == "bindx.IBindable" && t.name == "IBindable") {
|
|
||||||
res = true;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
for (it in t.interfaces)
|
|
||||||
check.push(it.t.get());
|
|
||||||
t = t.superClass != null ? t.superClass.t.get() : null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return res;
|
|
||||||
}
|
|
||||||
#end
|
|
||||||
}
|
|
||||||
+6
-342
@@ -1,365 +1,29 @@
|
|||||||
package bindx;
|
package bindx;
|
||||||
|
|
||||||
#if macro
|
|
||||||
|
|
||||||
import bindx.GenericError;
|
|
||||||
import haxe.macro.Expr;
|
import haxe.macro.Expr;
|
||||||
import haxe.macro.MacroStringTools;
|
|
||||||
import haxe.macro.Type;
|
|
||||||
import haxe.macro.Context;
|
import haxe.macro.Context;
|
||||||
import haxe.macro.Printer;
|
import bindx.macro.BindExtMacros;
|
||||||
|
|
||||||
using Lambda;
|
|
||||||
using StringTools;
|
|
||||||
using haxe.macro.Tools;
|
using haxe.macro.Tools;
|
||||||
|
|
||||||
typedef FieldExpr = {
|
@:access(bindx.macro.BindxExtMacro)
|
||||||
var field:ClassField;
|
|
||||||
var bindable:Bool;
|
|
||||||
var e:Expr;
|
|
||||||
@:optional var params:Array<Expr>;
|
|
||||||
}
|
|
||||||
|
|
||||||
typedef Chain = {
|
|
||||||
var init:Array<Expr>;
|
|
||||||
var bind:Array<Expr>;
|
|
||||||
var unbind:Array<Expr>;
|
|
||||||
var expr:Expr;
|
|
||||||
}
|
|
||||||
#end
|
|
||||||
|
|
||||||
@:access(bindx.BindMacros)
|
|
||||||
@:access(bindx.Bind)
|
|
||||||
class BindExt {
|
class BindExt {
|
||||||
|
|
||||||
@:noUsing macro static public function expr<T>(expr:ExprOf<T>, listener:ExprOf<Null<T>->Null<T>->Void>):ExprOf<Void->Void> {
|
@:noUsing macro static public function expr<T>(expr:ExprOf<T>, listener:ExprOf<Null<T>->Null<T>->Void>):ExprOf<Void->Void> {
|
||||||
return internalBindExpr(expr, listener);
|
return BindxExtMacro.internalBindExpr(expr, listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
@:noUsing macro static public function exprTo<T>(expr:ExprOf<T>, target:ExprOf<T>):ExprOf<Void->Void> {
|
@:noUsing macro static public function exprTo<T>(expr:ExprOf<T>, target:ExprOf<T>):ExprOf<Void->Void> {
|
||||||
var type = Context.typeof(expr).toComplexType();
|
var type = Context.typeof(expr).toComplexType();
|
||||||
return internalBindExpr(expr, macro function (_, to:Null<$type>) $target = to);
|
return BindxExtMacro.internalBindExpr(expr, macro function (_, to:Null<$type>) $target = to);
|
||||||
}
|
}
|
||||||
|
|
||||||
@:noUsing macro static public function chain<T>(expr:ExprOf<T>, listener:Expr):ExprOf<Void->Void> {
|
@:noUsing macro static public function chain<T>(expr:ExprOf<T>, listener:Expr):ExprOf<Void->Void> {
|
||||||
return internalBindChain(expr, listener);
|
return BindxExtMacro.internalBindChain(expr, listener);
|
||||||
}
|
}
|
||||||
|
|
||||||
@:noUsing macro static public function chainTo<T>(expr:ExprOf<T>, target:ExprOf<T>):ExprOf<Void->Void> {
|
@:noUsing macro static public function chainTo<T>(expr:ExprOf<T>, target:ExprOf<T>):ExprOf<Void->Void> {
|
||||||
var type = Context.typeof(expr).toComplexType();
|
var type = Context.typeof(expr).toComplexType();
|
||||||
return internalBindChain(expr, macro function (_, to:Null<$type>) $target = to);
|
return BindxExtMacro.internalBindChain(expr, macro function (_, to:Null<$type>) $target = to);
|
||||||
}
|
|
||||||
|
|
||||||
#if macro
|
|
||||||
|
|
||||||
static inline function internalBindChain(expr:Expr, listener:Expr):Expr {
|
|
||||||
var zeroListener = listenerName(0, "");
|
|
||||||
var chain = null;
|
|
||||||
try { chain = warnPrepareChain(expr); } catch (e:GenericError) e.contextError();
|
|
||||||
|
|
||||||
var res = macro (function ($zeroListener):Void->Void
|
|
||||||
$b { chain.init.concat(chain.bind).concat([(macro var res = function ():Void $b { chain.unbind }), macro return res]) }
|
|
||||||
)($listener);
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
static inline function unwrapFormatedString(expr:Expr):Expr {
|
|
||||||
return if (MacroStringTools.isFormatExpr(expr)) {
|
|
||||||
var f = switch (expr.expr) {
|
|
||||||
case EConst(CString(s)): s;
|
|
||||||
case _: null;
|
|
||||||
}
|
|
||||||
if (f != null) MacroStringTools.formatString(f, expr.pos) else expr;
|
|
||||||
} else expr;
|
|
||||||
}
|
|
||||||
|
|
||||||
static function internalBindExpr(expr:Expr, listener:Expr):Expr {
|
|
||||||
var type = Context.typeof(expr).toComplexType();
|
|
||||||
var listenerNameExpr = macro listener;
|
|
||||||
var fieldListenerName = "fieldListener";
|
|
||||||
var fieldListenerNameExpr = macro $i{fieldListenerName};
|
|
||||||
var methodListenerName = "methodListener";
|
|
||||||
var methodListenerNameExpr = macro $i{methodListenerName};
|
|
||||||
var chain:Chain = { init:[], bind:[], unbind:[], expr:expr };
|
|
||||||
var binded:Map<String, {prebind:Expr, c:Chain}> = new Map();
|
|
||||||
|
|
||||||
var prefix = 0;
|
|
||||||
function findChain(expr:Expr) {
|
|
||||||
var isChain;
|
|
||||||
expr = unwrapFormatedString(expr);
|
|
||||||
var e = expr;
|
|
||||||
do switch (e.expr) {
|
|
||||||
case EField(le, _) | ECall(le, _):
|
|
||||||
isChain = true;
|
|
||||||
e = le;
|
|
||||||
case _:
|
|
||||||
isChain = false;
|
|
||||||
} while (isChain);
|
|
||||||
var doBind = e != expr;
|
|
||||||
if (doBind) {
|
|
||||||
var key = expr.toString();
|
|
||||||
for (k in binded.keys()) if (k.startsWith(key)) {
|
|
||||||
doBind = false;
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (doBind) {
|
|
||||||
var pre = '_${prefix++}';
|
|
||||||
var zeroListener = listenerName(0, pre);
|
|
||||||
var c = null;
|
|
||||||
try {
|
|
||||||
c = warnPrepareChain(expr, pre, true);
|
|
||||||
} catch (e:GenericError) {
|
|
||||||
Warn.w('${expr.toString()} is not bindable.', e.pos, WarnPriority.ALL);
|
|
||||||
}
|
|
||||||
if (c != null) {
|
|
||||||
var key = c.expr.toString();
|
|
||||||
if (!binded.exists(key)) {
|
|
||||||
var ecall = switch (c.expr.expr) {
|
|
||||||
case EField(e, field):
|
|
||||||
var type = Context.typeof(e);
|
|
||||||
var classRef = type.getClass();
|
|
||||||
var field = classRef.findField(field);
|
|
||||||
field.kind.match(FMethod(_));
|
|
||||||
case _: false;
|
|
||||||
}
|
|
||||||
var prebind = macro var $zeroListener = ${ecall ? methodListenerNameExpr : fieldListenerNameExpr};
|
|
||||||
binded.set(key, {prebind:prebind, c:c});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
expr.iter(findChain);
|
|
||||||
}
|
|
||||||
findChain(expr);
|
|
||||||
|
|
||||||
var keys = [for (k in binded.keys()) k];
|
|
||||||
var i = 0;
|
|
||||||
while (i < keys.length) {
|
|
||||||
var k = keys[i];
|
|
||||||
var remove = false;
|
|
||||||
var j = i;
|
|
||||||
while (!remove && ++j < keys.length) remove = keys[j].startsWith(k);
|
|
||||||
if (remove) keys = keys.splice(i, 1); else i++;
|
|
||||||
}
|
|
||||||
|
|
||||||
var msg = [];
|
|
||||||
for (k in keys) {
|
|
||||||
var data = binded.get(k);
|
|
||||||
msg.push(data.c.expr.toString());
|
|
||||||
chain.bind.unshift(data.prebind);
|
|
||||||
var c = data.c;
|
|
||||||
chain.init = chain.init.concat(c.init);
|
|
||||||
chain.bind = chain.bind.concat(c.bind);
|
|
||||||
chain.unbind = chain.unbind.concat(c.unbind);
|
|
||||||
}
|
|
||||||
Warn.w('Bind \'${msg.join("', '")}\'', expr.pos, WarnPriority.INFO);
|
|
||||||
|
|
||||||
var zeroListener = listenerName(0, "");
|
|
||||||
var zeroValue = 'value0';
|
|
||||||
chain.unbind.unshift(macro $i { zeroValue } = null);
|
|
||||||
|
|
||||||
var callListener = switch (type) {
|
|
||||||
case macro : Void: macro if (!init) $i{zeroListener}();
|
|
||||||
case _:
|
|
||||||
macro if (!init) {
|
|
||||||
var v:Null < $type > = null;
|
|
||||||
try { v = $expr; } catch (e:Dynamic) { };
|
|
||||||
$i { zeroListener } ($i { zeroValue }, v);
|
|
||||||
$i { zeroValue } = v;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
var preInit = [
|
|
||||||
(macro var init:Bool = true),
|
|
||||||
macro var $zeroValue:Null<$type> = null
|
|
||||||
];
|
|
||||||
|
|
||||||
var postInit = [
|
|
||||||
macro function $fieldListenerName(?from:Dynamic, ?to:Dynamic) $callListener,
|
|
||||||
macro function $methodListenerName() $callListener
|
|
||||||
];
|
|
||||||
|
|
||||||
var result = [
|
|
||||||
macro init = false,
|
|
||||||
macro $i { methodListenerName } (),
|
|
||||||
(macro var res = function ():Void $b { chain.unbind } ),
|
|
||||||
macro return res
|
|
||||||
];
|
|
||||||
|
|
||||||
var res = macro (function ($zeroListener):Void->Void
|
|
||||||
$b { preInit.concat(chain.init).concat(postInit).concat(chain.bind).concat(result) }
|
|
||||||
)($listener);
|
|
||||||
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
static function checkFields(expr:Expr):Array<FieldExpr> {
|
|
||||||
var first = Bind.checkField(expr);
|
|
||||||
var firstParams;
|
|
||||||
if (first.field == null) {
|
|
||||||
switch (expr.expr) {
|
|
||||||
case ECall(e, params):
|
|
||||||
first = Bind.checkField(e);
|
|
||||||
firstParams = params;
|
|
||||||
case _:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (first.field == null) {
|
|
||||||
if (first.error != null) throw first.error;
|
|
||||||
else throw new FatalError('${expr.toString()} is not bindable.', expr.pos);
|
|
||||||
}
|
|
||||||
|
|
||||||
var prevField = {e:first.e, field:first.field, error:null};
|
|
||||||
var fields:Array<FieldExpr> = [ { field:first.field, bindable:first.error == null, e:first.e, params:firstParams } ];
|
|
||||||
|
|
||||||
var end;
|
|
||||||
do {
|
|
||||||
end = true;
|
|
||||||
var field = Bind.checkField(prevField.e);
|
|
||||||
if (field.field != null) {
|
|
||||||
fields.push( { field:field.field, bindable:field.error == null, e:field.e } );
|
|
||||||
end = false;
|
|
||||||
} else if (field.error != null) switch (prevField.e.expr) {
|
|
||||||
case ECall(e, params):
|
|
||||||
field = Bind.checkField(e);
|
|
||||||
if (field.field == null) throw new FatalError('${e.toString()} is not bindable.', expr.pos);
|
|
||||||
else fields.push( { e:field.e, field:field.field, params:params, bindable:field.error == null } );
|
|
||||||
end = false;
|
|
||||||
case _:
|
|
||||||
}
|
|
||||||
else if (field.e == null) {
|
|
||||||
throw new FatalError('${prevField.e.toString()} is not bindable.', prevField.e.pos);
|
|
||||||
}
|
|
||||||
prevField = field;
|
|
||||||
} while (!end);
|
|
||||||
return fields;
|
|
||||||
}
|
|
||||||
|
|
||||||
static function warnPrepareChain(expr:Expr, prefix = "", skipUnbindable = false):Chain {
|
|
||||||
var fields = checkFields(expr);
|
|
||||||
|
|
||||||
if (fields.length == 0)
|
|
||||||
throw new FatalError('Can\'t bind empty expression: ${expr.toString()}', expr.pos);
|
|
||||||
|
|
||||||
var i = fields.length;
|
|
||||||
var first = null;
|
|
||||||
while (i-- > 0) {
|
|
||||||
var f = fields[i];
|
|
||||||
if (first != null) f.bindable = false;
|
|
||||||
else if (!f.bindable && first == null) {
|
|
||||||
first = f;
|
|
||||||
if (skipUnbindable) {
|
|
||||||
fields = fields.splice(i+1, fields.length - i);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var bindableNum = fields.fold(function (it, n) return n += it.bindable ? 1 : 0, 0);
|
|
||||||
if (bindableNum == 0) {
|
|
||||||
throw new GenericError('${expr.toString()} is not bindable.', expr.pos);
|
|
||||||
}
|
|
||||||
if (first != null) {
|
|
||||||
Warn.w('${expr.toString()} is not full bindable. Can bind only "${first.e.toString()}".', expr.pos, WarnPriority.INFO);
|
|
||||||
}
|
|
||||||
return prepareChain(fields, expr, prefix);
|
|
||||||
}
|
|
||||||
|
|
||||||
inline static function listenerName(idx:Int, prefix) return '${prefix}listener$idx';
|
|
||||||
|
|
||||||
static function prepareChain(fields:Array<FieldExpr>, expr:Expr, prefix = ""):Chain {
|
|
||||||
var res:Chain = { init:[], bind:[], unbind:[], expr:null };
|
|
||||||
|
|
||||||
var prevListenerName = listenerName(0, prefix);
|
|
||||||
var prevListenerNameExpr = macro $i { prevListenerName };
|
|
||||||
var zeroListener = fields[0].bindable ? { f:fields[0], l:prevListenerNameExpr } : null;
|
|
||||||
if (zeroListener != null) {
|
|
||||||
var fn = zeroListener.f.field.name;
|
|
||||||
res.expr = macro @:pos(zeroListener.f.e.pos) ${zeroListener.f.e}.$fn;
|
|
||||||
}
|
|
||||||
var i = -1;
|
|
||||||
while (++i < fields.length - 1) {
|
|
||||||
var field = fields[i + 1];
|
|
||||||
var prev = fields[i];
|
|
||||||
var type = Context.typeof(field.e).toComplexType();
|
|
||||||
var listenerName = listenerName(i+1, prefix);
|
|
||||||
var listenerNameExpr = macro $i { listenerName };
|
|
||||||
|
|
||||||
var value = '${prefix}value${i+1}';
|
|
||||||
var valueExpr = macro $i { value };
|
|
||||||
|
|
||||||
var oldValue = '${prefix}oldValue${i+1}';
|
|
||||||
var oldValueExpr = macro $i { oldValue };
|
|
||||||
|
|
||||||
var fieldName = prev.field.name;
|
|
||||||
var e = prev.e;
|
|
||||||
|
|
||||||
var fieldListenerBody = [];
|
|
||||||
var fieldListener;
|
|
||||||
|
|
||||||
if (field.bindable) zeroListener = { f:field, l:listenerNameExpr };
|
|
||||||
|
|
||||||
if (prev.bindable && res.expr == null) {
|
|
||||||
var fn = prev.field.name;
|
|
||||||
res.expr = macro @:pos(prev.e.pos) ${prev.e}.$fn;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (prev.bindable) {
|
|
||||||
var unbind = BindMacros.bindingSignalProvider.getClassFieldUnbindExpr(valueExpr, prev.field, prevListenerNameExpr );
|
|
||||||
|
|
||||||
res.bind.push(macro var $value:Null<$type> = null );
|
|
||||||
res.unbind.push(macro if ($valueExpr != null) { $unbind; $valueExpr = null; } );
|
|
||||||
|
|
||||||
fieldListenerBody.push(macro if ($valueExpr != null) $unbind );
|
|
||||||
fieldListenerBody.push(macro $valueExpr = n );
|
|
||||||
fieldListenerBody.push(macro if (n != null)
|
|
||||||
$ { BindMacros.bindingSignalProvider.getClassFieldBindExpr(macro n, prev.field, prevListenerNameExpr ) });
|
|
||||||
}
|
|
||||||
var callPrevArgs = prev.params != null ? [] : [macro o != null ? o.$fieldName : null, macro n != null ? n.$fieldName : null];
|
|
||||||
var callPrev = macro $prevListenerNameExpr($a { callPrevArgs } );
|
|
||||||
fieldListenerBody.push(callPrev);
|
|
||||||
|
|
||||||
if (field.params != null) {
|
|
||||||
fieldListenerBody.unshift(macro $i { oldValue } = n);
|
|
||||||
fieldListenerBody.unshift(macro try { n = $e; } catch (e:Dynamic) { });
|
|
||||||
fieldListenerBody.unshift(macro var n:Null < $type > = null);
|
|
||||||
fieldListenerBody.unshift(macro var o:Null<$type> = $i{oldValue} );
|
|
||||||
|
|
||||||
res.init.push(macro var $oldValue:Null<$type> = null);
|
|
||||||
res.unbind.push(macro $oldValueExpr = null);
|
|
||||||
|
|
||||||
fieldListener = macro function $listenerName ():Void $b { fieldListenerBody };
|
|
||||||
} else {
|
|
||||||
if (prev.bindable) {
|
|
||||||
fieldListenerBody.unshift(macro if (o != null)
|
|
||||||
${BindMacros.bindingSignalProvider.getClassFieldUnbindExpr(macro o, prev.field, prevListenerNameExpr )}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
fieldListener = macro function $listenerName (o:Null<$type>, n:Null<$type>):Void $b { fieldListenerBody };
|
|
||||||
}
|
|
||||||
|
|
||||||
res.bind.push(fieldListener);
|
|
||||||
|
|
||||||
prevListenerName = listenerName;
|
|
||||||
prevListenerNameExpr = listenerNameExpr;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (zeroListener == null || zeroListener.f.bindable == false)
|
|
||||||
throw new GenericError('${expr.toString()} is not bindable.', expr.pos);
|
|
||||||
|
|
||||||
var zeroName = zeroListener.f.e.toString();
|
|
||||||
if (zeroName != "this")
|
|
||||||
res.init.unshift(macro var $zeroName = $i{zeroName});
|
|
||||||
|
|
||||||
res.bind.push(BindMacros.bindingSignalProvider.getClassFieldBindExpr(macro $i{zeroName}, zeroListener.f.field, zeroListener.l ));
|
|
||||||
res.unbind.push(BindMacros.bindingSignalProvider.getClassFieldUnbindExpr(macro $i{zeroName}, zeroListener.f.field, zeroListener.l ));
|
|
||||||
|
|
||||||
if (zeroListener.f.params != null) {
|
|
||||||
res.bind.push(macro ${zeroListener.l}());
|
|
||||||
} else {
|
|
||||||
var fieldName = zeroListener.f.field.name;
|
|
||||||
res.bind.push(macro $ { zeroListener.l } (null, $ { zeroListener.f.e } .$fieldName ));
|
|
||||||
}
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
#end
|
|
||||||
}
|
|
||||||
@@ -1,172 +1,5 @@
|
|||||||
package bindx;
|
package bindx;
|
||||||
|
|
||||||
#if macro
|
|
||||||
|
|
||||||
import bindx.BindSignal.BindSignalProvider;
|
|
||||||
import bindx.BindSignal.Signal;
|
|
||||||
import haxe.macro.Expr;
|
|
||||||
import haxe.macro.Type;
|
|
||||||
import haxe.macro.Context;
|
|
||||||
|
|
||||||
using bindx.MetaUtils;
|
|
||||||
using haxe.macro.Tools;
|
|
||||||
using Lambda;
|
|
||||||
|
|
||||||
class BindSignalProvider implements IBindingSignalProvider {
|
|
||||||
|
|
||||||
static inline var SIGNAL_POSTFIX = "Changed";
|
|
||||||
|
|
||||||
/**
|
|
||||||
* default value: true
|
|
||||||
*/
|
|
||||||
static inline var LAZY_SIGNAL = "lazySignal";
|
|
||||||
/**
|
|
||||||
* default value: false
|
|
||||||
*/
|
|
||||||
static inline var INLINE_SIGNAL_GETTER = "inlineSignalGetter";
|
|
||||||
|
|
||||||
public function new() {}
|
|
||||||
|
|
||||||
@:extern static inline function signalName(fieldName:String):String return fieldName + SIGNAL_POSTFIX;
|
|
||||||
@:extern static inline function signalGetterName(fieldName:String):String return "get_" + signalName(fieldName);
|
|
||||||
@:extern static inline function signalPrivateName(fieldName:String):String return "_" + signalName(fieldName);
|
|
||||||
|
|
||||||
public function getFieldDispatcher(field:Field, res:Array<Field>):Void {
|
|
||||||
switch (field.kind) {
|
|
||||||
case FFun(_):
|
|
||||||
generateSignal(field, macro : bindx.BindSignal.MethodSignal, macro new bindx.BindSignal.MethodSignal(), res);
|
|
||||||
case FProp(_, _, type, _) | FVar(type, _):
|
|
||||||
generateSignal(field, macro : bindx.BindSignal.FieldSignal<$type>, macro new bindx.BindSignal.FieldSignal<$type>(), res);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getFieldChangedExpr(field:Field, oldValue:Expr, newValue:Expr):Expr {
|
|
||||||
var args = switch (field.kind) { case FFun(_): []; case _: [oldValue, newValue]; }
|
|
||||||
return dispatchSignal(macro this, field.name, args, hasLazy(field.bindableMeta()));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getClassFieldBindExpr(expr:Expr, field:ClassField, listener:Expr):Expr {
|
|
||||||
var signalName = signalName(field.name);
|
|
||||||
return macro @:privateAccess $expr.$signalName.add($listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getClassFieldBindToExpr(expr:Expr, field:ClassField, target:Expr):Expr {
|
|
||||||
var signalName = signalName(field.name);
|
|
||||||
return switch (field.kind) {
|
|
||||||
case FMethod(_):
|
|
||||||
var fieldName = field.name;
|
|
||||||
macro @:privateAccess {
|
|
||||||
var listener = function () $target = $expr.$fieldName();
|
|
||||||
$expr.$signalName.add(listener);
|
|
||||||
function __unbind__() $expr.$signalName.remove(listener);
|
|
||||||
}
|
|
||||||
case FVar(_, _):
|
|
||||||
var type = field.type.follow().toComplexType();
|
|
||||||
macro @:privateAccess {
|
|
||||||
var listener = function (from:$type, to:$type) $target = to;
|
|
||||||
$expr.$signalName.add(listener);
|
|
||||||
function __unbind__() $expr.$signalName.remove(listener);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getClassFieldUnbindExpr(expr:Expr, field:ClassField, listener:Expr):Expr {
|
|
||||||
var signalName = signalName(field.name);
|
|
||||||
return if (!listener.isNullOrEmpty())
|
|
||||||
macro @:privateAccess $expr.$signalName.remove($listener);
|
|
||||||
else
|
|
||||||
macro @:privateAccess $expr.$signalName.removeAll();
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getClassFieldChangedExpr(expr:Expr, field:ClassField, oldValue:Expr, newValue:Expr):Expr {
|
|
||||||
var args = switch (field.kind) {
|
|
||||||
case FMethod(_):
|
|
||||||
if (!oldValue.isNullOrEmpty())
|
|
||||||
Context.error("method notify doesn't require oldValue", oldValue.pos);
|
|
||||||
if (!newValue.isNullOrEmpty())
|
|
||||||
Context.error("method notify doesn't require newValue", newValue.pos);
|
|
||||||
[];
|
|
||||||
case FVar(_, _):
|
|
||||||
[oldValue, newValue];
|
|
||||||
}
|
|
||||||
return dispatchSignal(expr, field.name, args, hasLazy(field.bindableMeta()));
|
|
||||||
}
|
|
||||||
|
|
||||||
public function getUnbindAllExpr(expr:ExprOf<IBindable>, type:Type):Expr {
|
|
||||||
return macro bindx.BindSignal.SignalTools.unbindAll($expr);
|
|
||||||
}
|
|
||||||
|
|
||||||
function generateSignal(field:Field, type:ComplexType, builder:Expr, res:Array<Field>):Void {
|
|
||||||
var signalName = signalName(field.name);
|
|
||||||
var meta = field.bindableMeta();
|
|
||||||
var inlineSignalGetter = meta.findParam(INLINE_SIGNAL_GETTER);
|
|
||||||
|
|
||||||
if (hasLazy(meta)) {
|
|
||||||
var signalPrivateName = signalPrivateName(field.name);
|
|
||||||
res.push({
|
|
||||||
name: signalPrivateName,
|
|
||||||
kind: FVar(type, null),
|
|
||||||
pos: field.pos,
|
|
||||||
meta: [ { name:SignalTools.BIND_SIGNAL_META, pos:field.pos, params: [macro true] } ],
|
|
||||||
access: [APrivate]
|
|
||||||
});
|
|
||||||
|
|
||||||
res.push({
|
|
||||||
name: signalName,
|
|
||||||
kind: FProp("get", "never", type, null),
|
|
||||||
pos: field.pos,
|
|
||||||
access: [APrivate],
|
|
||||||
});
|
|
||||||
|
|
||||||
var getter = macro function foo() {
|
|
||||||
if (this.$signalPrivateName == null)
|
|
||||||
this.$signalPrivateName = ${builder};
|
|
||||||
return $i{signalPrivateName};
|
|
||||||
};
|
|
||||||
var getterAccess = [APrivate];
|
|
||||||
if (inlineSignalGetter.isNotNullAndTrue()) getterAccess.push(AInline);
|
|
||||||
|
|
||||||
res.push({
|
|
||||||
name: signalGetterName(field.name),
|
|
||||||
kind: FFun(switch (getter.expr) { case EFunction (_, func): func; case _: throw false; }),
|
|
||||||
pos: field.pos,
|
|
||||||
access: getterAccess
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
if (inlineSignalGetter != null)
|
|
||||||
Context.warning('$INLINE_SIGNAL_GETTER works only with lazy signals', inlineSignalGetter.pos);
|
|
||||||
|
|
||||||
res.push({
|
|
||||||
name: signalName,
|
|
||||||
kind: FProp("default", "null", type, builder),
|
|
||||||
pos: field.pos,
|
|
||||||
access: [APrivate],
|
|
||||||
meta: [ { name:SignalTools.BIND_SIGNAL_META, pos:field.pos, params: [macro false] } ]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
inline function dispatchSignal(expr:Expr, fieldName:String, args:Array<Expr>, lazy:Bool):Expr {
|
|
||||||
return
|
|
||||||
if (lazy) {
|
|
||||||
var signalPrivateName = signalPrivateName(fieldName);
|
|
||||||
macro @:privateAccess {
|
|
||||||
if ($expr.$signalPrivateName != null)
|
|
||||||
$expr.$signalPrivateName.dispatch($a { args } );
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
var signalName = signalName(fieldName);
|
|
||||||
macro @:privateAccess $expr.$signalName.dispatch($a { args } );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@:extern inline function hasLazy(meta:MetadataEntry):Bool {
|
|
||||||
return meta.findParam(LAZY_SIGNAL).isNullOrTrue();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#end
|
|
||||||
|
|
||||||
class MethodSignal extends Signal<Void -> Void> {
|
class MethodSignal extends Signal<Void -> Void> {
|
||||||
|
|
||||||
public function dispatch():Void {
|
public function dispatch():Void {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
package bindx;
|
package bindx;
|
||||||
|
|
||||||
@:autoBuild(bindx.BindMacros.buildIBindable())
|
@:autoBuild(bindx.macro.BindableMacros.buildIBindable())
|
||||||
interface IBindable {
|
interface IBindable {
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,344 @@
|
|||||||
|
package bindx.macro;
|
||||||
|
|
||||||
|
#if macro
|
||||||
|
|
||||||
|
import bindx.macro.GenericError;
|
||||||
|
import haxe.macro.Expr;
|
||||||
|
import haxe.macro.MacroStringTools;
|
||||||
|
import haxe.macro.Type;
|
||||||
|
import haxe.macro.Context;
|
||||||
|
import bindx.macro.BindMacros;
|
||||||
|
|
||||||
|
using Lambda;
|
||||||
|
using StringTools;
|
||||||
|
using haxe.macro.Tools;
|
||||||
|
|
||||||
|
private typedef FieldExpr = {
|
||||||
|
var field:ClassField;
|
||||||
|
var bindable:Bool;
|
||||||
|
var e:Expr;
|
||||||
|
@:optional var params:Array<Expr>;
|
||||||
|
}
|
||||||
|
|
||||||
|
private typedef Chain = {
|
||||||
|
var init:Array<Expr>;
|
||||||
|
var bind:Array<Expr>;
|
||||||
|
var unbind:Array<Expr>;
|
||||||
|
var expr:Expr;
|
||||||
|
}
|
||||||
|
|
||||||
|
@:access(bindx.macro.BindableMacros)
|
||||||
|
@:access(bindx.macro.BindMacros)
|
||||||
|
class BindxExtMacro {
|
||||||
|
|
||||||
|
static inline function internalBindChain(expr:Expr, listener:Expr):Expr {
|
||||||
|
var zeroListener = listenerName(0, "");
|
||||||
|
var chain = null;
|
||||||
|
try { chain = warnPrepareChain(expr); } catch (e:GenericError) e.contextError();
|
||||||
|
|
||||||
|
var res = macro (function ($zeroListener):Void->Void
|
||||||
|
$b { chain.init.concat(chain.bind).concat([(macro var res = function ():Void $b { chain.unbind }), macro return res]) }
|
||||||
|
)($listener);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline function unwrapFormatedString(expr:Expr):Expr {
|
||||||
|
return if (MacroStringTools.isFormatExpr(expr)) {
|
||||||
|
var f = switch (expr.expr) {
|
||||||
|
case EConst(CString(s)): s;
|
||||||
|
case _: null;
|
||||||
|
}
|
||||||
|
if (f != null) MacroStringTools.formatString(f, expr.pos) else expr;
|
||||||
|
} else expr;
|
||||||
|
}
|
||||||
|
|
||||||
|
static function internalBindExpr(expr:Expr, listener:Expr):Expr {
|
||||||
|
var type = Context.typeof(expr).toComplexType();
|
||||||
|
var listenerNameExpr = macro listener;
|
||||||
|
var fieldListenerName = "fieldListener";
|
||||||
|
var fieldListenerNameExpr = macro $i{fieldListenerName};
|
||||||
|
var methodListenerName = "methodListener";
|
||||||
|
var methodListenerNameExpr = macro $i{methodListenerName};
|
||||||
|
var chain:Chain = { init:[], bind:[], unbind:[], expr:expr };
|
||||||
|
var binded:Map<String, {prebind:Expr, c:Chain}> = new Map();
|
||||||
|
|
||||||
|
var prefix = 0;
|
||||||
|
function findChain(expr:Expr) {
|
||||||
|
var isChain;
|
||||||
|
expr = unwrapFormatedString(expr);
|
||||||
|
var e = expr;
|
||||||
|
do switch (e.expr) {
|
||||||
|
case EField(le, _) | ECall(le, _):
|
||||||
|
isChain = true;
|
||||||
|
e = le;
|
||||||
|
case _:
|
||||||
|
isChain = false;
|
||||||
|
} while (isChain);
|
||||||
|
var doBind = e != expr;
|
||||||
|
if (doBind) {
|
||||||
|
var key = expr.toString();
|
||||||
|
for (k in binded.keys()) if (k.startsWith(key)) {
|
||||||
|
doBind = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (doBind) {
|
||||||
|
var pre = '_${prefix++}';
|
||||||
|
var zeroListener = listenerName(0, pre);
|
||||||
|
var c = null;
|
||||||
|
try {
|
||||||
|
c = warnPrepareChain(expr, pre, true);
|
||||||
|
} catch (e:GenericError) {
|
||||||
|
Warn.w('${expr.toString()} is not bindable.', e.pos, WarnPriority.ALL);
|
||||||
|
}
|
||||||
|
if (c != null) {
|
||||||
|
var key = c.expr.toString();
|
||||||
|
if (!binded.exists(key)) {
|
||||||
|
var ecall = switch (c.expr.expr) {
|
||||||
|
case EField(e, field):
|
||||||
|
var type = Context.typeof(e);
|
||||||
|
var classRef = type.getClass();
|
||||||
|
var field = classRef.findField(field);
|
||||||
|
field.kind.match(FMethod(_));
|
||||||
|
case _: false;
|
||||||
|
}
|
||||||
|
var prebind = macro var $zeroListener = ${ecall ? methodListenerNameExpr : fieldListenerNameExpr};
|
||||||
|
binded.set(key, {prebind:prebind, c:c});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
expr.iter(findChain);
|
||||||
|
}
|
||||||
|
findChain(expr);
|
||||||
|
|
||||||
|
var keys = [for (k in binded.keys()) k];
|
||||||
|
var i = 0;
|
||||||
|
while (i < keys.length) {
|
||||||
|
var k = keys[i];
|
||||||
|
var remove = false;
|
||||||
|
var j = i;
|
||||||
|
while (!remove && ++j < keys.length) remove = keys[j].startsWith(k);
|
||||||
|
if (remove) keys = keys.splice(i, 1); else i++;
|
||||||
|
}
|
||||||
|
|
||||||
|
var msg = [];
|
||||||
|
for (k in keys) {
|
||||||
|
var data = binded.get(k);
|
||||||
|
msg.push(data.c.expr.toString());
|
||||||
|
chain.bind.unshift(data.prebind);
|
||||||
|
var c = data.c;
|
||||||
|
chain.init = chain.init.concat(c.init);
|
||||||
|
chain.bind = chain.bind.concat(c.bind);
|
||||||
|
chain.unbind = chain.unbind.concat(c.unbind);
|
||||||
|
}
|
||||||
|
Warn.w('Bind \'${msg.join("', '")}\'', expr.pos, WarnPriority.INFO);
|
||||||
|
|
||||||
|
var zeroListener = listenerName(0, "");
|
||||||
|
var zeroValue = 'value0';
|
||||||
|
chain.unbind.unshift(macro $i { zeroValue } = null);
|
||||||
|
|
||||||
|
var callListener = switch (type) {
|
||||||
|
case macro : Void: macro if (!init) $i{zeroListener}();
|
||||||
|
case _:
|
||||||
|
macro if (!init) {
|
||||||
|
var v:Null < $type > = null;
|
||||||
|
try { v = $expr; } catch (e:Dynamic) { };
|
||||||
|
$i { zeroListener } ($i { zeroValue }, v);
|
||||||
|
$i { zeroValue } = v;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var preInit = [
|
||||||
|
(macro var init:Bool = true),
|
||||||
|
macro var $zeroValue:Null<$type> = null
|
||||||
|
];
|
||||||
|
|
||||||
|
var postInit = [
|
||||||
|
macro function $fieldListenerName(?from:Dynamic, ?to:Dynamic) $callListener,
|
||||||
|
macro function $methodListenerName() $callListener
|
||||||
|
];
|
||||||
|
|
||||||
|
var result = [
|
||||||
|
macro init = false,
|
||||||
|
macro $i { methodListenerName } (),
|
||||||
|
(macro var res = function ():Void $b { chain.unbind } ),
|
||||||
|
macro return res
|
||||||
|
];
|
||||||
|
|
||||||
|
var res = macro (function ($zeroListener):Void->Void
|
||||||
|
$b { preInit.concat(chain.init).concat(postInit).concat(chain.bind).concat(result) }
|
||||||
|
)($listener);
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
static function checkFields(expr:Expr):Array<FieldExpr> {
|
||||||
|
var first = BindMacros.checkField(expr);
|
||||||
|
var firstParams;
|
||||||
|
if (first.field == null) {
|
||||||
|
switch (expr.expr) {
|
||||||
|
case ECall(e, params):
|
||||||
|
first = BindMacros.checkField(e);
|
||||||
|
firstParams = params;
|
||||||
|
case _:
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (first.field == null) {
|
||||||
|
if (first.error != null) throw first.error;
|
||||||
|
else throw new FatalError('${expr.toString()} is not bindable.', expr.pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
var prevField = {e:first.e, field:first.field, error:null};
|
||||||
|
var fields:Array<FieldExpr> = [ { field:first.field, bindable:first.error == null, e:first.e, params:firstParams } ];
|
||||||
|
|
||||||
|
var end;
|
||||||
|
do {
|
||||||
|
end = true;
|
||||||
|
var field = BindMacros.checkField(prevField.e);
|
||||||
|
if (field.field != null) {
|
||||||
|
fields.push( { field:field.field, bindable:field.error == null, e:field.e } );
|
||||||
|
end = false;
|
||||||
|
} else if (field.error != null) switch (prevField.e.expr) {
|
||||||
|
case ECall(e, params):
|
||||||
|
field = BindMacros.checkField(e);
|
||||||
|
if (field.field == null) throw new FatalError('${e.toString()} is not bindable.', expr.pos);
|
||||||
|
else fields.push( { e:field.e, field:field.field, params:params, bindable:field.error == null } );
|
||||||
|
end = false;
|
||||||
|
case _:
|
||||||
|
}
|
||||||
|
else if (field.e == null) {
|
||||||
|
throw new FatalError('${prevField.e.toString()} is not bindable.', prevField.e.pos);
|
||||||
|
}
|
||||||
|
prevField = field;
|
||||||
|
} while (!end);
|
||||||
|
return fields;
|
||||||
|
}
|
||||||
|
|
||||||
|
static function warnPrepareChain(expr:Expr, prefix = "", skipUnbindable = false):Chain {
|
||||||
|
var fields = checkFields(expr);
|
||||||
|
|
||||||
|
if (fields.length == 0)
|
||||||
|
throw new FatalError('Can\'t bind empty expression: ${expr.toString()}', expr.pos);
|
||||||
|
|
||||||
|
var i = fields.length;
|
||||||
|
var first = null;
|
||||||
|
while (i-- > 0) {
|
||||||
|
var f = fields[i];
|
||||||
|
if (first != null) f.bindable = false;
|
||||||
|
else if (!f.bindable && first == null) {
|
||||||
|
first = f;
|
||||||
|
if (skipUnbindable) {
|
||||||
|
fields = fields.splice(i+1, fields.length - i);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var bindableNum = fields.fold(function (it, n) return n += it.bindable ? 1 : 0, 0);
|
||||||
|
if (bindableNum == 0) {
|
||||||
|
throw new GenericError('${expr.toString()} is not bindable.', expr.pos);
|
||||||
|
}
|
||||||
|
if (first != null) {
|
||||||
|
Warn.w('${expr.toString()} is not full bindable. Can bind only "${first.e.toString()}".', expr.pos, WarnPriority.INFO);
|
||||||
|
}
|
||||||
|
return prepareChain(fields, expr, prefix);
|
||||||
|
}
|
||||||
|
|
||||||
|
inline static function listenerName(idx:Int, prefix) return '${prefix}listener$idx';
|
||||||
|
|
||||||
|
static function prepareChain(fields:Array<FieldExpr>, expr:Expr, prefix = ""):Chain {
|
||||||
|
var res:Chain = { init:[], bind:[], unbind:[], expr:null };
|
||||||
|
|
||||||
|
var prevListenerName = listenerName(0, prefix);
|
||||||
|
var prevListenerNameExpr = macro $i { prevListenerName };
|
||||||
|
var zeroListener = fields[0].bindable ? { f:fields[0], l:prevListenerNameExpr } : null;
|
||||||
|
if (zeroListener != null) {
|
||||||
|
var fn = zeroListener.f.field.name;
|
||||||
|
res.expr = macro @:pos(zeroListener.f.e.pos) ${zeroListener.f.e}.$fn;
|
||||||
|
}
|
||||||
|
var i = -1;
|
||||||
|
while (++i < fields.length - 1) {
|
||||||
|
var field = fields[i + 1];
|
||||||
|
var prev = fields[i];
|
||||||
|
var type = Context.typeof(field.e).toComplexType();
|
||||||
|
var listenerName = listenerName(i+1, prefix);
|
||||||
|
var listenerNameExpr = macro $i { listenerName };
|
||||||
|
|
||||||
|
var value = '${prefix}value${i+1}';
|
||||||
|
var valueExpr = macro $i { value };
|
||||||
|
|
||||||
|
var oldValue = '${prefix}oldValue${i+1}';
|
||||||
|
var oldValueExpr = macro $i { oldValue };
|
||||||
|
|
||||||
|
var fieldName = prev.field.name;
|
||||||
|
var e = prev.e;
|
||||||
|
|
||||||
|
var fieldListenerBody = [];
|
||||||
|
var fieldListener;
|
||||||
|
|
||||||
|
if (field.bindable) zeroListener = { f:field, l:listenerNameExpr };
|
||||||
|
|
||||||
|
if (prev.bindable && res.expr == null) {
|
||||||
|
var fn = prev.field.name;
|
||||||
|
res.expr = macro @:pos(prev.e.pos) ${prev.e}.$fn;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prev.bindable) {
|
||||||
|
var unbind = BindableMacros.bindingSignalProvider.getClassFieldUnbindExpr(valueExpr, prev.field, prevListenerNameExpr );
|
||||||
|
|
||||||
|
res.bind.push(macro var $value:Null<$type> = null );
|
||||||
|
res.unbind.push(macro if ($valueExpr != null) { $unbind; $valueExpr = null; } );
|
||||||
|
|
||||||
|
fieldListenerBody.push(macro if ($valueExpr != null) $unbind );
|
||||||
|
fieldListenerBody.push(macro $valueExpr = n );
|
||||||
|
fieldListenerBody.push(macro if (n != null)
|
||||||
|
$ { BindableMacros.bindingSignalProvider.getClassFieldBindExpr(macro n, prev.field, prevListenerNameExpr ) });
|
||||||
|
}
|
||||||
|
var callPrevArgs = prev.params != null ? [] : [macro o != null ? o.$fieldName : null, macro n != null ? n.$fieldName : null];
|
||||||
|
var callPrev = macro $prevListenerNameExpr($a { callPrevArgs } );
|
||||||
|
fieldListenerBody.push(callPrev);
|
||||||
|
|
||||||
|
if (field.params != null) {
|
||||||
|
fieldListenerBody.unshift(macro $i { oldValue } = n);
|
||||||
|
fieldListenerBody.unshift(macro try { n = $e; } catch (e:Dynamic) { });
|
||||||
|
fieldListenerBody.unshift(macro var n:Null < $type > = null);
|
||||||
|
fieldListenerBody.unshift(macro var o:Null<$type> = $i{oldValue} );
|
||||||
|
|
||||||
|
res.init.push(macro var $oldValue:Null<$type> = null);
|
||||||
|
res.unbind.push(macro $oldValueExpr = null);
|
||||||
|
|
||||||
|
fieldListener = macro function $listenerName ():Void $b { fieldListenerBody };
|
||||||
|
} else {
|
||||||
|
if (prev.bindable) {
|
||||||
|
fieldListenerBody.unshift(macro if (o != null)
|
||||||
|
${BindableMacros.bindingSignalProvider.getClassFieldUnbindExpr(macro o, prev.field, prevListenerNameExpr )}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
fieldListener = macro function $listenerName (o:Null<$type>, n:Null<$type>):Void $b { fieldListenerBody };
|
||||||
|
}
|
||||||
|
|
||||||
|
res.bind.push(fieldListener);
|
||||||
|
|
||||||
|
prevListenerName = listenerName;
|
||||||
|
prevListenerNameExpr = listenerNameExpr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (zeroListener == null || zeroListener.f.bindable == false)
|
||||||
|
throw new GenericError('${expr.toString()} is not bindable.', expr.pos);
|
||||||
|
|
||||||
|
var zeroName = zeroListener.f.e.toString();
|
||||||
|
if (zeroName != "this")
|
||||||
|
res.init.unshift(macro var $zeroName = $i{zeroName});
|
||||||
|
|
||||||
|
res.bind.push(BindableMacros.bindingSignalProvider.getClassFieldBindExpr(macro $i{zeroName}, zeroListener.f.field, zeroListener.l ));
|
||||||
|
res.unbind.push(BindableMacros.bindingSignalProvider.getClassFieldUnbindExpr(macro $i{zeroName}, zeroListener.f.field, zeroListener.l ));
|
||||||
|
|
||||||
|
if (zeroListener.f.params != null) {
|
||||||
|
res.bind.push(macro ${zeroListener.l}());
|
||||||
|
} else {
|
||||||
|
var fieldName = zeroListener.f.field.name;
|
||||||
|
res.bind.push(macro $ { zeroListener.l } (null, $ { zeroListener.f.e } .$fieldName ));
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#end
|
||||||
@@ -0,0 +1,106 @@
|
|||||||
|
package bindx.macro;
|
||||||
|
|
||||||
|
#if macro
|
||||||
|
|
||||||
|
import bindx.macro.GenericError;
|
||||||
|
import haxe.macro.Expr;
|
||||||
|
import haxe.macro.Type;
|
||||||
|
import haxe.macro.Context;
|
||||||
|
|
||||||
|
using haxe.macro.Tools;
|
||||||
|
using bindx.macro.MetaUtils;
|
||||||
|
using Lambda;
|
||||||
|
|
||||||
|
@:access(bindx.macro.BindableMacros)
|
||||||
|
class BindMacros {
|
||||||
|
|
||||||
|
static inline function internalBind(field:Expr, listener:Expr, doBind:Bool):Expr {
|
||||||
|
var fieldData = warnCheckField(field);
|
||||||
|
return if (doBind) BindableMacros.bindingSignalProvider.getClassFieldBindExpr(fieldData.e, fieldData.field, listener);
|
||||||
|
else BindableMacros.bindingSignalProvider.getClassFieldUnbindExpr(fieldData.e, fieldData.field, listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline function internalBindTo(field:Expr, target:Expr):Expr {
|
||||||
|
var fieldData = warnCheckField(field);
|
||||||
|
return BindableMacros.bindingSignalProvider.getClassFieldBindToExpr(fieldData.e, fieldData.field, target);
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline function internalNotify(field:Expr, ?oldValue:Expr, ?newValue:Expr):Expr {
|
||||||
|
var fieldData = warnCheckField(field);
|
||||||
|
return BindableMacros.bindingSignalProvider.getClassFieldChangedExpr(fieldData.e, fieldData.field, oldValue, newValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline function internalUnbindAll(object:ExprOf<IBindable>):Expr {
|
||||||
|
var type = Context.typeof(object).follow();
|
||||||
|
if (!isBindable(type.getClass())) {
|
||||||
|
Context.error('\'${object.toString()}\' must be bindx.IBindable', object.pos);
|
||||||
|
}
|
||||||
|
return BindableMacros.bindingSignalProvider.getUnbindAllExpr(object, type);
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline function warnCheckField(field:Expr):{e:Expr, field:ClassField} {
|
||||||
|
var res = null;
|
||||||
|
try {
|
||||||
|
res = checkField(field);
|
||||||
|
if (res.error != null) res.error.contextError();
|
||||||
|
} catch (e:FatalError) {
|
||||||
|
e.contextFatal();
|
||||||
|
} catch (e:GenericError) {
|
||||||
|
e.contextError();
|
||||||
|
};
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
static function checkField(f:Expr):{e:Expr, field:ClassField, error:GenericError} {
|
||||||
|
var error:GenericError = null;
|
||||||
|
switch (f.expr) {
|
||||||
|
case EField(e, field):
|
||||||
|
var type = Context.typeof(e);
|
||||||
|
var classType = switch (type) { case TInst(c, _): c.get(); case _: null; };
|
||||||
|
if (classType == null) {
|
||||||
|
error = new FatalError('Type \'${e.toString()}\' is unknown', e.pos);
|
||||||
|
return {e:f, field:null, error:error};
|
||||||
|
}
|
||||||
|
if (!isBindable(classType)) {
|
||||||
|
error = new GenericError('\'${e.toString()}\' must be bindx.IBindable', e.pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
var field = classType.findField(field, null);
|
||||||
|
if (field == null) {
|
||||||
|
throw new FatalError('\'${e.toString()}.${field.name}\' expected', field.pos);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!field.hasBindableMeta()) {
|
||||||
|
error = new GenericError('\'${e.toString()}.${field.name}\' is not bindable', field.pos);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {e:e, field:field, error:error};
|
||||||
|
|
||||||
|
case EConst(CIdent(_)):
|
||||||
|
return {e:f, field:null, error:new GenericError('Can\'t bind \'${f.toString()}\'. Please use \'this.${f.toString()}\'', f.pos)};
|
||||||
|
|
||||||
|
case _:
|
||||||
|
}
|
||||||
|
return {e:f, field:null, error:new GenericError('\'${f.toString()}\' is not bindable', f.pos)};
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline function isBindable(classType:ClassType):Bool {
|
||||||
|
var check = [classType];
|
||||||
|
var res = false;
|
||||||
|
while (check.length > 0 && !res) {
|
||||||
|
var t = check.shift();
|
||||||
|
while (t != null) {
|
||||||
|
if (t.module == "bindx.IBindable" && t.name == "IBindable") {
|
||||||
|
res = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
for (it in t.interfaces)
|
||||||
|
check.push(it.t.get());
|
||||||
|
t = t.superClass != null ? t.superClass.t.get() : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#end
|
||||||
@@ -0,0 +1,164 @@
|
|||||||
|
package bindx.macro;
|
||||||
|
|
||||||
|
import bindx.BindSignal.Signal;
|
||||||
|
import haxe.macro.Expr;
|
||||||
|
import haxe.macro.Type;
|
||||||
|
import haxe.macro.Context;
|
||||||
|
import bindx.BindSignal;
|
||||||
|
|
||||||
|
using bindx.macro.MetaUtils;
|
||||||
|
using haxe.macro.Tools;
|
||||||
|
using Lambda;
|
||||||
|
|
||||||
|
class BindSignalProvider implements IBindingSignalProvider {
|
||||||
|
|
||||||
|
static inline var SIGNAL_POSTFIX = "Changed";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* default value: true
|
||||||
|
*/
|
||||||
|
static inline var LAZY_SIGNAL = "lazySignal";
|
||||||
|
/**
|
||||||
|
* default value: false
|
||||||
|
*/
|
||||||
|
static inline var INLINE_SIGNAL_GETTER = "inlineSignalGetter";
|
||||||
|
|
||||||
|
public function new() {}
|
||||||
|
|
||||||
|
@:extern static inline function signalName(fieldName:String):String return fieldName + SIGNAL_POSTFIX;
|
||||||
|
@:extern static inline function signalGetterName(fieldName:String):String return "get_" + signalName(fieldName);
|
||||||
|
@:extern static inline function signalPrivateName(fieldName:String):String return "_" + signalName(fieldName);
|
||||||
|
|
||||||
|
public function getFieldDispatcher(field:Field, res:Array<Field>):Void {
|
||||||
|
switch (field.kind) {
|
||||||
|
case FFun(_):
|
||||||
|
generateSignal(field, macro : bindx.BindSignal.MethodSignal, macro new bindx.BindSignal.MethodSignal(), res);
|
||||||
|
case FProp(_, _, type, _) | FVar(type, _):
|
||||||
|
generateSignal(field, macro : bindx.BindSignal.FieldSignal<$type>, macro new bindx.BindSignal.FieldSignal<$type>(), res);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getFieldChangedExpr(field:Field, oldValue:Expr, newValue:Expr):Expr {
|
||||||
|
var args = switch (field.kind) { case FFun(_): []; case _: [oldValue, newValue]; }
|
||||||
|
return dispatchSignal(macro this, field.name, args, hasLazy(field.bindableMeta()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getClassFieldBindExpr(expr:Expr, field:ClassField, listener:Expr):Expr {
|
||||||
|
var signalName = signalName(field.name);
|
||||||
|
return macro @:privateAccess $expr.$signalName.add($listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getClassFieldBindToExpr(expr:Expr, field:ClassField, target:Expr):Expr {
|
||||||
|
var signalName = signalName(field.name);
|
||||||
|
return switch (field.kind) {
|
||||||
|
case FMethod(_):
|
||||||
|
var fieldName = field.name;
|
||||||
|
macro @:privateAccess {
|
||||||
|
var listener = function () $target = $expr.$fieldName();
|
||||||
|
$expr.$signalName.add(listener);
|
||||||
|
function __unbind__() $expr.$signalName.remove(listener);
|
||||||
|
}
|
||||||
|
case FVar(_, _):
|
||||||
|
var type = field.type.follow().toComplexType();
|
||||||
|
macro @:privateAccess {
|
||||||
|
var listener = function (from:$type, to:$type) $target = to;
|
||||||
|
$expr.$signalName.add(listener);
|
||||||
|
function __unbind__() $expr.$signalName.remove(listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getClassFieldUnbindExpr(expr:Expr, field:ClassField, listener:Expr):Expr {
|
||||||
|
var signalName = signalName(field.name);
|
||||||
|
return if (!listener.isNullOrEmpty())
|
||||||
|
macro @:privateAccess $expr.$signalName.remove($listener);
|
||||||
|
else
|
||||||
|
macro @:privateAccess $expr.$signalName.removeAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getClassFieldChangedExpr(expr:Expr, field:ClassField, oldValue:Expr, newValue:Expr):Expr {
|
||||||
|
var args = switch (field.kind) {
|
||||||
|
case FMethod(_):
|
||||||
|
if (!oldValue.isNullOrEmpty())
|
||||||
|
Context.error("method notify doesn't require oldValue", oldValue.pos);
|
||||||
|
if (!newValue.isNullOrEmpty())
|
||||||
|
Context.error("method notify doesn't require newValue", newValue.pos);
|
||||||
|
[];
|
||||||
|
case FVar(_, _):
|
||||||
|
[oldValue, newValue];
|
||||||
|
}
|
||||||
|
return dispatchSignal(expr, field.name, args, hasLazy(field.bindableMeta()));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getUnbindAllExpr(expr:ExprOf<IBindable>, type:Type):Expr {
|
||||||
|
return macro bindx.BindSignal.SignalTools.unbindAll($expr);
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateSignal(field:Field, type:ComplexType, builder:Expr, res:Array<Field>):Void {
|
||||||
|
var signalName = signalName(field.name);
|
||||||
|
var meta = field.bindableMeta();
|
||||||
|
var inlineSignalGetter = meta.findParam(INLINE_SIGNAL_GETTER);
|
||||||
|
|
||||||
|
if (hasLazy(meta)) {
|
||||||
|
var signalPrivateName = signalPrivateName(field.name);
|
||||||
|
res.push({
|
||||||
|
name: signalPrivateName,
|
||||||
|
kind: FVar(type, null),
|
||||||
|
pos: field.pos,
|
||||||
|
meta: [ { name:SignalTools.BIND_SIGNAL_META, pos:field.pos, params: [macro true] } ],
|
||||||
|
access: [APrivate]
|
||||||
|
});
|
||||||
|
|
||||||
|
res.push({
|
||||||
|
name: signalName,
|
||||||
|
kind: FProp("get", "never", type, null),
|
||||||
|
pos: field.pos,
|
||||||
|
access: [APrivate],
|
||||||
|
});
|
||||||
|
|
||||||
|
var getter = macro function foo() {
|
||||||
|
if (this.$signalPrivateName == null)
|
||||||
|
this.$signalPrivateName = ${builder};
|
||||||
|
return $i{signalPrivateName};
|
||||||
|
};
|
||||||
|
var getterAccess = [APrivate];
|
||||||
|
if (inlineSignalGetter.isNotNullAndTrue()) getterAccess.push(AInline);
|
||||||
|
|
||||||
|
res.push({
|
||||||
|
name: signalGetterName(field.name),
|
||||||
|
kind: FFun(switch (getter.expr) { case EFunction (_, func): func; case _: throw false; }),
|
||||||
|
pos: field.pos,
|
||||||
|
access: getterAccess
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
if (inlineSignalGetter != null)
|
||||||
|
Context.warning('$INLINE_SIGNAL_GETTER works only with lazy signals', inlineSignalGetter.pos);
|
||||||
|
|
||||||
|
res.push({
|
||||||
|
name: signalName,
|
||||||
|
kind: FProp("default", "null", type, builder),
|
||||||
|
pos: field.pos,
|
||||||
|
access: [APrivate],
|
||||||
|
meta: [ { name:SignalTools.BIND_SIGNAL_META, pos:field.pos, params: [macro false] } ]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline function dispatchSignal(expr:Expr, fieldName:String, args:Array<Expr>, lazy:Bool):Expr {
|
||||||
|
return
|
||||||
|
if (lazy) {
|
||||||
|
var signalPrivateName = signalPrivateName(fieldName);
|
||||||
|
macro @:privateAccess {
|
||||||
|
if ($expr.$signalPrivateName != null)
|
||||||
|
$expr.$signalPrivateName.dispatch($a { args } );
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var signalName = signalName(fieldName);
|
||||||
|
macro @:privateAccess $expr.$signalName.dispatch($a { args } );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@:extern inline function hasLazy(meta:MetadataEntry):Bool {
|
||||||
|
return meta.findParam(LAZY_SIGNAL).isNullOrTrue();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,19 +1,17 @@
|
|||||||
package bindx;
|
package bindx.macro;
|
||||||
|
|
||||||
import bindx.GenericError;
|
import bindx.macro.GenericError;
|
||||||
import haxe.macro.Type;
|
import haxe.macro.Type;
|
||||||
import haxe.macro.Expr;
|
import haxe.macro.Expr;
|
||||||
import haxe.macro.Context;
|
import haxe.macro.Context;
|
||||||
import haxe.macro.TypeTools;
|
|
||||||
|
|
||||||
using haxe.macro.Tools;
|
using haxe.macro.Tools;
|
||||||
using Lambda;
|
using Lambda;
|
||||||
using StringTools;
|
using StringTools;
|
||||||
using bindx.MetaUtils;
|
using bindx.macro.MetaUtils;
|
||||||
using haxe.macro.Tools;
|
|
||||||
|
class BindableMacros {
|
||||||
|
|
||||||
class BindMacros {
|
|
||||||
#if macro
|
|
||||||
static inline var OLD_VALUE = "__oldValue__";
|
static inline var OLD_VALUE = "__oldValue__";
|
||||||
static inline var NEW_VALUE = "__newValue__";
|
static inline var NEW_VALUE = "__newValue__";
|
||||||
|
|
||||||
@@ -48,7 +46,7 @@ class BindMacros {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (bindingSignalProvider == null) {
|
if (bindingSignalProvider == null) {
|
||||||
bindingSignalProvider = new bindx.BindSignal.BindSignalProvider();
|
bindingSignalProvider = new bindx.macro.BindSignalProvider();
|
||||||
}
|
}
|
||||||
|
|
||||||
var fields = Context.getBuildFields();
|
var fields = Context.getBuildFields();
|
||||||
@@ -240,5 +238,4 @@ class BindMacros {
|
|||||||
case _: true;
|
case _: true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#end
|
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
package bindx;
|
package bindx.macro;
|
||||||
|
|
||||||
#if macro
|
|
||||||
|
|
||||||
import haxe.macro.Context;
|
import haxe.macro.Context;
|
||||||
import haxe.macro.Expr.Position;
|
import haxe.macro.Expr.Position;
|
||||||
@@ -51,4 +49,3 @@ class GenericError {
|
|||||||
Context.fatalError(message, pos);
|
Context.fatalError(message, pos);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#end
|
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
package bindx;
|
package bindx.macro;
|
||||||
|
|
||||||
#if macro
|
|
||||||
|
|
||||||
import haxe.macro.Expr;
|
import haxe.macro.Expr;
|
||||||
import haxe.macro.Type;
|
import haxe.macro.Type;
|
||||||
@@ -16,4 +14,3 @@ interface IBindingSignalProvider {
|
|||||||
function getClassFieldChangedExpr(expr:Expr, field:ClassField, oldValue:Expr, newValue:Expr):Expr;
|
function getClassFieldChangedExpr(expr:Expr, field:ClassField, oldValue:Expr, newValue:Expr):Expr;
|
||||||
function getUnbindAllExpr(expr:ExprOf<IBindable>, type:Type):Expr;
|
function getUnbindAllExpr(expr:ExprOf<IBindable>, type:Type):Expr;
|
||||||
}
|
}
|
||||||
#end
|
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
package bindx;
|
package bindx.macro;
|
||||||
|
|
||||||
#if macro
|
|
||||||
import haxe.macro.Type;
|
import haxe.macro.Type;
|
||||||
import haxe.macro.Expr;
|
import haxe.macro.Expr;
|
||||||
import haxe.macro.Context;
|
import haxe.macro.Context;
|
||||||
@@ -80,5 +79,3 @@ class ExprMetaUtils {
|
|||||||
static public inline function isNullOrTrue(expr:Expr):Bool
|
static public inline function isNullOrTrue(expr:Expr):Bool
|
||||||
return expr == null || isTrue(expr);
|
return expr == null || isTrue(expr);
|
||||||
}
|
}
|
||||||
|
|
||||||
#end
|
|
||||||
Reference in New Issue
Block a user