Browse Source

added cdb formulas (close #98)

Nicolas Cannasse 4 years ago
parent
commit
320b1d6617
11 changed files with 388 additions and 56 deletions
  1. 1 0
      bin/app.html
  2. 8 0
      bin/cdb.css
  3. 8 0
      bin/cdb.less
  4. 1 0
      bin/defaultProps.json
  5. 3 0
      hide/Ide.hx
  6. 42 35
      hide/comp/ScriptEditor.hx
  7. 34 4
      hide/comp/cdb/Cell.hx
  8. 39 15
      hide/comp/cdb/Editor.hx
  9. 238 0
      hide/comp/cdb/Formulas.hx
  10. 5 0
      hide/comp/cdb/Line.hx
  11. 9 2
      hide/view/Script.hx

+ 1 - 0
bin/app.html

@@ -67,6 +67,7 @@
 	<menu label="Database" class="database">
 		<menu label="View" class="dbView"></menu>
 		<menu label="Custom Types" class="dbCustom"></menu>
+		<menu label="Formulas" class="dbFormulas"></menu>
 		<menu label="Diff">
 			<menu label="Create" class="dbCreateDiff"></menu>
 			<menu label="Load" class="dbLoadDiff"></menu>

+ 8 - 0
bin/cdb.css

@@ -221,6 +221,13 @@
 .cdb .cdb-sheet td.t_float.zero {
   color: #888;
 }
+.cdb .cdb-sheet td.formula {
+  font-style: italic;
+  color: #888;
+}
+.cdb .cdb-sheet td.formula.error {
+  color: #C44;
+}
 .cdb .cdb-sheet td.t_list,
 .cdb .cdb-sheet td.t_properties {
   white-space: nowrap;
@@ -323,6 +330,7 @@
   color: white;
   border: 1px solid black;
   padding: 5px;
+  z-index: 1;
 }
 .cdb .cdb-sheet td.t_file .preview .previewContent .label {
   text-align: center;

+ 8 - 0
bin/cdb.less

@@ -247,6 +247,14 @@
 			color: #888;
 		}
 
+		td.formula {
+			font-style : italic;
+			color : #888;
+			&.error {
+				color : #C44;
+			}
+		}
+
 		td.t_list, td.t_properties {
 			white-space: nowrap;
 			cursor : pointer;

+ 1 - 0
bin/defaultProps.json

@@ -77,6 +77,7 @@
 
 	// cdb config
 	"cdb.databaseFile" : "data.cdb",
+	"cdb.formulasFile" : "formulas.hx",
 	"cdb": {
 		// When file column contains an image - controls whenever it should provide inline preview of the image instead of file path.
 		"inlineImageFiles": false

+ 3 - 0
hide/Ide.hx

@@ -1002,6 +1002,9 @@ class Ide {
 		db.find(".dbCustom").click(function(_) {
 			open("hide.view.CdbCustomTypes",{});
 		});
+		db.find(".dbFormulas").click(function(_) {
+			open("hide.comp.cdb.FormulasView",{ path : config.current.get("cdb.formulasFile") });
+		});
 
 		// layout
 		var layouts = menu.find(".layout .content");

+ 42 - 35
hide/comp/ScriptEditor.hx

@@ -114,41 +114,8 @@ class ScriptChecker {
 			}
 
 			if( api.cdbEnums != null ) {
-				for( c in api.cdbEnums ) {
-					var path = c.split(".");
-					var sname = path.join("@");
-					var objPath = null;
-					if( path.length > 1 ) // might be a scoped id
-						objPath = this.constants.get("cdb.objID").split(":");
-					for( s in ide.database.sheets ) {
-						if( s.name != sname ) continue;
-						var name = path[path.length - 1];
-						name = name.charAt(0).toUpperCase() + name.substr(1);
-						var kname = path.join("_")+"Kind";
-						kname = kname.charAt(0).toUpperCase() + kname.substr(1);
-						if( cdbPack != "" ) kname = cdbPack + "." + kname;
-						var kind = checker.types.resolve(kname);
-						if( kind == null )
-							kind = TEnum({ name : kname, params : [], constructors : [] },[]);
-						var cl : hscript.Checker.CClass = {
-							name : name,
-							params : [],
-							fields : new Map(),
-							statics : new Map()
-						};
-						var refPath = s.idCol.scope == null ? null : objPath.slice(0, s.idCol.scope).join(":")+":";
-						for( o in s.all ) {
-							var id = o.id;
-							if( id == null || id == "" ) continue;
-							if( refPath != null ) {
-								if( !StringTools.startsWith(id, refPath) ) continue;
-								id = id.substr(refPath.length);
-							}
-							cl.fields.set(id, { name : id, params : [], canWrite : false, t : kind, isPublic: true, complete : true });
-						}
-						checker.setGlobal(name, TInst(cl,[]));
-					}
-				}
+				for( c in api.cdbEnums )
+					addCDBEnum(c, cdbPack);
 			}
 
 			if( api.evalTo != null )
@@ -196,6 +163,46 @@ class ScriptChecker {
 		}
 	}
 
+	public function addCDBEnum( name : String, ?cdbPack : String ) {
+		var path = name.split(".");
+		var sname = path.join("@");
+		var objPath = null;
+		if( path.length > 1 ) { // might be a scoped id
+			var objID = this.constants.get("cdb.objID");
+			objPath = objID == null ? [] : objID.split(":");
+		}
+		for( s in ide.database.sheets ) {
+			if( s.name != sname ) continue;
+			var name = path[path.length - 1];
+			name = name.charAt(0).toUpperCase() + name.substr(1);
+			var kname = path.join("_")+"Kind";
+			kname = kname.charAt(0).toUpperCase() + kname.substr(1);
+			if( cdbPack != "" ) kname = cdbPack + "." + kname;
+			var kind = checker.types.resolve(kname);
+			if( kind == null )
+				kind = TEnum({ name : kname, params : [], constructors : [] },[]);
+			var cl : hscript.Checker.CClass = {
+				name : name,
+				params : [],
+				fields : new Map(),
+				statics : new Map()
+			};
+			var refPath = s.idCol.scope == null ? null : objPath.slice(0, s.idCol.scope).join(":")+":";
+			for( o in s.all ) {
+				var id = o.id;
+				if( id == null || id == "" ) continue;
+				if( refPath != null ) {
+					if( !StringTools.startsWith(id, refPath) ) continue;
+					id = id.substr(refPath.length);
+				}
+				cl.fields.set(id, { name : id, params : [], canWrite : false, t : kind, isPublic: true, complete : true });
+			}
+			checker.setGlobal(name, TInst(cl,[]));
+			return kind;
+		}
+		return null;
+	}
+
 	function typeFromValue( value : Dynamic ) : hscript.Checker.TType {
 		switch( std.Type.typeof(value) ) {
 		case TNull:

+ 34 - 4
hide/comp/cdb/Cell.hx

@@ -65,6 +65,17 @@ class Cell extends Component {
 		});
 	}
 
+	function evaluate() {
+		var f = editor.formulas.get(this);
+		if( f == null ) return;
+		var newV : Float = try f.call(line.obj) catch( e : Dynamic ) Math.NaN;
+		if( newV != currentValue ) {
+			currentValue = newV;
+			Reflect.setField(line.obj, column.name, newV);
+			refresh();
+		}
+	}
+
 	function showMenu() {
 		var menu : Array<hide.comp.ContextMenu.ContextMenuItem> = null;
 		switch( column.type ) {
@@ -73,10 +84,27 @@ class Cell extends Component {
 				menu = [
 					{ label : "Goto", click : () -> @:privateAccess editor.gotoReference(this) },
 				];
+		case TInt, TFloat:
+			function setF( f : Formulas.Formula ) {
+				editor.beginChanges();
+				editor.formulas.set(this, f);
+				line.evaluate();
+				editor.endChanges();
+				refresh();
+			}
+			var forms : Array<hide.comp.ContextMenu.ContextMenuItem>;
+			var current = editor.formulas.get(this);
+			forms = [for( f in editor.formulas.getList(this) ) { label : f.name, click : () -> if( f == current ) setF(null) else setF(f), checked : f == current }];
+			forms.push({ label : "New...", click : () -> editor.formulas.createNew(this, setF) });
+			menu = [
+				{ label : "Formula", menu : forms }
+			];
 		default:
 		}
-		if( menu != null )
+		if( menu != null ) {
+			focus();
 			new ContextMenu(menu);
+		}
 	}
 
 	public function canEdit() {
@@ -114,10 +142,12 @@ class Cell extends Component {
 		element.removeClass("edit_long");
 		switch( column.type ) {
 		case TBool:
-			element.removeClass("true false").addClass( value==true ? "true" : "false" );
+			element.toggleClass("true", value == true);
+			element.toggleClass("false", value == false);
 		case TInt, TFloat:
-			element.removeClass("zero");
-			if( value == 0 ) element.addClass("zero");
+			element.toggleClass("zero", value == 0 );
+			element.toggleClass("error", Math.isNaN(value));
+			element.toggleClass("formula", editor.formulas.has(this) );
 		default:
 		}
 	}

+ 39 - 15
hide/comp/cdb/Editor.hx

@@ -43,6 +43,7 @@ class Editor extends Component {
 	public var cursor : Cursor;
 	public var keys : hide.ui.Keys;
 	public var undo : hide.ui.UndoHistory;
+	public var formulas : Formulas;
 
 	public function new(config,api) {
 		super(null,null);
@@ -202,6 +203,11 @@ class Editor extends Component {
 			var out = {};
 			for( x in sel.x1...sel.x2+1 ) {
 				var c = cursor.table.columns[x];
+				var form = @:privateAccess formulas.getFormulaNameFromValue(obj, c);
+				if( form != null ) {
+					Reflect.setField(out, c.name+"__f", form);
+					continue;
+				}
 				var v = Reflect.field(obj, c.name);
 				if( v != null )
 					Reflect.setField(out, c.name, v);
@@ -221,22 +227,24 @@ class Editor extends Component {
 		var columns = cursor.table.columns;
 		var sheet = cursor.table.sheet;
 		var realSheet = cursor.table.getRealSheet();
+
+		var x1 = cursor.x;
+		var y1 = cursor.y;
+		var x2 = cursor.select == null ? x1 : cursor.select.x;
+		var y2 = cursor.select == null ? y1 : cursor.select.y;
+		if( x1 > x2 ) {
+			var tmp = x1;
+			x1 = x2;
+			x2 = tmp;
+		}
+		if( y1 > y2 ) {
+			var tmp = y1;
+			y1 = y2;
+			y2 = tmp;
+		}
+
 		if( clipboard == null || text != clipboard.text ) {
 			if( cursor.x < 0 || cursor.y < 0 ) return;
-			var x1 = cursor.x;
-			var y1 = cursor.y;
-			var x2 = cursor.select == null ? x1 : cursor.select.x;
-			var y2 = cursor.select == null ? y1 : cursor.select.y;
-			if( x1 > x2 ) {
-				var tmp = x1;
-				x1 = x2;
-				x2 = tmp;
-			}
-			if( y1 > y2 ) {
-				var tmp = y1;
-				y1 = y2;
-				y2 = tmp;
-			}
 			beginChanges();
 			for( x in x1...x2+1 ) {
 				var col = columns[x];
@@ -263,9 +271,11 @@ class Editor extends Component {
 					}
 					if( value == null ) continue;
 					var obj = sheet.lines[y];
+					formulas.removeFromValue(obj, col);
 					Reflect.setField(obj, col.name, value);
 				}
 			}
+			formulas.evaluateAll(realSheet);
 			endChanges();
 			realSheet.sync();
 			refreshAll();
@@ -274,7 +284,10 @@ class Editor extends Component {
 		beginChanges();
 		var posX = cursor.x < 0 ? 0 : cursor.x;
 		var posY = cursor.y < 0 ? 0 : cursor.y;
-		for( obj1 in clipboard.data ) {
+		var data = clipboard.data;
+		if( data.length == 1 && y1 != y2 )
+			data = [for( i in y1...y2+1 ) data[0]];
+		for( obj1 in data ) {
 			if( posY == sheet.lines.length ) {
 				if( !cursor.table.canInsert() ) break;
 				sheet.newLine();
@@ -288,6 +301,12 @@ class Editor extends Component {
 				if( !cursor.table.canEditColumn(c2.name) )
 					continue;
 
+				var form = Reflect.field(obj1, c1.name+"__f");
+				if( form != null && c2.type.equals(c2.type) ) {
+					formulas.setForValue(obj2, sheet, c2, form);
+					continue;
+				}
+
 				var f = base.getConvFunction(c1.type, c2.type);
 				var v : Dynamic = Reflect.field(obj1, c1.name);
 				if( f == null )
@@ -307,6 +326,7 @@ class Editor extends Component {
 			}
 			posY++;
 		}
+		formulas.evaluateAll(realSheet);
 		endChanges();
 		realSheet.sync();
 		refreshAll();
@@ -361,7 +381,9 @@ class Editor extends Component {
 			Reflect.deleteField(line.obj, column.name);
 		else
 			Reflect.setField(line.obj, column.name, value);
+		formulas.removeFromValue(line.obj, column);
 		line.table.getRealSheet().updateValue(column, line.index, prev);
+		line.evaluate(); // propagate
 		endChanges();
 	}
 
@@ -561,6 +583,8 @@ class Editor extends Component {
 			cursor.load(c);
 		});
 
+		formulas = new Formulas(this);
+
 		var content = new Element("<table>");
 		tables = [];
 		new Table(this, currentSheet, content, displayMode);

+ 238 - 0
hide/comp/cdb/Formulas.hx

@@ -0,0 +1,238 @@
+package hide.comp.cdb;
+import hscript.Checker;
+
+typedef Formula = { name : String, type : String, call : Dynamic -> Null<Float> }
+
+class Formulas {
+
+	var ide : hide.Ide;
+	var editor : Editor;
+	var formulasFile : String;
+
+	var formulas : Array<Formula> = [];
+	var fmap : Map<String, Map<String, Formula>> = [];
+
+	public function new( editor : Editor ) {
+		ide = hide.Ide.inst;
+		this.editor = editor;
+		formulasFile = editor.config.get("cdb.formulasFile");
+		ide.fileWatcher.register(formulasFile, reloadFile, @:privateAccess editor.searchBox /* hack to handle end-of-life */);
+		load();
+	}
+
+	function reloadFile() {
+		load();
+		evaluateAll();
+		editor.save();
+		Editor.refreshAll();
+	}
+
+	public function evaluateAll( ?sheet : cdb.Sheet ) {
+		for( s in editor.base.sheets ) {
+			if( sheet != null && sheet != s ) continue;
+			var forms = fmap.get(s.name);
+			if( forms == null ) continue;
+			var columns = [for( c in s.columns ) if( c.type == TInt || c.type == TFloat ) c];
+			for( o in s.getLines() ) {
+				for( c in columns ) {
+					var fname = Reflect.field(o,c.name+"__f");
+					if( fname == null ) continue;
+					var f = forms.get(fname);
+					if( f == null ) continue;
+					var v = try f.call(o) catch( e : Dynamic ) Math.NaN;
+					if( v == null )
+						Reflect.deleteField(o, c.name);
+					else
+						Reflect.setField(o, c.name, v);
+				}
+			}
+		}
+	}
+
+	function load() {
+		var code = try sys.io.File.getContent(ide.getPath(formulasFile)) catch( e : Dynamic ) return;
+		var parser = new hscript.Parser();
+		parser.allowTypes = true;
+		var expr = try parser.parseString(code) catch( e : Dynamic ) return;
+
+		var sheetNames = new Map();
+		for( s in editor.base.sheets )
+			if( s.idCol != null )
+				sheetNames.set(getTypeName(s), true);
+		function replaceRec( e : hscript.Expr ) {
+			switch( e.e ) {
+			case EField({ e : EIdent(s) }, name) if( sheetNames.exists(s) ):
+				e.e = EConst(CString(name)); // replace for faster eval
+			default:
+				hscript.Tools.iter(e, replaceRec);
+			}
+		}
+		replaceRec(expr);
+
+		formulas = [];
+		fmap = new Map();
+		var interp = new hscript.Interp();
+		try interp.execute(expr) catch( e : hscript.Expr.Error ) {
+			ide.error(formulasFile+": "+e.toString());
+			return;
+		}
+		function browseRec(expr:hscript.Expr) {
+			switch( expr.e ) {
+			case EBlock(el):
+				for( e in el )
+					browseRec(e);
+			case EFunction([{ t : CTPath([t]) }],_, name) if( name != null && t != null ):
+				var value = interp.variables.get(name);
+				if( value == null ) return;
+				var sname = typeNameToSheet(t);
+				var tmap = fmap.get(sname);
+				if( tmap == null ) {
+					tmap = new Map();
+					fmap.set(sname, tmap);
+				}
+				var f : Formula = { name : name, type : t, call : value };
+				tmap.set(name, f);
+				formulas.push(f);
+			default:
+			}
+		}
+		browseRec(expr);
+	}
+
+	public function getList( c : Cell ) : Array<Formula> {
+		var type = getTypeName(c.table.sheet);
+		return [for( f in formulas ) if( f.type == type ) f];
+	}
+
+	public function get( c : Cell ) : Null<Formula> {
+		var f = Reflect.field(c.line.obj, c.column.name+"__f");
+		if( f == null )
+			return null;
+		var tmap = fmap.get(c.table.sheet.name);
+		if( tmap == null )
+			return null;
+		return tmap.get(f);
+	}
+
+	public function set( c : Cell, f : Formula ) {
+		var obj = c.line.obj;
+		var field = c.column.name+"__f";
+		if( f == null ) {
+			Reflect.deleteField(obj, field);
+			var def = c.table.editor.base.getDefault(c.column,c.table.sheet);
+			if( def == null ) Reflect.deleteField(obj, c.column.name) else Reflect.setField(obj, c.column.name, def);
+		} else
+			Reflect.setField(obj, field, f.name);
+	}
+
+	public inline function has(c:Cell) {
+		return Reflect.field(c.line.obj, c.column.name+"__f") != null;
+	}
+
+	public function removeFromValue( obj : Dynamic, c : cdb.Data.Column ) {
+		Reflect.deleteField(obj, c.name+"__f");
+	}
+
+	public function setForValue( obj : Dynamic, sheet : cdb.Sheet, c : cdb.Data.Column, fname : String ) {
+		Reflect.setField(obj, c.name+"__f", fname);
+		Reflect.deleteField(obj, c.name);
+		var tmap = fmap.get(sheet.name);
+		if( tmap != null ) {
+			var f = tmap.get(fname);
+			if( f != null ) {
+				var v = f.call(obj);
+				if( v != null ) Reflect.setField(obj, c.name, v);
+			}
+		}
+	}
+
+	function getFormulaNameFromValue( obj : Dynamic, c : cdb.Data.Column ) {
+		return Reflect.field(obj, c.name+"__f");
+	}
+
+	public static function getTypeName( s : cdb.Sheet ) {
+		var name = s.name.split("@").join("_");
+		name = name.charAt(0).toUpperCase() + name.substr(1);
+		return name;
+	}
+
+	function typeNameToSheet( t : String ) {
+		for( s in editor.base.sheets )
+			if( getTypeName(s) == t )
+				return s.name;
+		return t;
+	}
+
+	public function createNew( c : Cell, ?onCreated : Formula -> Void ) {
+		var name = ide.ask("Formula name");
+		if( name == null ) return;
+		var t = getTypeName(c.table.sheet);
+		edit('function $name( v : $t ) {\n}\n');
+	}
+
+	public function edit( ?insert : String ) {
+		var fullPath = ide.getPath(formulasFile);
+		var created = false;
+		if( !sys.FileSystem.exists(fullPath) ) {
+			sys.io.File.saveContent(fullPath,"");
+			created = true;
+		}
+		ide.open("hide.comp.cdb.FormulasView",{ path : formulasFile }, (v) -> {
+			var script = @:privateAccess cast(v, hide.view.Script).script;
+			if( insert != null ) {
+				if( !created ) insert = "\n\n"+insert;
+				script.setCode(script.code + insert);
+			}
+		});
+	}
+
+}
+
+class FormulasView extends hide.view.Script {
+
+	override function getScriptChecker() {
+		var check = new hide.comp.ScriptEditor.ScriptChecker(config,"cdb formula");
+		check.checker.allowAsync = false;
+		var skind = new Map();
+		for( s in ide.database.sheets ) {
+			if( s.idCol != null )
+				skind.set(s.name, check.addCDBEnum(s.name.split("@").join(".")));
+		}
+		var tstring = check.checker.types.resolve("String");
+		var cdefs = new Map();
+		for( s in ide.database.sheets ) {
+			var cdef : CClass = {
+				name : Formulas.getTypeName(s),
+				fields : [],
+				statics : [],
+				params : [],
+			};
+			cdefs.set(s.name, cdef);
+		}
+		for( s in ide.database.sheets ) {
+			var cdef = cdefs.get(s.name);
+			for( c in s.columns ) {
+				var t = switch( c.type ) {
+				case TId: skind.get(s.name);
+				case TInt, TColor, TEnum(_), TFlags(_): TInt;
+				case TFloat: TFloat;
+				case TBool: TBool;
+				case TDynamic: TDynamic;
+				case TRef(other): skind.get(other);
+				case TCustom(_), TImage, TLayer(_), TTileLayer, TTilePos: null;
+				case TList, TProperties:
+					var t = TInst(cdefs.get(s.name+"@"+c.name),[]);
+					c.type == TList ? @:privateAccess check.checker.types.getType("Array",[t]) : t;
+				case TString, TFile:
+					tstring;
+				}
+				if( t == null ) continue;
+				cdef.fields.set(c.name, { t : t, name : c.name, isPublic : true, complete : true, canWrite : false, params : [] });
+			}
+			@:privateAccess check.checker.types.types.set(cdef.name, CTClass(cdef));
+		}
+		return check;
+	}
+
+	static var _ = hide.ui.View.register(FormulasView);
+}

+ 5 - 0
hide/comp/cdb/Line.hx

@@ -31,6 +31,11 @@ class Line extends Component {
 		}
 	}
 
+	public function evaluate() {
+		for( c in cells )
+			@:privateAccess c.evaluate();
+	}
+
 	public function hide() {
 		if( subTable != null ) {
 			subTable.close();

+ 9 - 2
hide/view/Script.hx

@@ -6,6 +6,12 @@ class Script extends FileView {
 	var script : hide.comp.ScriptEditor;
 	var originData : String;
 
+	function getScriptChecker() {
+		if( extension != "hx" )
+			return null;
+		return new hide.comp.ScriptEditor.ScriptChecker(config,"hx");
+	}
+
 	override function onDisplay() {
 		element.addClass("script-editor");
 		var lang = switch( extension ) {
@@ -16,8 +22,9 @@ class Script extends FileView {
 		default: "text";
 		}
 		originData = sys.io.File.getContent(getPath());
-		if( extension == "hx" ) {
-			script = new hide.comp.ScriptEditor(originData, new hide.comp.ScriptEditor.ScriptChecker(config,"hx"), element);
+		var checker = getScriptChecker();
+		if( checker != null ) {
+			script = new hide.comp.ScriptEditor(originData, checker, element);
 			script.onSave = function() onSave(script.code);
 			script.onChanged = function() {
 				modified = script.code != originData;