haxe/ui/macros/ComponentMacros.hx
package haxe.ui.macros;
#if macro
import haxe.macro.Context;
import haxe.macro.Expr;
import haxe.macro.ExprTools;
import haxe.macro.TypeTools;
import haxe.ui.core.ComponentClassMap;
import haxe.ui.core.ComponentFieldMap;
import haxe.ui.core.TypeMap;
import haxe.ui.macros.ModuleMacros;
import haxe.ui.macros.helpers.ClassBuilder;
import haxe.ui.macros.helpers.CodeBuilder;
import haxe.ui.macros.helpers.FunctionBuilder;
import haxe.ui.parsers.ui.ComponentInfo;
import haxe.ui.parsers.ui.ComponentParser;
import haxe.ui.parsers.ui.LayoutInfo;
import haxe.ui.parsers.ui.ValidatorInfo;
import haxe.ui.parsers.ui.resolvers.FileResourceResolver;
import haxe.ui.util.EventInfo;
import haxe.ui.util.ExpressionUtil;
import haxe.ui.util.SimpleExpressionEvaluator;
import haxe.ui.util.StringUtil;
import haxe.ui.util.TypeConverter;
import sys.io.File;
typedef NamedComponentDescription = {
var generatedVarName:String;
var type:String;
};
typedef ScriptData = {
var generatedVarName:String;
var eventName:String;
var code:String;
}
typedef BindingData = {
var generatedVarName:String;
var varProp:String;
var bindingExpr:String;
var propType:String;
}
typedef LanguageBindingData = {
var generatedVarName:String;
var varProp:String;
var bindingExpr:String;
}
typedef BuildData = {
@:optional var namedComponents:Map<String, NamedComponentDescription>;
@:optional var bindingExprs:Array<Expr>;
@:optional var scripts:Array<ScriptData>;
@:optional var bindings:Array<BindingData>;
@:optional var languageBindings:Array<LanguageBindingData>;
@:optional var params:Map<String, Dynamic>;
}
#end
@:access(haxe.ui.macros.Macros)
class ComponentMacros {
@:deprecated("'haxe.ui.macros.ComponentMacros.build' is deprecated, use 'haxe.ui.ComponentBuilder.build' instead")
macro public static function build(resourcePath:String, params:Expr = null):Array<Field> {
return buildCommon(resourcePath, params);
}
@:deprecated("'haxe.ui.macros.ComponentMacros.buildComponent' is deprecated, use 'haxe.ui.ComponentBuilder.fromFile' instead")
macro public static function buildComponent(filePath:String, params:Expr = null):Expr {
return buildComponentCommon(filePath, params);
}
@:deprecated("'haxe.ui.macros.ComponentMacros.buildComponentFromString' is deprecated, use 'haxe.ui.ComponentBuilder.fromString' instead")
macro public static function buildComponentFromString(source:String, params:Expr = null):Expr {
return buildFromStringCommon(source, params);
}
macro public static function createInstance(filePath:String):Expr {
var cls = ModuleMacros.createDynamicClass(filePath);
var parts = cls.split(".");
var name:String = parts.pop();
var t:TypePath = {
pack: parts,
name: name
}
return macro new $t();
}
macro public static function cascadeStylesTo(id:Expr, styleProperties:Expr = null, recursiveFind:Null<Bool> = null):Expr {
if (styleProperties == null) {
return macro null;
}
var stylePropertiesArray = MacroHelpers.exprToArray(styleProperties);
if (stylePropertiesArray == null || stylePropertiesArray.length == 0) {
return macro null;
}
var builder = new CodeBuilder();
if (recursiveFind != null) {
builder.add(macro var c = _component.findComponent($e{id}, haxe.ui.core.Component, $v{recursiveFind}));
} else {
builder.add(macro var c = _component.findComponent($e{id}, haxe.ui.core.Component));
}
var propertyExprs = [];
for (prop in stylePropertiesArray) {
propertyExprs.push(macro {
if (style.$prop != null && c.customStyle.$prop != style.$prop) {
c.customStyle.$prop = style.$prop;
invalidate = true;
}
});
}
builder.add(macro if (c != null) {
var invalidate = false;
$b{propertyExprs}
if (invalidate == true) {
c.invalidateComponentStyle();
}
});
return builder.expr;
}
macro public static function cascadeStylesToList(componentType:Expr, styleProperties:Expr = null):Expr {
if (styleProperties == null) {
return macro null;
}
var stylePropertiesArray = MacroHelpers.exprToArray(styleProperties);
if (stylePropertiesArray == null || stylePropertiesArray.length == 0) {
return macro null;
}
var builder = new CodeBuilder();
builder.add(macro var list = _component.findComponents($e{componentType}, 0xffffff));
var propertyExprs = [];
for (prop in stylePropertiesArray) {
propertyExprs.push(macro {
if (style.$prop != null && c.customStyle.$prop != style.$prop) {
c.customStyle.$prop = style.$prop;
invalidate = true;
}
});
}
builder.add(macro for (c in list) {
var invalidate = false;
$b{propertyExprs}
if (invalidate == true) {
c.invalidateComponentStyle();
}
});
return builder.expr;
}
#if macro
private static function buildFromStringCommon(source:String, params:Expr = null):Expr {
#if haxeui_macro_times
var stopTimer = Context.timer("ComponentMacros.buildFromStringCommon");
#end
var builder = new CodeBuilder();
var buildData:BuildData = {
params: MacroHelpers.exprToMap(params)
};
buildComponentFromStringCommon(builder, source, buildData, "rootComponent", true);
buildBindings(builder, buildData, true);
buildLanguageBindings(builder, buildData, true);
builder.add(macro rootComponent);
#if haxeui_macro_times
stopTimer();
#end
return builder.expr;
}
private static function buildComponentCommon(filePath:String, params:Expr = null):Expr {
#if haxeui_macro_times
var stopTimer = Context.timer("ComponentMacros.buildComponentCommon");
#end
var builder = new CodeBuilder();
var buildData:BuildData = {
params: MacroHelpers.exprToMap(params)
};
buildComponentFromFile(null, builder, filePath, buildData, "rootComponent");
buildBindings(builder, buildData, true);
buildLanguageBindings(builder, buildData, true);
builder.add(macro rootComponent);
#if haxeui_macro_times
stopTimer();
#end
return builder.expr;
}
private static function buildCommon(resourcePath:String, params:Expr = null):Array<Field> {
#if haxeui_macro_times
var stopTimer = Context.timer("ComponentMacros.buildCommon");
#end
var pos = haxe.macro.Context.currentPos();
var fields = haxe.macro.Context.getBuildFields();
var builder = new ClassBuilder(Context.getBuildFields(), Context.getLocalType(), Context.currentPos());
#if !haxeui_dont_impose_base_class
if (builder.hasSuperClass("haxe.ui.core.Component") == false) {
Context.error("Must have a superclass of haxe.ui.core.Component", Context.currentPos());
}
#end
Macros.addConstructor(builder);
if (builder.ctor == null) {
Context.error("A class building component must have a constructor", Context.currentPos());
}
var originalRes = resourcePath;
resourcePath = MacroHelpers.resolveFile(resourcePath);
if (resourcePath == null) { // we couldnt find it relative to classpath roots, let see about relative to this class
var relativePath = haxe.io.Path.normalize(builder.pkg.join("/") + "/" + originalRes);
resourcePath = MacroHelpers.resolveFile(relativePath);
}
if (resourcePath == null || sys.FileSystem.exists(resourcePath) == false) {
Context.error('UI markup file "${originalRes}" not found', Context.currentPos());
}
var codeBuilder = new CodeBuilder();
var buildData:BuildData = {
params: MacroHelpers.exprToMap(params)
};
var c:ComponentInfo = buildComponentFromFile(builder, codeBuilder, resourcePath, buildData, "this", false);
var superClass:String = builder.superClass.t.toString();
var rootType = ModuleMacros.resolveComponentClass(c.type, c.namespace);
#if !haxeui_dont_impose_base_class
if (haxe.ui.util.RTTI.hasSuperClass(builder.fullPath, rootType) == false) {
Context.warning('The class hierarchy of "${builder.fullPath}" does not contain the root node of "${resourcePath}" (${rootType}) - this may have unintended consequences', pos);
}
#else
builder.ctor.add(macro applyRootLayout($v{c.type}));
#end
for (id in buildData.namedComponents.keys()) {
var safeId:String = StringUtil.capitalizeHyphens(id);
var varDescription = buildData.namedComponents.get(id);
var cls:String = varDescription.type;
if (!builder.hasVar(safeId)) {
builder.addVar(safeId, TypeTools.toComplexType(Context.getType(cls)));
}
codeBuilder.add(macro
$i{safeId} = $i{varDescription.generatedVarName}
);
}
var resolvedClass:String = "" + Context.getLocalClass();
var alias = resolvedClass.substr(resolvedClass.lastIndexOf(".") + 1, resolvedClass.length).toLowerCase();
ComponentClassMap.register(alias, resolvedClass);
for (expr in buildData.bindingExprs) {
codeBuilder.add(expr);
}
buildBindings(codeBuilder, buildData);
buildLanguageBindings(codeBuilder, buildData);
var bindFields = builder.getFieldsWithMeta("bind");
for (f in bindFields) {
for (n in 0...f.getMetaCount("bind")) { // single method can be bound to multiple events
var meta = f.getMetaByIndex("bind", n);
switch (meta.params) {
case [{expr: EField(variable, field), pos: pos}]: // one param, lets assume binding to component prop
Macros.buildPropertyBinding(builder, f, variable, field);
case [param1]:
Macros.buildPropertyBinding(builder, f, param1, "value"); // input component that has value
}
}
}
builder.ctor.add(codeBuilder, AfterSuper);
#if haxeui_macro_times
stopTimer();
#end
return builder.fields;
}
private static function buildComponentFromFile(classBuilder:ClassBuilder, builder:CodeBuilder, filePath:String, buildData:BuildData = null, rootVarName:String = "this", buildRoot:Bool = true):ComponentInfo {
#if haxeui_macro_times
var stopTimer = Context.timer("ComponentMacros.buildComponentFromFile");
#end
populateBuildData(buildData);
var f = MacroHelpers.resolveFile(filePath);
if (f == null) {
throw "Could not resolve: " + filePath + "(cwd: " + Sys.getCwd() + ", classpath: " + Context.getClassPath() + ")";
}
Context.registerModuleDependency(Context.getLocalModule(), f);
var fileContent:String = StringUtil.replaceVars(File.getContent(f), buildData.params);
var c:ComponentInfo = ComponentParser.get(MacroHelpers.extension(f)).parse(fileContent, new FileResourceResolver(f, buildData.params), filePath);
for (s in c.styles) {
if (s.scope == "global") {
builder.add(macro haxe.ui.Toolkit.styleSheet.parse($v{s.style}, "user"));
}
}
if (buildRoot == true) {
buildComponentNode(builder, c, 0, -1, buildData, false);
builder.add(macro var $rootVarName = c0);
}
var fullScript = "";
for (scriptString in c.scriptlets) {
fullScript += scriptString;
}
if (fullScript.length > 0) {
if (classBuilder != null) {
buildScriptFunctions(classBuilder, builder, buildData.namedComponents, fullScript);
} else {
buildScriptFunctionForwardDeclarations(builder, fullScript);
}
}
var n = 0;
for (child in c.children) {
var componentId = "c" + n;
var r = buildComponentFromInfo(builder, child, buildData, function(componentInfo:ComponentInfo, codeBuilder:CodeBuilder) {
if (componentInfo.condition != null && SimpleExpressionEvaluator.evalCondition(componentInfo.condition) == false) {
return;
}
codeBuilder.add(macro $i{rootVarName}.addComponent($i{componentId}));
for (scriptString in componentInfo.scriptlets) {
fullScript += scriptString;
}
}, n);
n = r;
}
buildScriptHandlers(builder, buildData.namedComponents, buildData.scripts);
if (buildRoot == false) {
if (classBuilder.hasInterface("haxe.ui.core.IDataComponent") == true && c.data != null) {
buildDataSourceCode(builder, c, 'ds_root', "this");
}
buildData.scripts = [];
assignComponentProperties(builder, c, rootVarName, buildData);
if (c.layout != null) {
buildLayoutCode(builder, c, rootVarName);
}
buildScriptHandlers(builder, buildData.namedComponents, buildData.scripts);
}
if (classBuilder == null) {
buildScriptFunctions(classBuilder, builder, buildData.namedComponents, fullScript);
}
builder.add(macro $i{rootVarName}.bindingRoot = true);
#if haxeui_macro_times
stopTimer();
#end
return c;
}
private static function populateBuildData(buildData:BuildData) {
if (buildData == null) {
buildData = { };
}
if (buildData.namedComponents == null) {
buildData.namedComponents = new Map<String, NamedComponentDescription>();
}
if (buildData.scripts == null) {
buildData.scripts = [];
}
if (buildData.bindings == null) {
buildData.bindings = [];
}
if (buildData.languageBindings == null) {
buildData.languageBindings = [];
}
if (buildData.bindingExprs == null) {
buildData.bindingExprs = [];
}
if (buildData.params == null) {
buildData.params = new Map<String, Dynamic>();
}
}
private static function buildComponentFromStringCommon(builder:CodeBuilder, source:String, buildData:BuildData = null, rootVarName:String = "this", buildRoot:Bool = false, classBuilder:ClassBuilder = null):ComponentInfo {
#if haxeui_macro_times
var stopTimer = Context.timer("ComponentMacros.buildComponentFromStringCommon");
#end
populateBuildData(buildData);
source = StringUtil.replaceVars(source, buildData.params);
var c:ComponentInfo = ComponentParser.get("xml").parse(source);
#if haxeui_dont_impose_base_class
builder.add(macro applyRootLayout($v{c.type}));
#end
for (s in c.styles) {
if (s.scope == "global") {
builder.add(macro haxe.ui.Toolkit.styleSheet.parse($v{s.style}, "user"));
}
}
if (buildRoot == true) {
buildComponentNode(builder, c, 0, -1, buildData, false);
builder.add(macro var $rootVarName = c0);
}
if (c.layout != null) {
buildLayoutCode(builder, c, rootVarName);
}
var fullScript = "";
for (scriptString in c.scriptlets) {
fullScript += scriptString;
}
var n = 0;
for (child in c.children) {
var componentId = "c" + n;
var r = buildComponentFromInfo(builder, child, buildData, function(componentInfo:ComponentInfo, codeBuilder:CodeBuilder) {
if (componentInfo.condition != null && SimpleExpressionEvaluator.evalCondition(componentInfo.condition) == false) {
return;
}
codeBuilder.add(macro $i{rootVarName}.addComponent($i{componentId}));
for (scriptString in componentInfo.scriptlets) {
fullScript += scriptString;
}
}, n);
n = r;
}
buildScriptHandlers(builder, buildData.namedComponents, buildData.scripts);
if (classBuilder != null && classBuilder.hasInterface("haxe.ui.core.IDataComponent") == true && c.data != null) {
buildDataSourceCode(builder, c, 'ds_root', rootVarName);
}
assignComponentProperties(builder, c, rootVarName, buildData);
builder.add(macro $i{rootVarName}.bindingRoot = true);
#if haxeui_macro_times
stopTimer();
#end
return c;
}
private static function buildComponentFromInfo(builder:CodeBuilder, c:ComponentInfo, buildData:BuildData, cb:ComponentInfo->CodeBuilder->Void = null, firstId:Int = 0) {
#if haxeui_macro_times
var stopTimer = Context.timer("ComponentMacros.buildComponentFromInfo");
#end
populateBuildData(buildData);
for (s in c.styles) {
if (s.scope == "global") {
builder.add(macro haxe.ui.Toolkit.styleSheet.parse($v{s.style}, "user"));
}
}
var r = buildComponentNode(builder, c, firstId, -1, buildData);
if (cb != null) {
cb(c, builder);
}
#if haxeui_macro_times
stopTimer();
#end
return r;
}
private static function buildScriptFunctionForwardDeclarations(builder:CodeBuilder, script:String) {
var expr = Context.parseInlineString("{" + script + "}", Context.currentPos());
switch (expr.expr) {
case EBlock(exprs):
for (e in exprs) {
switch (e.expr) {
#if haxe4
case EFunction(kind, f):
switch (kind) {
case FNamed(name, inlined):
builder.add(macro var $name = null);
case _:
trace("unsupported " + kind);
}
#else
case EFunction(name, f):
builder.add(macro var $name = null);
#end
case EVars(vars):
for (v in vars) {
var varName = v.name;
builder.add(macro var $varName);
}
case _:
trace("unsupported " + e);
}
}
case _:
trace("unsupported " + expr);
}
}
private static function buildScriptFunctions(classBuilder:ClassBuilder, builder:CodeBuilder, namedComponents:Map<String, NamedComponentDescription>, script:String) {
#if haxeui_macro_times
var stopTimer = Context.timer("ComponentMacros.buildScriptFunctions");
#end
var replaceOverride = new EReg("(override).*function", "gm");
script = replaceOverride.map(script, function(r) {
return StringTools.replace(r.matched(0), "override", "@:override");
});
var replacePrivate = new EReg("(private).*function", "gm");
script = replacePrivate.map(script, function(r) {
return StringTools.replace(r.matched(0), "private", "@:private");
});
var replaceGetSet = new EReg("var .*(get|set).*;", "gm");
script = replaceGetSet.map(script, function(r) {
var s = r.matched(0);
var n1 = s.indexOf("(");
var n2 = s.indexOf(")");
var before = s.substr(0, n1);
var after = s.substr(n2 + 1);
var middle = s.substr(n1, n2);
var modified = before + after;
if (middle.indexOf("get") != -1) {
modified = "@:get " + modified;
}
if (middle.indexOf("set") != -1) {
modified = "@:set " + modified;
}
return modified;
});
var expr = Context.parseInlineString("{" + script + "}", Context.currentPos());
expr = ExprTools.map(expr, replaceShortClassNames);
expr = ExprTools.map(expr, replaceInternalShortNames);
switch (expr.expr) {
case EBlock(exprs):
for (e in exprs) {
buildScriptFunctionsFromExpr(e, classBuilder, builder, namedComponents, null);
}
case _:
trace("unsupported " + expr);
}
#if haxeui_macro_times
stopTimer();
#end
}
private static function buildScriptFunctionsFromExpr(e:Expr, classBuilder:ClassBuilder, builder:CodeBuilder, namedComponents:Map<String, NamedComponentDescription>, metas:Array<MetadataEntry>) {
#if haxeui_macro_times
var stopTimer = Context.timer("ComponentMacros.buildScriptFunctionsFromExpr");
#end
switch (e.expr) {
case EMeta(s, e):
if (metas == null) {
metas = [];
}
metas.push(s);
buildScriptFunctionsFromExpr(e, classBuilder, builder, namedComponents, metas);
#if haxe4
case EFunction(kind, f):
switch (kind) {
case FNamed(name, inlined):
if (classBuilder != null) {
var access = [APublic];
if (metas != null) {
for (m in metas) {
if (m.name == ":private" || m.name == "private") {
access.remove(APublic);
access.push(APrivate);
}
if (m.name == ":override" || m.name == "override") {
access.push(AOverride);
}
}
}
classBuilder.addFunction(name, f.expr, f.args, f.ret, access);
} else {
var functionBuilder = new FunctionBuilder(null, f);
if (namedComponents != null) {
for (namedComponent in namedComponents.keys()) {
var details = namedComponents.get(namedComponent);
functionBuilder.addToStart(macro var $namedComponent = $i{details.generatedVarName});
}
}
var anonFunc:Expr = {
expr: EFunction(FAnonymous, functionBuilder.fn),
pos: Context.currentPos()
}
builder.add(macro $i{name} = $e{anonFunc});
}
case _:
trace("unsupported " + kind);
}
#else
case EFunction(name, f):
if (classBuilder != null) {
classBuilder.addFunction(name, f.expr, f.args, f.ret);
} else {
var functionBuilder = new FunctionBuilder(null, f);
if (namedComponents != null) {
for (namedComponent in namedComponents.keys()) {
var details = namedComponents.get(namedComponent);
functionBuilder.addToStart(macro var $namedComponent = $i{details.generatedVarName});
}
}
/* TODO - not sure how to do this in 3.4.7
var anonFunc:Expr = {
expr: EFunction(FAnonymous, functionBuilder.fn),
pos: Context.currentPos()
}
builder.add(macro $i{name} = $e{anonFunc});
*/
}
#end
case EVars(vars):
for (v in vars) {
if (classBuilder != null) {
var vtype = v.type;
if (vtype == null) {
vtype = macro: Dynamic;
}
var get = null;
var set = null;
if (metas != null) {
for (m in metas) {
if (m.name == ":get" || m.name == "get") {
get = "get";
}
if (m.name == ":set" || m.name == "set") {
set = "set";
}
}
}
if (get == null && set == null) {
classBuilder.addVar(v.name, vtype, v.expr);
} else {
classBuilder.addProp(v.name, vtype, v.expr, get, set);
}
} else {
if (v.expr != null) {
builder.add(macro $i{v.name} = $e{v.expr});
}
}
}
case _:
trace("unsupported " + e);
}
#if haxeui_macro_times
stopTimer();
#end
}
private static function buildLanguageBindings(builder:CodeBuilder, buildData:BuildData, addLocalVars:Bool = false) {
for (languageBinding in buildData.languageBindings) {
assignLanguageBinding(builder, languageBinding, buildData.namedComponents, addLocalVars);
}
}
private static function assignLanguageBinding(builder:CodeBuilder, languageBinding:LanguageBindingData, namedComponents:Map<String, NamedComponentDescription>, addLocalVars:Bool = false) {
#if haxeui_macro_times
var stopTimer = Context.timer("ComponentMacros.assignLanguageBinding");
#end
var fixedExpr = ExpressionUtil.stringToLanguageExpression(languageBinding.bindingExpr);
if (StringTools.endsWith(fixedExpr, ";") == false) {
fixedExpr += ";";
}
var varName = languageBinding.generatedVarName;
var field = languageBinding.varProp;
var expr = Context.parseInlineString("{" + fixedExpr + "}", Context.currentPos());
var exprBuilder = new CodeBuilder(expr);
var dependants:Map<String, Array<String>> = getDependants(expr);
for (dependantName in dependants.keys()) {
if (namedComponents.exists(dependantName) == false) {
continue;
}
var generatedDependantName = namedComponents.get(dependantName).generatedVarName;
var ifBuilder = new CodeBuilder(macro {
});
var propList = dependants.get(dependantName);
for (dependantProp in propList) {
ifBuilder.add(macro if (e.data == $v{dependantProp}) {
//haxe.ui.locale.LocaleManager.instance.refreshFor($i{varName});
haxe.ui.locale.LocaleManager.instance.refreshAll();
});
}
if (addLocalVars == true) {
for (namedComponent in namedComponents.keys()) {
var namedComponentData = namedComponents.get(namedComponent);
ifBuilder.addToStart(macro var $namedComponent = $i{namedComponentData.generatedVarName});
exprBuilder.addToStart(macro var $namedComponent = $i{namedComponentData.generatedVarName});
}
}
builder.add(macro {
$i{generatedDependantName}.registerEvent(haxe.ui.events.UIEvent.PROPERTY_CHANGE, function(e:haxe.ui.events.UIEvent) {
$e{ifBuilder.expr}
});
});
}
builder.add(macro $i{varName}.$field = $e{expr});
builder.add(macro haxe.ui.locale.LocaleManager.instance.registerComponent($i{varName}, $v{field}, function() {
return $e{expr};
}));
#if haxeui_macro_times
stopTimer();
#end
}
private static function buildBindings(builder:CodeBuilder, buildData:BuildData, addLocalVars:Bool = false) {
for (binding in buildData.bindings) {
assignBinding(builder, binding, buildData.namedComponents, addLocalVars);
}
}
private static function buildScriptHandlers(builder:CodeBuilder, namedComponents:Map<String, NamedComponentDescription>, scripts:Array<ScriptData>) {
// generate macro code for event handlers
for (sh in scripts) {
if (sh.eventName != null && sh.generatedVarName != null) {
var fixedCode = sh.code;
if (StringTools.endsWith(fixedCode, ";") == false) {
fixedCode += ";";
}
fixedCode = "{" + fixedCode + "}";
var event = sh.eventName.substr(2);
var scriptBuilder = new CodeBuilder(macro {
var __this__ = $i{sh.generatedVarName};
});
var expr = Context.parseInlineString(fixedCode, Context.currentPos());
var usedVars = [];
expr = ExprTools.map(expr, replaceThis);
expr = ExprTools.map(expr, replaceShortClassNames);
expr = ExprTools.map(expr, replaceInternalShortNames);
expr = ExprTools.map(expr, findVars.bind(usedVars));
for (namedComponent in namedComponents.keys()) {
var details = namedComponents.get(namedComponent);
var safeId:String = StringUtil.capitalizeHyphens(namedComponent);
if (usedVars.indexOf(safeId) == -1) {
continue;
}
scriptBuilder.add(macro var $safeId = $i{details.generatedVarName});
}
scriptBuilder.add(expr);
var eventType = "haxe.ui.events.UIEvent";
if (EventInfo.nameToType.exists(sh.eventName)) {
eventType = EventInfo.nameToType.get(sh.eventName);
}
var eventTypeParts = eventType.split(".");
var eventTypeName = eventTypeParts.pop();
var eventComplexType = ComplexType.TPath({pack: eventTypeParts, name: eventTypeName});
builder.add(macro $i{sh.generatedVarName}.registerEvent($v{event}, function(event:$eventComplexType) { $e{scriptBuilder.expr} }));
}
}
}
private static function replaceThis(e:Expr):Expr {
return switch (e.expr) {
case EConst(CIdent("this")):
{expr: EConst(CIdent("__this__")), pos: e.pos };
case _:
ExprTools.map(e, replaceThis);
}
}
private static function findVars(list:Array<String>, e:Expr):Expr {
switch (e.expr) {
case EConst(CIdent(name)):
list.push(name);
case _:
}
return ExprTools.map(e, findVars.bind(list));
}
private static function replaceShortClassNames(e:Expr):Expr {
return switch (e.expr) {
case ENew(t, params):
var fullPath = t.pack.concat([t.name]).join(".");
var registeredClass = ModuleMacros.resolveComponentClass(fullPath);
var r = e;
if (registeredClass != null) {
r = { expr: ENew({ pack: ["haxe", "ui", "components"], name: "Button", params: t.params, sub: t.sub}, params), pos: e.pos};
}
r;
case _:
ExprTools.map(e, replaceShortClassNames);
}
}
private static function replaceInternalShortNames(e:Expr):Expr {
return switch (e.expr) {
case EConst(CIdent(s)):
var r = e;
if (s == "theme") {
r = macro haxe.ui.themes.ThemeManager.instance;
}
r;
case _:
ExprTools.map(e, replaceInternalShortNames);
}
}
// returns next free id
private static function buildComponentNode(builder:CodeBuilder, c:ComponentInfo, id:Int, parentId:Int, buildData:BuildData, recurseChildren:Bool = true) {
#if macro_times_verbose
var stopTimer = Context.timer("ComponentMacros.buildComponentNode");
#end
if (c.condition != null && SimpleExpressionEvaluator.evalCondition(c.condition) == false) {
#if macro_times_verbose
stopTimer();
#end
return id;
}
var className = ModuleMacros.resolveComponentClass(c.type, c.namespace);
if (className == null) {
#if macro_times_verbose
stopTimer();
#end
Sys.println("WARNING: no class found for component '" + c.type + "' in '" + c.filename + "'");
return id;
}
//var classInfo = new ClassBuilder(Context.getModule(className)[0]);
var classInfo = new ClassBuilder(Context.getType(className));
var useNamedComponents = true;
if (classInfo.hasSuperClass("haxe.ui.core.ItemRenderer")) { // we dont really want to create variable instances of contents of item renderers
useNamedComponents = false;
}
if (classInfo.hasDirectInterface("haxe.ui.core.IDirectionalComponent")) {
var direction = c.direction;
if (direction == null) {
direction = "horizontal"; // default to horizontal
}
var directionalClassName = ModuleMacros.resolveComponentClass(direction + c.type, c.namespace);
if (directionalClassName == null) {
trace("WARNING: no directional class found for component: " + c.type + " (" + (direction + c.type.toLowerCase()) + ")");
#if macro_times_verbose
stopTimer();
#end
return id;
}
var directionalClassInfo = new ClassBuilder(Context.getModule(directionalClassName)[0]); // we want to use get getModule (and therefore create a ref) to ensure macro order is determinate
className = directionalClassName;
}
c.resolvedClassName = className;
var typePath = {
var split = className.split(".");
{ name: split.pop(), pack: split }
};
var componentVarName = 'c${id}';
builder.add(macro var $componentVarName = new $typePath());
if (c.styles.length > 0) {
builder.add(macro $i{componentVarName}.styleSheet = new haxe.ui.styles.StyleSheet());
for (s in c.styles) {
if (s.scope == "local") {
builder.add(macro $i{componentVarName}.styleSheet.parse($v{s.style}));
}
}
}
assignComponentProperties(builder, c, componentVarName, buildData);
if (c.layout != null) {
buildLayoutCode(builder, c, componentVarName);
}
if (classInfo.hasInterface("haxe.ui.core.IDataComponent") == true && c.data != null) {
buildDataSourceCode(builder, c, 'ds${id}', componentVarName);
}
if (c.validators != null) {
buildValidatorCode(builder, c.validators, id);
}
if (c.id != null && buildData.namedComponents != null && useNamedComponents == true) {
var rootComponentInfo = c.findRootComponent();
var rootClassName = ModuleMacros.resolveComponentClass(rootComponentInfo.type, rootComponentInfo.namespace);
var rootClassInfo = new ClassBuilder(Context.getModule(rootClassName)[0]);
if (rootClassInfo.hasField(c.id, true) == false) {
var varDescription = {
generatedVarName: componentVarName,
type: className
};
buildData.namedComponents.set(c.id, varDescription);
} else {
if (classInfo.hasSuperClass("haxe.ui.components.Column") == false) {
trace('WARNING: skipped adding a member variable (${c.id}) that conflicted with a property found on root component (${rootClassName})');
}
}
}
var childId = id + 1;
if (recurseChildren == true) {
for (child in c.children) {
var nc = buildData.namedComponents;
if (useNamedComponents == false) {
nc = null;
}
childId = buildComponentNode(builder, child, childId, id, {
namedComponents: nc,
bindingExprs: buildData.bindingExprs,
bindings: buildData.bindings,
languageBindings: buildData.languageBindings,
scripts: buildData.scripts
});
}
if (parentId != -1) {
builder.add(macro $i{"c" + (parentId)}.addComponent($i{componentVarName}));
}
}
#if macro_times_verbose
stopTimer();
#end
return childId;
}
private static function buildDataSourceCode(builder:CodeBuilder, c:ComponentInfo, dsVarName:String, componentVarName:String) {
var items = new haxe.ui.data.DataSourceFactory<Dynamic>().fromStringToArray(c.dataString);
builder.add(macro var $dsVarName = new haxe.ui.data.ArrayDataSource<Dynamic>());
for (item in items) {
var hasExpression:Bool = false;
// lets first find out if any of the items are expressions
for (f in Reflect.fields(item)) {
var v = Std.string(Reflect.field(item, f));
if (StringTools.startsWith(v, "${") && StringTools.endsWith(v, "}")) {
hasExpression = true;
break;
}
}
if (hasExpression) {
builder.add(macro var __item:Dynamic = {} );
for (f in Reflect.fields(item)) {
var v = Reflect.field(item, f);
var stringValue = Std.string(v);
if (StringTools.startsWith(stringValue, "${") && StringTools.endsWith(stringValue, "}")) {
stringValue = stringValue.substring(2, stringValue.length - 1);
var expr = Context.parseInlineString(stringValue, Context.currentPos());
builder.add(macro __item.$f = $e{expr});
} else {
builder.add(macro __item.$f = $v{v});
}
}
builder.add(macro $i{dsVarName}.add($i{"__item"}));
} else {
builder.add(macro $i{dsVarName}.add($v{item}));
}
}
builder.add(macro ($i{componentVarName} : haxe.ui.core.IDataComponent).dataSource = $i{dsVarName});
}
private static function buildLayoutCode(builder:CodeBuilder, c:ComponentInfo, componentVarName:String) {
var l = c.layout;
var layoutVarName = 'layout_${componentVarName}';
var buildData = {
namedComponents: new Map<String, NamedComponentDescription>(),
bindingExprs: [],
scripts: [],
bindings: []
}
var layoutName = l.type.toLowerCase();
var layoutClass = haxe.ui.layouts.LayoutFactory.lookupClass(layoutName);
if (layoutClass == null) {
Sys.println("WARNING: layout '" + l.type + "' not found for '" + c.type + "' in '" + c.filename + "'");
return;
}
var parts = layoutClass.split(".");
var typePath = {
name: parts.pop(),
pack: parts
}
builder.add(macro var $layoutVarName = new $typePath());
assignProperties(builder, layoutVarName, l.properties, buildData, null);
builder.add(macro $i{componentVarName}.layout = $i{layoutVarName});
}
private static var _nextValidatorId = 0;
private static function buildValidatorCode(builder:CodeBuilder, validators:Array<ValidatorInfo>, id:Int) {
if (validators.length == 0) {
return;
}
var validatorExprs:Array<Expr> = [];
var validatorAssignExprs:Array<Expr> = [];
for (validator in validators) {
var type = validator.type;
var validatorId = 'validator${_nextValidatorId}';
validatorExprs.push(macro @:mergeBlock {
var $validatorId = haxe.ui.validators.ValidatorManager.instance.createValidator($v{type});
});
if (validator.properties != null) {
for (propertyName in validator.properties.keys()) {
var propertyValue = validator.properties.get(propertyName);
var convertedPropertyValue = TypeConverter.convertFrom(propertyValue);
validatorExprs.push(macro $i{validatorId}.setProperty($v{propertyName}, $v{convertedPropertyValue}));
}
}
validatorAssignExprs.push(macro $i{validatorId});
_nextValidatorId++;
}
if (id != -1) {
for (e in validatorExprs) {
builder.add(e);
}
builder.add(macro $i{"c" + (id)}.validators = $a{validatorAssignExprs});
builder.add(macro @:privateAccess $i{"c" + (id)}.validators.component = $i{"c" + (id)});
}
}
private static function assignComponentProperties(builder:CodeBuilder, c:ComponentInfo, componentVarName:String, buildData:BuildData) {
if (c.id != null) assignField(builder, componentVarName, "id", c.id, buildData, c);
if (c.left != null) assignField(builder, componentVarName, "left", c.left, buildData, c);
if (c.top != null) assignField(builder, componentVarName, "top", c.top, buildData, c);
if (c.width != null) assignField(builder, componentVarName, "width", c.width, buildData, c);
if (c.height != null) assignField(builder, componentVarName, "height", c.height, buildData, c);
if (c.percentWidth != null) assignField(builder, componentVarName, "percentWidth", c.percentWidth, buildData, c);
if (c.percentHeight != null) assignField(builder, componentVarName, "percentHeight", c.percentHeight, buildData, c);
if (c.contentWidth != null) assignField(builder, componentVarName, "contentWidth", c.contentWidth, buildData, c);
if (c.contentHeight != null) assignField(builder, componentVarName, "contentHeight", c.contentHeight, buildData, c);
if (c.percentContentWidth != null) assignField(builder, componentVarName, "percentContentWidth", c.percentContentWidth, buildData, c);
if (c.percentContentHeight != null) assignField(builder, componentVarName, "percentContentHeight", c.percentContentHeight, buildData, c);
if (c.text != null) assignField(builder, componentVarName, "text", c.text, buildData, c);
if (c.styleNames != null) assignField(builder, componentVarName, "styleNames", c.styleNames, buildData, c);
if (c.style != null) assignField(builder, componentVarName, "styleString", c.styleString, buildData, c);
assignProperties(builder, componentVarName, c.properties, buildData, c);
}
// We'll re-use the same code for properties and components
// certain things dont actually apply to layouts (namely "ComponentFieldMap", "on" and "${")
// but they shouldnt cause any issues with layouts and the reuse is useful
private static function assignProperties(builder:CodeBuilder, varName:String, properties:Map<String, String>, buildData:BuildData, c:ComponentInfo) {
for (propName in properties.keys()) {
var propValue = properties.get(propName);
propName = ComponentFieldMap.mapField(propName);
if (StringTools.startsWith(propName, "on")) {
if (propValue != null && StringTools.trim(propValue).length > 0) {
buildData.scripts.push({
generatedVarName: varName,
eventName: propName.toLowerCase(),
code: propValue
});
}
} else if (Std.string(propValue).indexOf("${") != -1) {
buildData.bindings.push({
generatedVarName: varName,
varProp: propName,
bindingExpr: propValue,
propType: TypeMap.getTypeInfo(c.resolvedClassName, propName)
});
} else {
if (c != null && c.resolvedClassName != null) {
var propType = null;
var propInfo = haxe.ui.util.RTTI.getClassProperty(c.resolvedClassName, propName);
if (propInfo != null) {
propType = propInfo.propertyType;
}
//var propExpr = macro $v{TypeConverter.convertTo(TypeConverter.convertFrom(propValue), propType)};
var pos = Context.currentPos();
var propExpr = macro @:pos(pos) $v{TypeConverter.convertFrom(propValue)};
builder.add(macro $i{varName}.$propName = $propExpr);
} else {
var propExpr = macro $v{TypeConverter.convertFrom(propValue)};
builder.add(macro $i{varName}.$propName = $propExpr);
}
}
}
}
private static function assignField(builder:CodeBuilder, varName:String, field:String, value:Any, buildData:BuildData, c:ComponentInfo) {
var stringValue = Std.string(value);
if (stringValue.indexOf("${") != -1) {
buildData.bindings.push({
generatedVarName: varName,
varProp: field,
bindingExpr: value,
propType: TypeMap.getTypeInfo(c.resolvedClassName, field)
});
} else if (stringValue.indexOf("{{") != -1 && stringValue.indexOf("}}") != -1) {
buildData.languageBindings.push({
generatedVarName: varName,
varProp: field,
bindingExpr: value,
});
builder.add(macro $i{varName}.$field = $v{value});
} else {
builder.add(macro $i{varName}.$field = $v{value});
}
}
private static function assignBinding(builder:CodeBuilder, bindingData:BindingData, namedComponents:Map<String, NamedComponentDescription>, addLocalVars:Bool = false) {
#if haxeui_macro_times
var stopTimer = Context.timer("ComponentMacros.assignBinding");
#end
var bindingExpr = bindingData.bindingExpr;
var varName = bindingData.generatedVarName;
var varProp = bindingData.varProp;
var propType = bindingData.propType;
if (StringTools.startsWith(bindingExpr, "${") && StringTools.endsWith(bindingExpr, "}")) {
bindingExpr = bindingExpr.substring(2, bindingExpr.length - 1);
}
var expr = Context.parseInlineString(bindingExpr, Context.currentPos());
expr = ExprTools.map(expr, replaceInternalShortNames);
var dependants = getDependants(expr);
var target = varName;
var dependantCount = 0;
for (dependantName in dependants.keys()) {
if (namedComponents.exists(dependantName) == false) {
continue;
}
var generatedDependantName = namedComponents.get(dependantName).generatedVarName;
var ifBuilder = new CodeBuilder(macro {
});
var propList = dependants.get(dependantName);
var initialExprs:Array<Expr> = [];
for (dependantProp in propList) {
if (propType == "string") {
var expr = macro $i{target}.$varProp = Std.string($e{expr});
initialExprs.push(expr);
ifBuilder.add(macro if (e.data == $v{dependantProp}) {
${expr};
});
} else if (propType == "bool") {
var expr = macro $i{target}.$varProp = Std.string($e{expr}) == "true" || Std.string($e{expr}) == "1";
initialExprs.push(expr);
ifBuilder.add(macro if (e.data == $v{dependantProp}) {
${expr}
});
} else if (propType == "float") {
var expr = macro $i{target}.$varProp = Std.parseFloat(Std.string($e{expr}));
initialExprs.push(expr);
ifBuilder.add(macro if (e.data == $v{dependantProp}) {
${expr}
});
} else if (propType == "int") {
var expr = macro $i{target}.$varProp = Std.parseInt(Std.string($e{expr}));
initialExprs.push(expr);
ifBuilder.add(macro if (e.data == $v{dependantProp}) {
${expr}
});
} else {
var expr = macro $i{target}.$varProp = $e{expr};
initialExprs.push(expr);
ifBuilder.add(macro if (e.data == $v{dependantProp}) {
${expr}
});
}
}
if (addLocalVars == true) {
for (namedComponent in namedComponents.keys()) {
var namedComponentData = namedComponents.get(namedComponent);
ifBuilder.addToStart(macro var $namedComponent = $i{namedComponentData.generatedVarName});
}
}
for (expr in initialExprs) {
builder.add(macro ${expr});
}
builder.add(macro {
$i{generatedDependantName}.registerEvent(haxe.ui.events.UIEvent.PROPERTY_CHANGE, function(e:haxe.ui.events.UIEvent) {
$e{ifBuilder.expr}
});
});
dependantCount++;
}
for (dependantName in dependants.keys()) {
if (dependantName == "theme") { // special case for themes - may need to expand on this for other parts of core
builder.add(macro {
var theme = haxe.ui.themes.ThemeManager.instance;
haxe.ui.themes.ThemeManager.instance.registerEvent(haxe.ui.events.ThemeEvent.THEME_CHANGED, function(_) {
$i{target}.$varProp = $e{expr};
});
$i{target}.$varProp = $e{expr};
});
}
}
if (dependantCount == 0) {
if (propType == "string") {
builder.add(macro $i{target}.$varProp = Std.string($e{expr}));
} else {
builder.add(macro $i{target}.$varProp = $e{expr});
}
}
#if haxeui_macro_times
stopTimer();
#end
}
private static function getDependants(expr:Expr):Map<String, Array<String>> {
var dependants:Map<String, Array<String>> = new Map<String, Array<String>>();
var iterateExprs = null;
iterateExprs = function(e:Expr) {
switch (e.expr) {
case EField({ expr: EConst(CIdent(varName)), pos: _}, varField):
var list = dependants.get(varName);
if (list == null) {
list = [];
dependants.set(varName, list);
}
if (list.indexOf(varField) == -1) {
list.push(varField);
}
#if haxe4
case EConst(CString(s, _)):
#else
case EConst(CString(s)):
#end
var n1 = s.indexOf("${");
while (n1 != -1) {
var n2 = s.indexOf("}", n1);
var code = s.substring(n1 + 2, n2);
iterateExprs(Context.parseInlineString(code, Context.currentPos()));
n1 = s.indexOf("${", n2);
}
case _:
ExprTools.iter(e, iterateExprs);
}
}
iterateExprs(expr);
return dependants;
}
#end
}