Procházet zdrojové kódy

ProjectSettings: allow user to edit render props and mat libraries from hide

lviguier před 8 měsíci
rodič
revize
a2c2fe372a
5 změnil soubory, kde provedl 495 přidání a 72 odebrání
  1. 1 1
      bin/common.less
  2. 93 3
      bin/style.css
  3. 117 3
      bin/style.less
  4. 8 0
      hide/comp/FileSelect.hx
  5. 276 65
      hide/view/settings/ProjectSettings.hx

+ 1 - 1
bin/common.less

@@ -18,7 +18,7 @@
 @cdb-preview-layer: @popup-layer + 10;
 
 @color-highlight-gray: #969696;
-@color-highlight: #4772b3;
+@color-highlight: #2c5d87;
 @color-border: #3d3d3d;
 @color-border-hover: #3d3d3d;
 

+ 93 - 3
bin/style.css

@@ -3472,6 +3472,12 @@ div.gradient-box {
   height: 20px;
   border-bottom: solid #000000 1px;
 }
+.settings .settings-header .search-bar {
+  margin: 2px 5px 0px 0px;
+  float: right;
+  width: 15%;
+  height: 16px;
+}
 .settings .settings-body {
   width: 100%;
   height: 100%;
@@ -3480,7 +3486,7 @@ div.gradient-box {
 .settings .settings-body .categories {
   width: 20%;
   height: 100%;
-  padding-top: 10px;
+  padding-top: 5px;
   border-right: solid #000000 1px;
 }
 .settings .settings-body .categories p {
@@ -3490,22 +3496,26 @@ div.gradient-box {
 .settings .settings-body .categories p:hover {
   background-color: #4d4d4d;
 }
+.settings .settings-body .categories .selected {
+  background-color: #2c5d87 !important;
+}
 .settings .settings-body .content {
   height: 100%;
   width: 100%;
-  padding: 5px 15px 15px 15px;
+  padding: 0px 15px 15px 10px;
 }
 .settings .settings-body .content h1 {
   margin-top: 0;
+  margin-bottom: 0;
 }
 .settings .settings-body .content dl {
+  display: flex;
   overflow-x: hidden;
   margin: 0px;
 }
 .settings .settings-body .content dd,
 .settings .settings-body .content dt {
   vertical-align: middle;
-  display: inline-block;
   text-wrap: wrap;
   word-break: break-word;
   margin-top: 4px;
@@ -3522,6 +3532,86 @@ div.gradient-box {
   text-transform: capitalize;
   font-weight: normal;
 }
+.settings .settings-body .content dd {
+  flex-grow: 1;
+}
+.project-settings {
+  width: 100%;
+  height: 100%;
+}
+.project-settings h1 {
+  margin: 5px 5px 5px 10px;
+}
+.project-settings .body {
+  width: 100%;
+  height: 100%;
+  display: flex;
+}
+.project-settings .body .left-panel {
+  flex: 0.5;
+  padding: 10px;
+}
+.project-settings .body .left-panel .filter-container {
+  display: flex;
+  margin-bottom: 10px;
+}
+.project-settings .body .left-panel .filter-container p {
+  margin: 0 5px 0 0;
+}
+.project-settings .body .left-panel .filter-container .filter {
+  flex: 1;
+}
+.project-settings .body .right-panel {
+  flex: 1;
+  padding: 10px;
+}
+.project-settings .body .array .rows {
+  min-height: 20px;
+  width: 100%;
+  background: #3f3f3f;
+  border-radius: 2px 2px 0px 2px;
+}
+.project-settings .body .array .rows .row {
+  display: flex;
+  flex-direction: row;
+  width: 100%;
+}
+.project-settings .body .array .rows .row .ico {
+  margin-left: 5px;
+  margin-right: 5px;
+  align-self: center;
+}
+.project-settings .body .array .rows .row input {
+  flex: 1;
+  border: 1px solid #666;
+  border-radius: 5px;
+  box-sizing: border-box;
+  margin: 2px;
+}
+.project-settings .body .array .rows .row:hover {
+  background: #5a5a5a;
+}
+.project-settings .body .array .rows .row.selected {
+  background: #2c5d87;
+}
+.project-settings .body .array .rows .row.filtered {
+  display: none;
+}
+.project-settings .body .array .buttons {
+  float: right;
+  padding: 5px 10px 5px 10px;
+  border-radius: 0px 0px 2px 2px;
+  background: #3f3f3f;
+}
+.project-settings .body .array .buttons .add-btn {
+  margin-right: 10px;
+}
+.project-settings .body .array .buttons div {
+  cursor: pointer;
+}
+.project-settings .body .array .buttons div:hover {
+  color: #e6e6e6;
+}
 .lods .lods-line {
   display: flex;
   overflow: hidden;

+ 117 - 3
bin/style.less

@@ -4052,6 +4052,13 @@ div.gradient-box {
 		width: 100%;
 		height: 20px;
 		border-bottom: solid #000000 1px;
+
+		.search-bar {
+			margin: 2px 5px 0px 0px;
+			float: right;
+			width: 15%;
+			height: 16px;
+		}
 	}
 
 	.settings-body {
@@ -4062,7 +4069,7 @@ div.gradient-box {
 		.categories {
 			width: 20%;
 			height: 100%;
-			padding-top: 10px;
+			padding-top: 5px;
 			border-right: solid #000000 1px;
 
 			p {
@@ -4073,25 +4080,30 @@ div.gradient-box {
 			p:hover {
 				background-color: #4d4d4d;
 			}
+
+			.selected {
+				background-color: @color-highlight !important;
+			}
 		}
 
 		.content {
 			height: 100%;
 			width: 100%;
-			padding: 5px 15px 15px 15px;
+			padding: 0px 15px 15px 10px;
 
 			h1 {
 				margin-top: 0;
+				margin-bottom: 0;
 			}
 
 			dl {
+				display:flex;
 				overflow-x:hidden;
 				margin:0px;
 			}
 
 			dd, dt {
 				vertical-align:middle;
-				display:inline-block;
 				text-wrap: wrap;
 				word-break: break-word;
 				margin-top:4px;
@@ -4109,6 +4121,108 @@ div.gradient-box {
 				text-transform: capitalize;
 				font-weight: normal;
 			}
+
+			dd {
+				flex-grow: 1;
+			}
+		}
+	}
+}
+
+.project-settings {
+	width: 100%;
+	height: 100%;
+
+	h1 {
+		margin: 5px 5px 5px 10px;
+	}
+
+	.body {
+		width: 100%;
+		height:100%;
+		display: flex;
+
+		.left-panel {
+			flex: 0.5;
+			padding: 10px;
+
+			.filter-container {
+				display: flex;
+				margin-bottom: 10px;
+
+				p {
+					margin: 0 5px 0 0;
+				}
+
+				.filter {
+					flex : 1;
+				}
+			}
+		}
+
+		.right-panel {
+			flex: 1;
+			padding: 10px;
+		}
+
+		.array {
+			.rows {
+				min-height: 20px;
+				width: 100%;
+				background: #3f3f3f;
+				border-radius: 2px 2px 0px 2px;
+
+				.row {
+					display: flex;
+					flex-direction: row;
+					width: 100%;
+
+					.ico {
+						margin-left: 5px;
+						margin-right: 5px;
+						align-self : center;
+					}
+
+					input {
+						flex: 1;
+						border: 1px solid #666;
+						border-radius: 5px;
+						box-sizing: border-box;
+						margin: 2px;
+					}
+
+					&:hover {
+						background: #5a5a5a;
+					}
+
+					&.selected {
+						background: @color-highlight;
+					}
+
+					&.filtered {
+						display: none;
+					}
+				}
+			}
+
+			.buttons {
+				float: right;
+				padding: 5px 10px 5px 10px;
+				border-radius: 0px 0px 2px 2px;
+				background: #3f3f3f;
+
+				.add-btn {
+					margin-right: 10px;
+				}
+
+				div {
+					cursor: pointer;
+				}
+
+				div:hover {
+					color: #e6e6e6;
+				}
+			}
 		}
 	}
 }

+ 8 - 0
hide/comp/FileSelect.hx

@@ -15,6 +15,14 @@ class FileSelect extends Component {
 		root.mousedown(function(e) {
 			e.preventDefault();
 			if( e.button == 0 ) {
+				if (extensions == null || extensions.length == 0) {
+					ide.chooseDirectory(function(path) {
+						this.path = path;
+						onChange();
+					}, false, false);
+					return;
+				}
+
 				ide.chooseFile(extensions, function(path) {
 					this.path = path;
 					onChange();

+ 276 - 65
hide/view/settings/ProjectSettings.hx

@@ -1,107 +1,318 @@
 package hide.view.settings;
 
-typedef LocalSettings = {
+typedef LocalSetting = {
+	var folder : String;
 	var file : String;
 	var content : Dynamic;
-	var children : Array<LocalSettings>;
 }
 
-class ProjectSettings extends Settings {
+enum Filter {
+	NONE;
+	LOD;
+	MATLIB;
+	RENDERPROPS;
+}
+
+class ProjectSettings extends hide.ui.View<{}> {
 	public static var SETTINGS_FILE = "props.json";
 
-	var settings : Array<LocalSettings>;
+	public static var MATLIB_ENTRY = "materialLibraries";
+	public static var RENDERPROPS_ENTRY = "scene.renderProps";
+	public static var LOD_ENTRY = "lods.screenRatio";
+
+	var settings : Array<LocalSetting>;
+	var currentFilter = Filter.NONE;
 
 	public function new( ?state ) {
 		super(state);
+		settings = [];
+		getPropsFiles(ide.projectDir);
+	}
+
+	override function getTitle() {
+		return "Project Settings";
+	}
+
+	override function onDisplay() {
+		keys.register("undo", function() undo.undo());
+		keys.register("redo", function() undo.redo());
 
-		var localSettings = getPropsFiles(ide.projectDir);
+		var root = new Element('<div class="project-settings">
+			<h1>Project settings</h1>
+			<div class="body">
+				<div class="left-panel">
+					<h2>Settings</h2>
+					<div class="filter-container">
+						<p>Filter : </p>
+						<select class="filter">
+							<option value="0" selected>None</option>
+							<option value="1">LODs</option>
+							<option value="2">Material libraries</option>
+							<option value="3">Render props</option>
+						</select>
+					</div>
+				</div>
+				<div class="right-panel">
+				</div>
+			</div>
+		</div>').appendTo(element);
 
-		var general = new hide.view.settings.Settings.Categorie("General");
-		categories.push(general);
+		var overridesEl = new Element('<div class="array">
+			<div class="rows">
+			</div>
+			<div class="buttons">
+				<div class="add-btn icon ico ico-plus"></div>
+				<div class="remove-btn icon ico ico-minus"></div>
+			</div>
+		</div>').appendTo(element.find(".left-panel"));
 
-		for (f in Reflect.fields(localSettings[0].content)) {
-			var cat = general;
-			if (f.split('.').length > 1) {
-				var catName = sublimeName(f.split('.')[0]);
-				cat = getCategorie(catName);
-				if (cat == null) {
-					cat = new hide.view.settings.Settings.Categorie(catName);
-					categories.push(cat);
+		function updateOverrides() {
+			var rows = overridesEl.find(".rows");
+			rows.empty();
+
+			for (s in settings) {
+				var row = new Element('<div class="row">
+					<div class="ico ico-circle"></div>
+				</div>').appendTo(rows);
+
+				var filtered = false;
+				if (!currentFilter.match(Filter.NONE)) {
+					filtered = filtered || currentFilter.match(Filter.LOD) && !Reflect.hasField(s.content, LOD_ENTRY);
+					filtered = filtered || currentFilter.match(Filter.MATLIB) && !Reflect.hasField(s.content, MATLIB_ENTRY);
+					filtered = filtered || currentFilter.match(Filter.RENDERPROPS) && !Reflect.hasField(s.content, RENDERPROPS_ENTRY);
 				}
-			}
 
-			var settingName = sublimeName(f.split('.')[f.split('.').length - 1]);
-			var type = Type.typeof(Reflect.field(localSettings[0].content, f));
-			var settingElement = switch (type) {
-				case TClass(String):
-					new Element('<input/>');
-				case TBool:
-					new Element('<input type="checkbox"/>');
-				case TInt, TFloat:
-					new Element('<input type="number"/>');
-				default:
-					new Element('<p>EDITION NOT SUPPORTED</p>');
-			}
+				row.toggleClass('filtered', filtered);
 
-			cat.add(settingName, settingElement, null);
+				row.click(function(e) {
+					overridesEl.find(".selected").removeClass('selected');
+					row.addClass("selected");
+					inspect(s);
+				});
+
+				var fileSelect = new hide.comp.FileSelect(null, row, null);
+				fileSelect.path = s.folder;
+			}
 		}
 
-		categories.sort(function(p1, p2) return (p1.name > p2.name) ? 1 : -1);
-	}
+		updateOverrides();
 
-	override function getTitle() {
-		return "Project Settings";
+		var filterEl = root.find(".filter");
+		filterEl.on('change', function(e) {
+			currentFilter = haxe.EnumTools.createByIndex(Filter, Std.parseInt(filterEl.val()));
+			updateOverrides();
+		});
+
+		overridesEl.find(".add-btn").click(function(e){
+			ide.chooseDirectory(function(path) {
+				var fpath = '${path}/${SETTINGS_FILE}';
+				if (sys.FileSystem.exists(fpath)) {
+					ide.quickError("Settings already exist in this folder");
+					return;
+				}
+				sys.io.File.saveContent(fpath, "{}");
+				settings.push({folder: path, file: fpath, content: {}});
+				updateOverrides();
+			}, true, false);
+		});
+
+		overridesEl.find(".remove-btn").click(function(e){
+			if (settings == null || settings.length == 0)
+				return;
+
+			var selIdx = getSelectionIdxInArray(overridesEl, settings);
+
+			sys.FileSystem.deleteFile(settings[selIdx].file);
+			settings.remove(settings[selIdx]);
+			updateOverrides();
+			element.find(".right-panel").empty();
+		});
 	}
 
-	function getPropsFiles(path: String) : Array<LocalSettings> {
-		var res : Array<LocalSettings> = [];
+	function inspect(s : LocalSetting) {
+		element.find(".right-panel").empty();
 
-		var settingsPath = '${path}/${ProjectSettings.SETTINGS_FILE}';
+		var obj = s.content;
 
-		var localSettings : LocalSettings = null;
-		if (sys.FileSystem.exists(settingsPath)) {
-			var content = sys.io.File.getContent(settingsPath);
-			var obj = try haxe.Json.parse(content) catch( e : Dynamic ) throw "Failed to parse " + settingsPath + "("+e+")";
-			localSettings = { file: settingsPath, content: obj, children: null };
+		function onChange(file : String, oldObj : Dynamic, newObj : Dynamic) {
+			sys.io.File.saveContent(file, haxe.Json.stringify(newObj, '\t'));
+			inspect(s);
+
+			undo.change(Custom(function(undo) {
+				// TODO
+				// var o = oldObj;
+				// var n = newObj;
+				// obj = undo ? o : n;
+				// sys.io.File.saveContent(settings[0].file, haxe.Json.stringify(obj, '\t'));
+				// onDisplay();
+			}));
 		}
 
-		for (f in sys.FileSystem.readDirectory(path)) {
-			if (!sys.FileSystem.isDirectory('${path}/${f}'))
-				continue;
+		// Material library
+		var matLibs : Array<Dynamic> = Reflect.field(obj, MATLIB_ENTRY);
+		var matLibsEl = new Element('<div>
+			<h2>Material libraries</h2>
+			<div class="array">
+				<div class="rows"></div>
+				<div class="buttons">
+					<div class="add-btn icon ico ico-plus"></div>
+					<div class="remove-btn icon ico ico-minus"></div>
+				</div>
+			</div>
+		</div>').appendTo(element.find(".right-panel"));
 
-			var children = getPropsFiles('${path}/${f}');
-			if (children == null)
-				continue;
+		if (matLibs != null) {
+			for (ml in matLibs) {
+				var row = new Element('<div class="row">
+					<div class="ico ico-circle"></div>
+					<input value="${Reflect.field(ml, "name")}"/>
+				</div>').appendTo(matLibsEl.find(".rows"));
+
+				row.click(function(e) {
+					matLibsEl.find(".row").removeClass("selected");
+					row.addClass("selected");
+				});
 
-			res = res.concat(children);
+				var nameInput = row.find("input");
+				nameInput.change(function(e) {
+					var oldObj = Reflect.copy(obj);
+					Reflect.setField(ml, "name", nameInput.val());
+					onChange(s.file, oldObj, obj);
+				});
+
+				var file = new hide.comp.FileSelect(["prefab"], row, null);
+				file.path = Reflect.field(ml, "path");
+				file.onChange = function() {
+					var oldObj = Reflect.copy(obj);
+					Reflect.setField(ml, "path", file.path);
+					onChange(s.file, oldObj, obj);
+				};
+			}
 		}
 
-		if (localSettings == null)
-			return res;
+		matLibsEl.find(".add-btn").click(function(e) {
+			if (matLibs == null) {
+				matLibs = [];
+				Reflect.setField(obj, MATLIB_ENTRY, matLibs);
+			}
 
-		localSettings.children = res;
-		return [localSettings];
-	}
+			var selIdx = getSelectionIdxInArray(matLibsEl, matLibs);
+			var oldObj = Reflect.copy(obj);
+			matLibs.insert(selIdx + 1, {name:"New", path:null});
+			onChange(s.file, oldObj, obj);
+		});
 
-	function sublimeName(string : String) {
-		var res = "";
-		for (cIdx in 0...string.length) {
-			var c = string.charAt(cIdx);
+		matLibsEl.find(".remove-btn").click(function(e) {
+			if (matLibs == null)
+				return;
 
-			if (cIdx == 0) {
-				res += c.toUpperCase();
-				continue;
+			var selIdx = getSelectionIdxInArray(matLibsEl, matLibs);
+			var oldObj = Reflect.copy(obj);
+			matLibs.remove(matLibs[selIdx]);
+			if (matLibs.length == 0) {
+				Reflect.deleteField(obj, MATLIB_ENTRY);
+				matLibs = null;
 			}
+			onChange(s.file, oldObj, obj);
 
-			if (c == c.toUpperCase()) {
-				res += " " + c;
-				continue;
+		});
+
+		// Render props
+		var renderProps : Array<Dynamic> = Reflect.field(obj, RENDERPROPS_ENTRY);
+		var renderPropsEl = new Element('<div>
+			<h2>Render props</h2>
+			<div class="array">
+				<div class="rows"></div>
+				<div class="buttons">
+					<div class="add-btn icon ico ico-plus"></div>
+					<div class="remove-btn icon ico ico-minus"></div>
+				</div>
+			</div>
+		</div>').appendTo(element.find(".right-panel"));
+
+		renderPropsEl.find(".add-btn").click(function(e) {
+			if (renderProps == null) {
+				renderProps = [];
+				Reflect.setField(obj, RENDERPROPS_ENTRY, renderProps);
+			}
+
+			var selIdx = getSelectionIdxInArray(renderPropsEl, renderProps);
+			var oldObj = Reflect.copy(obj);
+			renderProps.insert(selIdx + 1, {name:"New", value:null});
+			onChange(s.file, oldObj, obj);
+		});
+
+		renderPropsEl.find(".remove-btn").click(function(e) {
+			if (renderProps == null)
+				return;
+
+			var selIdx = getSelectionIdxInArray(renderPropsEl, renderProps);
+			var oldObj = Reflect.copy(obj);
+			renderProps.remove(renderProps[selIdx]);
+			if (renderProps.length == 0) {
+				Reflect.deleteField(obj, RENDERPROPS_ENTRY);
+				renderProps = null;
+			}
+			onChange(s.file, oldObj, obj);
+
+		});
+
+		if (renderProps != null) {
+			for (rp in renderProps) {
+				var row = new Element('<div class="row">
+					<div class="ico ico-circle"></div>
+					<input value="${Reflect.field(rp, "name")}"/>
+				</div>').appendTo(renderPropsEl.find(".rows"));
+
+				row.click(function(e) {
+					renderPropsEl.find(".row").removeClass("selected");
+					row.addClass("selected");
+				});
+
+				var nameInput = row.find("input");
+				nameInput.change(function(e) {
+					var oldObj = Reflect.copy(obj);
+					Reflect.setField(rp, "name", nameInput.val());
+					onChange(s.file, oldObj, obj);
+				});
+
+				var file = new hide.comp.FileSelect(["prefab"], row, null);
+				file.path = Reflect.field(rp, "value");
+				file.onChange = function() {
+					var oldObj = Reflect.copy(obj);
+					Reflect.setField(rp, "value", file.path);
+					onChange(s.file, oldObj, obj);
+				};
 			}
+		}
+	}
+
+	function getSelectionIdxInArray(arrEl : Element, arr : Array<Dynamic>) {
+		var selIdx = arr.length - 1;
+			for (idx in 0...arr.length)
+				if (arrEl.find(".row").eq(idx).hasClass("selected"))
+					selIdx = idx;
+			return selIdx;
+	}
+
+	function getPropsFiles(path: String) {
+		var res : Array<LocalSetting> = [];
+		var settingsPath = '${path}/${ProjectSettings.SETTINGS_FILE}';
 
-			res += c;
+		if (sys.FileSystem.exists(settingsPath)) {
+			var content = sys.io.File.getContent(settingsPath);
+			var obj = try haxe.Json.parse(content) catch( e : Dynamic ) throw "Failed to parse " + settingsPath + "("+e+")";
+			var tmp = settingsPath.split('/');
+			tmp.pop();
+			settings.push({folder: tmp.join("/"), file: settingsPath, content: obj});
 		}
 
-		return res;
+		for (f in sys.FileSystem.readDirectory(path)) {
+			if (!sys.FileSystem.isDirectory('${path}/${f}'))
+				continue;
+
+			getPropsFiles('${path}/${f}');
+		}
 	}
 
 	static var _ = hide.ui.View.register(ProjectSettings);