initial commit
This commit is contained in:
+14
@@ -0,0 +1,14 @@
|
|||||||
|
bin/
|
||||||
|
dump/
|
||||||
|
out/
|
||||||
|
target/
|
||||||
|
|
||||||
|
*.hxproj
|
||||||
|
.idea
|
||||||
|
*.iml
|
||||||
|
*.ipr
|
||||||
|
*.sublime-project
|
||||||
|
*.sublime-workspace
|
||||||
|
|
||||||
|
.DS_Store
|
||||||
|
Thumbs.db
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"name": "bindx2",
|
||||||
|
"url" : "https://github.com/profelis/bindx2",
|
||||||
|
"license": "MIT",
|
||||||
|
"tags": ["bind", "binding", "bindings", "cross"],
|
||||||
|
"description": "Powerful and fast macro-based data binding engine inspired by Flex Bindings with easy-to-use syntax.",
|
||||||
|
"classPath": "src",
|
||||||
|
"version": "2.0.0-alpha",
|
||||||
|
"releasenote": "bindx reification",
|
||||||
|
"contributors": [
|
||||||
|
"deep"
|
||||||
|
]
|
||||||
|
}
|
||||||
+59
@@ -0,0 +1,59 @@
|
|||||||
|
package ;
|
||||||
|
class Main {
|
||||||
|
|
||||||
|
static function main() {
|
||||||
|
trace("tada");
|
||||||
|
|
||||||
|
bindx.BindSignal.BindSignalProvider.register();
|
||||||
|
|
||||||
|
var v = new Value();
|
||||||
|
var s = {t:0};
|
||||||
|
|
||||||
|
bindx.Bind.bindx(v.str, function (from, to) {trace('str changed from $from to $to');});
|
||||||
|
v.strChanged.add(function (from, to) {trace('str changed from $from to $to');});
|
||||||
|
v.str = "12";
|
||||||
|
bindx.Bind.bindx(v.int, function (a, b) { trace(b); });
|
||||||
|
var unbind = bindx.Bind.bindTo(v.int, s.t);
|
||||||
|
v.int = 10;
|
||||||
|
trace(s.t);
|
||||||
|
unbind();
|
||||||
|
v.int = 12;
|
||||||
|
trace(s.t);
|
||||||
|
trace(v.str);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@:bindable
|
||||||
|
class Value implements bindx.IBindable {
|
||||||
|
|
||||||
|
public function new() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@:bindable(lazySignal=true, inlineSignalGetter=false, inlineSetter=true)
|
||||||
|
public var str:String;
|
||||||
|
|
||||||
|
@:bindable(force=true, inlineSetter=true)
|
||||||
|
public var int(default, set):Int;
|
||||||
|
|
||||||
|
private var noBindPrivate:Int;
|
||||||
|
|
||||||
|
function set_int(v):Int {
|
||||||
|
|
||||||
|
if (v < 0) {
|
||||||
|
int = 0;
|
||||||
|
toStringChanged.dispatch();
|
||||||
|
intChanged.dispatch(v, int);
|
||||||
|
return int;
|
||||||
|
}
|
||||||
|
|
||||||
|
intChanged.dispatch(int, int = v);
|
||||||
|
toStringChanged.dispatch();
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
@:bindable()
|
||||||
|
public function toString() {
|
||||||
|
return str + int;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
package bindx;
|
||||||
|
|
||||||
|
#if macro
|
||||||
|
import haxe.macro.Expr;
|
||||||
|
import haxe.macro.Type;
|
||||||
|
import haxe.macro.Context;
|
||||||
|
|
||||||
|
using haxe.macro.Tools;
|
||||||
|
using bindx.MetaUtils;
|
||||||
|
#end
|
||||||
|
using Lambda;
|
||||||
|
|
||||||
|
class Bind {
|
||||||
|
|
||||||
|
@:noUsing macro static public function bindx(field:Expr, listener:Expr):Expr {
|
||||||
|
return bind(field, listener, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@:noUsing macro static public function bindTo(field:Expr, target:Expr):Expr {
|
||||||
|
var fieldData = checkField(field);
|
||||||
|
return BindMacros.bindingSignalProvider.getClassFieldBindToExpr(fieldData.e, fieldData.field, target);
|
||||||
|
}
|
||||||
|
|
||||||
|
@:noUsing macro static public function unbindx(field:Expr, listener:Expr):Expr {
|
||||||
|
return bind(field, listener, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
@:noUsing macro static public function notify(field:Expr, ?oldValue:Expr, ?newValue:Expr):Expr {
|
||||||
|
var fieldData = checkField(field);
|
||||||
|
return BindMacros.bindingSignalProvider.getClassFieldChangedExpr(fieldData.e, fieldData.field, oldValue, newValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
#if macro
|
||||||
|
static function bind(field:Expr, listener:Expr, doBind:Bool):Expr {
|
||||||
|
var fieldData = checkField(field);
|
||||||
|
return if (fieldData != null) {
|
||||||
|
if (doBind) BindMacros.bindingSignalProvider.getClassFieldBindExpr(fieldData.e, fieldData.field, listener);
|
||||||
|
else BindMacros.bindingSignalProvider.getClassFieldUnbindExpr(fieldData.e, fieldData.field, listener);
|
||||||
|
} else macro {};
|
||||||
|
}
|
||||||
|
|
||||||
|
static function checkField(field:Expr):{e:Expr, field:ClassField} {
|
||||||
|
switch (field.expr) {
|
||||||
|
case EField(e, field):
|
||||||
|
var classType = Context.typeof(e).getClass();
|
||||||
|
if (classType == null || !isBindable(classType)) {
|
||||||
|
Context.error('\'${e.toString()}\' must be bindx.IBindable', e.pos);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var field:ClassField = classType.findField(field, null);
|
||||||
|
if (field == null) {
|
||||||
|
Context.error('\'${e.toString()}.$field\' expected', field.pos);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!field.hasBindableMeta()) {
|
||||||
|
Context.error('\'${e.toString()}.$field\' is not bindable', field.pos);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {e:e, field:field};
|
||||||
|
|
||||||
|
case EConst(CIdent(_)):
|
||||||
|
Context.error('can\'t bind \'${field.toString()}\'. Please use \'this.${field.toString()}\'', field.pos);
|
||||||
|
|
||||||
|
case _:
|
||||||
|
Context.error('can\'t bind field \'${field.toString()}\'', field.pos);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline function isBindable(classType:ClassType) {
|
||||||
|
return classType.interfaces.exists(function (it) {
|
||||||
|
var t = it.t.get();
|
||||||
|
return t.module == "bindx.IBindable" && t.name == "IBindable";
|
||||||
|
});
|
||||||
|
}
|
||||||
|
#end
|
||||||
|
}
|
||||||
@@ -0,0 +1,183 @@
|
|||||||
|
package bindx;
|
||||||
|
|
||||||
|
import haxe.macro.Type;
|
||||||
|
import haxe.macro.Expr;
|
||||||
|
import haxe.macro.Context;
|
||||||
|
|
||||||
|
using haxe.macro.Tools;
|
||||||
|
using Lambda;
|
||||||
|
using StringTools;
|
||||||
|
using bindx.MetaUtils;
|
||||||
|
|
||||||
|
class BindMacros {
|
||||||
|
#if macro
|
||||||
|
static inline var OLD_VALUE = "__oldValue__";
|
||||||
|
static inline var NEW_VALUE = "__newValue__";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* default value: false
|
||||||
|
*/
|
||||||
|
static public inline var INLINE_SETTER = "inlineSetter";
|
||||||
|
/**
|
||||||
|
* default value: false
|
||||||
|
*/
|
||||||
|
static public inline var FORCE = "force";
|
||||||
|
|
||||||
|
static var processed:Array<Type> = [];
|
||||||
|
|
||||||
|
static public var bindingSignalProvider:IBindingSignalProvider;
|
||||||
|
|
||||||
|
static public function setBindingSignalProvider(value:IBindingSignalProvider) {
|
||||||
|
bindingSignalProvider = value;
|
||||||
|
return macro {};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro static public function buildIBindable():Array<Field> {
|
||||||
|
var type = Context.getLocalType();
|
||||||
|
if (processed.indexOf(type) > -1) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
processed.push(type);
|
||||||
|
if (bindingSignalProvider == null) {
|
||||||
|
bindingSignalProvider = new bindx.BindSignal.BindSignalProvider();
|
||||||
|
}
|
||||||
|
|
||||||
|
var classType = type.getClass();
|
||||||
|
var fields = Context.getBuildFields();
|
||||||
|
|
||||||
|
var meta = classType.bindableMeta();
|
||||||
|
if (meta != null) injectBindableMeta(fields, meta);
|
||||||
|
|
||||||
|
var res = [];
|
||||||
|
for (f in fields)
|
||||||
|
if (f.hasBindableMeta()) {
|
||||||
|
if (!isFieldBindable(f, fields)) Context.error('can\'t bind field \'${f.name}\'', f.pos);
|
||||||
|
|
||||||
|
bindField(f, fields, res);
|
||||||
|
} else res.push(f);
|
||||||
|
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
static function bindField(field:Field, fields:Array<Field>, res:Array<Field>) {
|
||||||
|
var meta = field.bindableMeta();
|
||||||
|
bindingSignalProvider.getFieldDispatcher(field, res);
|
||||||
|
|
||||||
|
var forceParam = meta.findParam(FORCE);
|
||||||
|
var inlineSetter = meta.findParam(INLINE_SETTER);
|
||||||
|
if (forceParam.isNotNullAndTrue()) {
|
||||||
|
if (inlineSetter != null)
|
||||||
|
Context.warning('\'$INLINE_SETTER\' ingored. \'$FORCE\' mode', inlineSetter.pos);
|
||||||
|
res.push(field);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (field.kind) {
|
||||||
|
case FVar(type, expr):
|
||||||
|
var fieldName = field.name;
|
||||||
|
var setterName = 'set_$fieldName';
|
||||||
|
field.kind = FProp("default", "set", type, expr);
|
||||||
|
res.push(field);
|
||||||
|
var setter = macro function foo(value:$type) {
|
||||||
|
var $OLD_VALUE = this.$fieldName;
|
||||||
|
if ($i{OLD_VALUE} == value) return $i{OLD_VALUE};
|
||||||
|
this.$fieldName = value;
|
||||||
|
${bindingSignalProvider.getFieldChangedExpr(field, macro $i{OLD_VALUE}, macro $i{"value"})}
|
||||||
|
return value;
|
||||||
|
};
|
||||||
|
var setterAccess = [APrivate];
|
||||||
|
if (inlineSetter.isNotNullAndTrue()) setterAccess.push(AInline);
|
||||||
|
res.push({
|
||||||
|
name: setterName,
|
||||||
|
kind: FFun(switch (setter.expr) { case EFunction (_, func): func; case _: throw false; }),
|
||||||
|
pos: field.pos,
|
||||||
|
access: setterAccess
|
||||||
|
});
|
||||||
|
|
||||||
|
case FProp(get, set, type, expr):
|
||||||
|
if (inlineSetter != null)
|
||||||
|
Context.warning('$INLINE_SETTER ingored. Setter already exist', inlineSetter.pos);
|
||||||
|
var fieldName = field.name;
|
||||||
|
var setter = fields.find(function (it) return it.name == 'set_$fieldName');
|
||||||
|
if (setter == null) return;
|
||||||
|
switch (setter.kind) {
|
||||||
|
case FFun(func):
|
||||||
|
patchField = field;
|
||||||
|
func.expr = macro {
|
||||||
|
var $OLD_VALUE = this.$fieldName;
|
||||||
|
if ($i{OLD_VALUE} == $i{func.args[0].name}) return $i{OLD_VALUE};
|
||||||
|
$e{func.expr.map(patchSetter)};
|
||||||
|
};
|
||||||
|
patchField = null;
|
||||||
|
case _:
|
||||||
|
|
||||||
|
}
|
||||||
|
res.push(field);
|
||||||
|
|
||||||
|
case FFun(f):
|
||||||
|
if (inlineSetter != null)
|
||||||
|
Context.warning('methods doesn\'t support \'$INLINE_SETTER\'', inlineSetter.pos);
|
||||||
|
res.push(field);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static var patchField:Field;
|
||||||
|
|
||||||
|
static function patchSetter(expr:Expr):Expr {
|
||||||
|
return switch (expr.expr) {
|
||||||
|
case EReturn(res):
|
||||||
|
var fieldName = patchField.name;
|
||||||
|
|
||||||
|
macro {
|
||||||
|
var $NEW_VALUE = ${res.map(patchSetter)};
|
||||||
|
${bindingSignalProvider.getFieldChangedExpr(patchField, macro $i{OLD_VALUE}, macro $i{NEW_VALUE})};
|
||||||
|
return $i{NEW_VALUE};
|
||||||
|
}
|
||||||
|
|
||||||
|
case _: expr.map(patchSetter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline function injectBindableMeta(fields:Array<Field>, meta:MetadataEntry) {
|
||||||
|
for (f in fields) {
|
||||||
|
if (f.hasBindableMeta()) continue;
|
||||||
|
if (f.access.exists(function (it) return it.equals(APrivate))) continue;
|
||||||
|
|
||||||
|
var forceParam = meta.findParam(FORCE);
|
||||||
|
if (isFieldBindable(f, fields, forceParam.isNotNullAndTrue()))
|
||||||
|
switch (f.kind) {
|
||||||
|
case FFun(_):
|
||||||
|
case _: f.meta.push({name:MetaUtils.BINDABLE_META, pos:f.pos, params:meta.params});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static function isFieldBindable(field:Field, fields:Array<Field>, force = false):Bool {
|
||||||
|
if (field.name == "new") return false;
|
||||||
|
|
||||||
|
if (field.access.exists(function (it) return it.equals(AMacro) || it.equals(ADynamic) || it.equals(AStatic)))
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (field.name.startsWith("set_") || field.name.startsWith("get_")) {
|
||||||
|
var propName = field.name.substr(4);
|
||||||
|
if (fields.exists(function(it) return it.name == propName)) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!force) {
|
||||||
|
var meta = field.bindableMeta();
|
||||||
|
var forceParam = meta != null ? meta.findParam(FORCE) : null;
|
||||||
|
force = forceParam.isNotNullAndTrue();
|
||||||
|
}
|
||||||
|
if (force)
|
||||||
|
return switch (field.kind) {
|
||||||
|
case FProp("never", _, _, _): false;
|
||||||
|
case _: true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return switch (field.kind) {
|
||||||
|
case FProp("never", _, _, _) | FProp(_, "never", _, _) | FProp(_, "dynamic", _, _) | FProp(_, "null", _, _): false;
|
||||||
|
case _: true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#end
|
||||||
|
}
|
||||||
@@ -0,0 +1,210 @@
|
|||||||
|
package bindx;
|
||||||
|
|
||||||
|
import haxe.macro.Expr;
|
||||||
|
import haxe.macro.Type;
|
||||||
|
import haxe.macro.Context;
|
||||||
|
|
||||||
|
using bindx.MetaUtils;
|
||||||
|
|
||||||
|
class BindSignalProvider implements IBindingSignalProvider {
|
||||||
|
|
||||||
|
macro static public function register() {
|
||||||
|
bindx.BindMacros.setBindingSignalProvider(new BindSignalProvider());
|
||||||
|
return macro {};
|
||||||
|
}
|
||||||
|
|
||||||
|
#if macro
|
||||||
|
|
||||||
|
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() {}
|
||||||
|
|
||||||
|
@:expose static inline function signalName(fieldName:String) return fieldName + SIGNAL_POSTFIX;
|
||||||
|
@:expose static inline function signalGetterName(fieldName:String) return "get_" + signalName(fieldName);
|
||||||
|
@:expose static inline function signalPrivateName(fieldName:String) return "_" + signalName(fieldName);
|
||||||
|
|
||||||
|
public function getFieldDispatcher(field:Field, res:Array<Field>) {
|
||||||
|
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, hasLazy(field.bindableMeta()), args);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getClassFieldBindExpr(expr:Expr, field:ClassField, listener:Expr):Expr {
|
||||||
|
var signalName = signalName(field.name);
|
||||||
|
return macro $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 {
|
||||||
|
var listener = function () $target = $expr.$fieldName();
|
||||||
|
$expr.$signalName.add(listener);
|
||||||
|
function __unbind__() $expr.$signalName.remove(listener);
|
||||||
|
}
|
||||||
|
case FVar(_, _):
|
||||||
|
macro {
|
||||||
|
var listener = function (from, to) $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 macro $expr.$signalName.remove($listener);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getClassFieldChangedExpr(expr:Expr, field:ClassField, oldValue:Expr, newValue:Expr):Expr {
|
||||||
|
var args = switch (field.kind) {
|
||||||
|
case FMethod(_): [];
|
||||||
|
case FVar(_, _): [oldValue, newValue];
|
||||||
|
}
|
||||||
|
return dispatchSignal(expr, field.name, hasLazy(field.bindableMeta()), args);
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateSignal(field:Field, type:ComplexType, builder:Expr, res:Array<Field>) {
|
||||||
|
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
|
||||||
|
});
|
||||||
|
|
||||||
|
res.push({
|
||||||
|
name: signalName,
|
||||||
|
kind: FProp("get", "never", type, null),
|
||||||
|
pos: field.pos,
|
||||||
|
access: [APublic]
|
||||||
|
});
|
||||||
|
|
||||||
|
var getter = macro function foo() {
|
||||||
|
if (this.$signalPrivateName == null) {
|
||||||
|
this.$signalPrivateName = ${builder}
|
||||||
|
}
|
||||||
|
return $i{signalPrivateName};
|
||||||
|
};
|
||||||
|
var getterAccess = [];
|
||||||
|
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: [APublic]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline function dispatchSignal(expr:Expr, fieldName:String, lazy:Bool, args:Array<Expr>) {
|
||||||
|
return
|
||||||
|
if (lazy) {
|
||||||
|
var signalPrivateName = signalPrivateName(fieldName);
|
||||||
|
macro if ($expr.$signalPrivateName != null) {
|
||||||
|
$expr.$signalPrivateName.dispatch($a{args});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var signalName = signalName(fieldName);
|
||||||
|
macro $expr.$signalName.dispatch($a{args});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@:expose inline function hasLazy(meta:MetadataEntry) {
|
||||||
|
return meta.findParam(LAZY_SIGNAL).isNullOrTrue();
|
||||||
|
}
|
||||||
|
#end
|
||||||
|
}
|
||||||
|
|
||||||
|
class MethodSignal extends Signal<Void -> Void> {
|
||||||
|
|
||||||
|
public function dispatch():Void {
|
||||||
|
lock ++;
|
||||||
|
for (l in listeners) l();
|
||||||
|
if (lock > 0) lock --;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class FieldSignal<T> extends Signal<T -> T -> Void> {
|
||||||
|
|
||||||
|
public function dispatch(oldValue:T = null, newValue:T = null):Void {
|
||||||
|
lock ++;
|
||||||
|
for (l in listeners) l(oldValue, newValue);
|
||||||
|
if (lock > 0) lock --;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class Signal<T> {
|
||||||
|
|
||||||
|
var listeners:Array<T>;
|
||||||
|
|
||||||
|
var lock = 0;
|
||||||
|
|
||||||
|
public function new() {
|
||||||
|
removeAll();
|
||||||
|
}
|
||||||
|
|
||||||
|
public inline function removeAll() {
|
||||||
|
listeners = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
public inline function dispose() {
|
||||||
|
listeners = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function add(listener:T):Void {
|
||||||
|
if (listeners.indexOf(listener) == -1) {
|
||||||
|
checkLock();
|
||||||
|
listeners.push(listener);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function remove(listener:T):Void {
|
||||||
|
var pos = listeners.indexOf(listener);
|
||||||
|
if (pos > -1) {
|
||||||
|
checkLock();
|
||||||
|
listeners.splice(pos, 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@:expose inline function checkLock() {
|
||||||
|
if (lock > 0) {
|
||||||
|
listeners = listeners.copy();
|
||||||
|
lock --;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
package bindx;
|
||||||
|
|
||||||
|
@:autoBuild(bindx.BindMacros.buildIBindable())
|
||||||
|
interface IBindable {
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
package bindx;
|
||||||
|
|
||||||
|
import haxe.macro.Expr;
|
||||||
|
import haxe.macro.Type;
|
||||||
|
|
||||||
|
interface IBindingSignalProvider {
|
||||||
|
#if macro
|
||||||
|
function getFieldDispatcher(field:Field, result:Array<Field>):Void;
|
||||||
|
function getFieldChangedExpr(field:Field, oldValue:Expr, newValue:Expr):Expr;
|
||||||
|
|
||||||
|
function getClassFieldBindExpr(expr:Expr, field:ClassField, listener:Expr):Expr;
|
||||||
|
function getClassFieldBindToExpr(expr:Expr, field:ClassField, target:Expr):Expr;
|
||||||
|
function getClassFieldUnbindExpr(expr:Expr, field:ClassField, listener:Expr):Expr;
|
||||||
|
function getClassFieldChangedExpr(expr:Expr, field:ClassField, oldValue:Expr, newValue:Expr):Expr;
|
||||||
|
#end
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package bindx;
|
||||||
|
|
||||||
|
import haxe.macro.Type;
|
||||||
|
import haxe.macro.Expr;
|
||||||
|
import haxe.macro.Context;
|
||||||
|
|
||||||
|
using haxe.macro.Tools;
|
||||||
|
using Lambda;
|
||||||
|
|
||||||
|
class MetaUtils {
|
||||||
|
|
||||||
|
static public inline var BINDABLE_META = ":bindable";
|
||||||
|
|
||||||
|
static public function findParam(meta:MetadataEntry, name:String):Expr {
|
||||||
|
if (meta.params == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (p in meta.params) {
|
||||||
|
switch (p.expr) {
|
||||||
|
case EBinop(OpAssign, e1, e2):
|
||||||
|
if (e1.toString() == name) return {expr:e2.expr, pos:p.pos};
|
||||||
|
case _:
|
||||||
|
Context.warning('Bindable arguments syntax error. Supported syntax: (flag1=true, flag2=false)', p.pos);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
static public inline function bindableMeta(meta:Metadata):MetadataEntry
|
||||||
|
return meta.find(function (it) return it.name == BINDABLE_META);
|
||||||
|
}
|
||||||
|
|
||||||
|
class FieldMetaUtils {
|
||||||
|
static public inline function bindableMeta(field:Field):MetadataEntry
|
||||||
|
return MetaUtils.bindableMeta(field.meta);
|
||||||
|
|
||||||
|
static public inline function hasBindableMeta(field:Field):Bool
|
||||||
|
return bindableMeta(field) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ClassFieldMetaUtils {
|
||||||
|
static public inline function bindableMeta(field:ClassField):MetadataEntry
|
||||||
|
return MetaUtils.bindableMeta(field.meta.get());
|
||||||
|
|
||||||
|
static public inline function hasBindableMeta(field:ClassField):Bool
|
||||||
|
return bindableMeta(field) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ClassTypeMetaUtils {
|
||||||
|
static public inline function bindableMeta(classType:ClassType):MetadataEntry
|
||||||
|
return MetaUtils.bindableMeta(classType.meta.get());
|
||||||
|
|
||||||
|
static public inline function hasBindableMeta(classType:ClassType):Bool
|
||||||
|
return bindableMeta(classType) != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ExprMetaUtils {
|
||||||
|
static public inline function isTrue(expr:Expr):Bool
|
||||||
|
return expr.expr.match(EConst(CIdent("true")));
|
||||||
|
|
||||||
|
static public inline function isFalse(expr:Expr):Bool
|
||||||
|
return expr.expr.match(EConst(CIdent("false")));
|
||||||
|
|
||||||
|
static public inline function isNotNullAndTrue(expr:Expr):Bool
|
||||||
|
return expr != null && isTrue(expr);
|
||||||
|
|
||||||
|
static public inline function isNullOrTrue(expr:Expr):Bool
|
||||||
|
return expr == null || isTrue(expr);
|
||||||
|
}
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
package ;
|
||||||
|
|
||||||
|
import haxe.unit.TestCase;
|
||||||
|
|
||||||
|
class BaseTest extends TestCase {
|
||||||
|
|
||||||
|
public function new() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
function test1() {
|
||||||
|
var b = new Bindable1();
|
||||||
|
b.str = "a";
|
||||||
|
var callNum = 0;
|
||||||
|
b.strChanged.add(function (from, to) {
|
||||||
|
assertEquals(from, "a");
|
||||||
|
assertEquals(to, "b");
|
||||||
|
callNum ++;
|
||||||
|
});
|
||||||
|
|
||||||
|
bindx.Bind.bindx(b.str, function (from, to) {
|
||||||
|
assertEquals(from, "a");
|
||||||
|
assertEquals(to, "b");
|
||||||
|
callNum ++;
|
||||||
|
});
|
||||||
|
b.str = "b";
|
||||||
|
assertEquals(callNum, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function test2() {
|
||||||
|
var b = new Bindable1();
|
||||||
|
b.str = null;
|
||||||
|
var callNum = 0;
|
||||||
|
var listener = function (from, to) {
|
||||||
|
assertEquals(from, null);
|
||||||
|
assertEquals(to, "");
|
||||||
|
callNum ++;
|
||||||
|
}
|
||||||
|
|
||||||
|
b.strChanged.add(listener);
|
||||||
|
bindx.Bind.bindx(b.str, listener);
|
||||||
|
b.str = "";
|
||||||
|
assertEquals(callNum, 1);
|
||||||
|
|
||||||
|
b.strChanged.add(listener);
|
||||||
|
bindx.Bind.unbindx(b.str, listener);
|
||||||
|
b.str = "1";
|
||||||
|
assertEquals(callNum, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function test3() {
|
||||||
|
var b = new Bindable1();
|
||||||
|
b.str = null;
|
||||||
|
var callNum = 0;
|
||||||
|
|
||||||
|
bindx.Bind.bindx(b.str, function (_, _) callNum++);
|
||||||
|
bindx.Bind.bindx(b.str, function (_, _) callNum++);
|
||||||
|
b.str = "";
|
||||||
|
assertEquals(callNum, 2);
|
||||||
|
|
||||||
|
b.strChanged.removeAll();
|
||||||
|
b.str = "1";
|
||||||
|
assertEquals(callNum, 2);
|
||||||
|
|
||||||
|
b.strChanged.dispose();
|
||||||
|
var addError = false;
|
||||||
|
try {
|
||||||
|
b.strChanged.add(function (_, _) {});
|
||||||
|
} catch (e:Dynamic) {
|
||||||
|
addError = true;
|
||||||
|
}
|
||||||
|
assertTrue(addError);
|
||||||
|
}
|
||||||
|
|
||||||
|
function test4() {
|
||||||
|
var b = new Bindable1();
|
||||||
|
b.str = null;
|
||||||
|
var callNum = 0;
|
||||||
|
var listener = function (from, to) {
|
||||||
|
assertEquals(from, "1");
|
||||||
|
assertEquals(to, "2");
|
||||||
|
callNum ++;
|
||||||
|
}
|
||||||
|
b.strChanged.add(listener);
|
||||||
|
b.strChanged.dispatch("1", "2");
|
||||||
|
assertEquals(callNum, 1);
|
||||||
|
|
||||||
|
bindx.Bind.notify(b.str, "1", "2");
|
||||||
|
assertEquals(callNum, 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
function test5() {
|
||||||
|
var b = new Bindable1();
|
||||||
|
b.str = null;
|
||||||
|
var callNum = 0;
|
||||||
|
b.bindChanged.add(function () callNum++);
|
||||||
|
|
||||||
|
b.i = 10;
|
||||||
|
assertEquals(callNum, 1);
|
||||||
|
assertFalse(Reflect.hasField(b, "noBindChanged"));
|
||||||
|
|
||||||
|
bindx.Bind.notify(b.bind);
|
||||||
|
assertEquals(callNum, 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@:bindable
|
||||||
|
class Bindable1 implements bindx.IBindable {
|
||||||
|
|
||||||
|
|
||||||
|
public var str:String;
|
||||||
|
|
||||||
|
@:bindable
|
||||||
|
public var i(default, set):Int;
|
||||||
|
|
||||||
|
@:bindable
|
||||||
|
private var privateVar:Bool;
|
||||||
|
|
||||||
|
public function new() {
|
||||||
|
if (this.privateVarChanged == null)
|
||||||
|
throw "no private binding";
|
||||||
|
}
|
||||||
|
|
||||||
|
function set_i(v) {
|
||||||
|
i = v;
|
||||||
|
bindx.Bind.notify(this.bind);
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function noBind() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@:bindable
|
||||||
|
public function bind() {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package ;
|
||||||
|
|
||||||
|
import bindx.IBindable;
|
||||||
|
|
||||||
|
class InheritanceTest extends haxe.unit.TestCase {
|
||||||
|
public function new() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
function testChild() {
|
||||||
|
var c = new BindableChild();
|
||||||
|
c.i = 0;
|
||||||
|
c.s = "0";
|
||||||
|
var iChanged = 0;
|
||||||
|
c.iChanged.add(function (from, to) {
|
||||||
|
assertEquals(from, 0);
|
||||||
|
assertEquals(to, 1);
|
||||||
|
iChanged ++;
|
||||||
|
});
|
||||||
|
c.i = 1;
|
||||||
|
assertEquals(iChanged, 1);
|
||||||
|
|
||||||
|
var sChanged = 0;
|
||||||
|
c.sChanged.add(function (from, to) {
|
||||||
|
assertEquals(from, "0");
|
||||||
|
assertEquals(to, "1");
|
||||||
|
sChanged ++;
|
||||||
|
});
|
||||||
|
c.s = "1";
|
||||||
|
assertEquals(sChanged, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@:bindable
|
||||||
|
class BindableParent implements IBindable {
|
||||||
|
public function new() {}
|
||||||
|
|
||||||
|
public var i:Int;
|
||||||
|
}
|
||||||
|
|
||||||
|
@:bindable
|
||||||
|
class BindableChild extends BindableParent {
|
||||||
|
|
||||||
|
public var s:String;
|
||||||
|
}
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
package ;
|
||||||
|
|
||||||
|
class TestProperty extends haxe.unit.TestCase {
|
||||||
|
public function new() {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
function test1() {
|
||||||
|
var p = new BindableProperty();
|
||||||
|
p.s = "1";
|
||||||
|
var callNum = 0;
|
||||||
|
|
||||||
|
p.sChanged.add(function (from, to) {
|
||||||
|
assertEquals(from, "1");
|
||||||
|
assertEquals(to, "");
|
||||||
|
callNum ++;
|
||||||
|
});
|
||||||
|
|
||||||
|
p.s = null;
|
||||||
|
|
||||||
|
p.sChanged.removeAll();
|
||||||
|
|
||||||
|
p.sChanged.add(function (from, to) {
|
||||||
|
assertEquals(from, "");
|
||||||
|
assertEquals(to, "1");
|
||||||
|
callNum ++;
|
||||||
|
});
|
||||||
|
p.s = "1";
|
||||||
|
|
||||||
|
assertEquals(callNum, 2);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class BindableProperty implements bindx.IBindable {
|
||||||
|
public function new() {
|
||||||
|
}
|
||||||
|
|
||||||
|
@:bindable
|
||||||
|
public var s(default, set):String;
|
||||||
|
|
||||||
|
function set_s(v) {
|
||||||
|
if (v == null) {
|
||||||
|
return s = "";
|
||||||
|
}
|
||||||
|
s = v;
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package ;
|
||||||
|
|
||||||
|
import haxe.unit.TestRunner;
|
||||||
|
|
||||||
|
class Tests {
|
||||||
|
|
||||||
|
static function main() {
|
||||||
|
var runner = new TestRunner();
|
||||||
|
runner.add(new BaseTest());
|
||||||
|
runner.add(new InheritanceTest());
|
||||||
|
runner.add(new TestProperty());
|
||||||
|
runner.run();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user