UNPKG

sunrize

Version:

Sunrize — A Multi-Platform X3D Editor

639 lines (516 loc) 22.9 kB
"use strict"; const $ = require ("jquery"), electron = require ("electron"), capitalize = require ("capitalize"), Dialog = require ("../Controls/Dialog"), Tabs = require ("../Controls/Tabs"), Algorithm = require ("../Bits/Algorithm"), Units = require ("./Units"), Editor = require ("../Undo/Editor"), UndoManager = require ("../Undo/UndoManager"), _ = require ("../Application/GetText"); require ("../Fields"); module .exports = new class SceneProperties extends Dialog { constructor () { super ("Sunrize.SceneProperties."); this .setup (); } initialize () { super .initialize (); // Add class. this .element .addClass ("scene-properties"); // Add tabs. this .tabs = new Tabs ($("<div></div>") .attr ("id", "scene-properties-tabs"), "top"); this .tabs .addTextTab ("profile-and-components", _("Profile & Components")); this .tabs .addTextTab ("units", _("Units")); this .tabs .addTextTab ("meta-data", _("Metadata")); this .tabs .addTextTab ("world-info", _("World Info")); this .tabs .setup (); this .tabs .element .appendTo (this .element); // Add panels. this .profileAndComponents = $("#profile-and-components") .addClass ("scrollable"); this .units = $("#units") .addClass ("scrollable"); this .metaData = $("#meta-data") .addClass ("scrollable"); this .worldInfo = $("#world-info") .addClass ("scrollable"); // Profile And Components this .profileAndComponents .table = $("<table></table>") .appendTo (this .profileAndComponents) .height ("100%"); this .profileAndComponents .table .body = $("<tbody></tbody>") .appendTo (this .profileAndComponents .table); this .profileAndComponents .inputs = { }; this .profileAndComponents .inputs .checkbox = $("<input></input>") .attr ("id", "infer-profile-and-components-checkbox") .attr ("type", "checkbox") .on ("click", () => this .toggleInferProfileAndComponents ()); this .profileAndComponents .inputs .profile = $("<select></select>") .on ("change", () => this .changeProfile ()); this .profileAndComponents .components = $("<div></div>") .css ({ "overflow-y": "auto" }); this .profileAndComponents .components .table = $("<table></table>") .appendTo (this .profileAndComponents .components); this .profileAndComponents .components .body = $("<tbody></tbody>") .appendTo (this .profileAndComponents .components .table); for (const profile of this .browser .getSupportedProfiles ()) { $("<option></option>") .text (profile .title) .val (profile .name) .appendTo (this .profileAndComponents .inputs .profile); } for (const component of this .browser .getSupportedComponents ()) { $("<tr></tr>") .append ($("<td></td>") .width ("15px") .append ($("<input></input>") .attr ("type", "checkbox") .attr ("component", component .name) .on ("change", () => this .changeComponents ()))) .append ($("<td></td>") .text (component .title)) .append ($("<td></td>") .width ("15px") .append ($("<input></input>") .attr ("component", component .name) .attr ("level", component .level) .val (component .level) .on ("change", () => this .changeComponents ()))) .appendTo (this .profileAndComponents .components .body); } this .profileAndComponents .checkboxRow = $("<tr></tr>") .append ($("<th></th>")) .append ($("<td></td>") .append (this .profileAndComponents .inputs .checkbox) .append ($("<label></label>") .attr ("for", "infer-profile-and-components-checkbox") .text (_("Infer Profile and Components from Source when Saving")))) .appendTo (this .profileAndComponents .table .body); $("<tr></tr>") .append ($("<th></th>") .text (_("Profile"))) .append ($("<td></td>") .append (this .profileAndComponents .inputs .profile)) .appendTo (this .profileAndComponents .table .body); $("<tr></tr>") .append ($("<th></th>") .text (_("Components"))) .append ($("<td></td>") .append (this .profileAndComponents .components)) .appendTo (this .profileAndComponents .table .body); // Units this .units .table = $("<table></table>") .appendTo (this .units); this .units .table .head = $("<thead></thead>") .appendTo (this .units .table); this .units .table .body = $("<tbody></tbody>") .appendTo (this .units .table); this .units .table .foot = $("<tfoot></tfoot>") .appendTo (this .units .table); this .units .inputs = new Map (Units .map (unit => [unit .category, { name: $("<input></input>") .attr ("list", unit .category), conversionFactor: $("<input></input>") .attr ("category", unit .category), }])); $("<tr></tr>") .append ($("<th></th>")) .append ($("<th></th>") .text (_("Name"))) .append ($("<th></th>") .text (_("Conversion Factor"))) .appendTo (this .units .table .head); for (const units of Units) { $("<tr></tr>") .append ($("<th></th>") .text (capitalize (units .category))) .append ($("<td></td>") .css ("width", "50%") .append (this .units .inputs .get (units .category) .name)) .append ($("<td></td>") .css ("width", "50%") .append (this .units .inputs .get (units .category) .conversionFactor)) .appendTo (this .units .table .body); const datalist = $("<datalist></datalist>") .attr ("id", units .category) .appendTo (this .units); for (const unit of units .units) $("<option></option>") .attr ("value", unit .name) .appendTo (datalist); } this .units .find ("input[list]") .on ("click", function () { if (!$(this) .val ()) return $(this) .attr ("placeholder", $(this) .val ()) $(this) .val ("") }) .on ("mouseleave", function () { if ($(this) .val ()) return $(this) .val ($(this) .attr ("placeholder")) }) .on ("change", event => this .changeUnitName (event)); this .units .find ("input[category]") .on ("change", event => this .changeUnitValue (event)); $("<tr></tr>") .append ($("<th></th>")) .append ($("<th></th>")) .append ($("<th></th>")) .appendTo (this .units .table .foot); // Meta Data this .metaData .table = $("<table></table>") .addClass ("sticky-headers") .appendTo (this .metaData); this .metaData .table .head = $("<thead></thead>") .appendTo (this .metaData .table); this .metaData .table .body = $("<tbody></tbody>") .appendTo (this .metaData .table) .sortable ({ helper (event, tr) { const originals = tr .children (), helper = tr .clone (); helper .children () .each (function (index) { // Set helper cell sizes to match the original sizes. $(this) .width (originals .eq (index) .width ()) .css ("padding-top", "2px"); }); return helper; }, update: (event, ui) => { this .changeMetaData (); }, }); this .metaData .table .foot = $("<tfoot></tfoot>") .appendTo (this .metaData .table); $("<tr></tr>") .append ($("<th></th>") .css ("width", "15px")) .append ($("<th></th>") .addClass ("button") .css ("width", "25%") .append ($("<span></span>") .text (_("Key"))) .append ($("<span></span>") .attr ("title", "Sort column alphabetically.") .addClass (["material-icons", "sort-key"]) .addClass (this .config .file .sortMetaData ? ["active"] : [ ]) .css ("font-size", "inherit") .text ("sort_by_alpha")) .on ("click", (event) => this .sortMetaData (event))) .append ($("<th></th>") .css ("width", "auto") .text (_("Value"))) .append ($("<th></th>") .css ("width", "15px")) .appendTo (this .metaData .table .head); // World Info this .worldInfo .table = $("<table></table>") .height ("100%") .appendTo (this .worldInfo); this .worldInfo .table .body = $("<tbody></tbody>") .appendTo (this .worldInfo .table); this .worldInfo .inputs = { }; this .worldInfo .inputs .checkbox = $("<input></input>") .attr ("id", "world-info-checkbox") .attr ("type", "checkbox") .on ("click", () => this .toggleWorldInfo ()); this .worldInfo .inputs .title = $("<input></input>"); this .worldInfo .inputs .info = $("<textarea></textarea>") .height ("100%") .css ("resize", "none"); this .worldInfo .checkboxRow = $("<tr></tr>") .height ("19.5px") .append ($("<th></th>") .css ("width", "20%")) .append ($("<td></td>") .append (this .worldInfo .inputs .checkbox) .append ($("<label></label>") .attr ("for", "world-info-checkbox") .text (_("World Info")))) .appendTo (this .worldInfo .table .body); $("<tr></tr>") .height ("19.5px") .append ($("<th></th>") .text (_("Title"))) .append ($("<td></td>") .append (this .worldInfo .inputs .title)) .appendTo (this .worldInfo .table .body); $("<tr></tr>") .append ($("<th></th>") .text (_("Info"))) .append ($("<td></td>") .append (this .worldInfo .inputs .info)) .appendTo (this .worldInfo .table .body); } configure () { super .configure ({ size: [600, 400] }); this .config .file .setDefaultValues ({ sortMetaData: false, }); this .updateMetaDataSort () if (this .executionContext) this .onclose (); this .executionContext = this .browser .currentScene; this .onopen (); } onopen () { this .executionContext .profile_changed .addInterest ("updateProfile", this); this .executionContext .components .addInterest ("updateComponents", this); this .executionContext .units .addInterest ("updateUnits", this); this .executionContext .metadata_changed .addInterest ("updateMetaData", this); this .executionContext .getWorldInfos () .addInterest ("updateWorldInfo", this); const app = require ("../Application/Window"); this .profileAndComponents .inputs .checkbox .prop ("checked", app .config .file .inferProfileAndComponents); this .toggleInferProfileAndComponents (); this .updateProfile (); this .updateComponents (); this .updateUnits (); this .updateMetaData (); this .updateWorldInfo (); } onclose () { this .executionContext .profile_changed .removeInterest ("updateProfile", this); this .executionContext .components .removeInterest ("updateComponents", this); this .executionContext .units .removeInterest ("updateUnits", this); this .executionContext .metadata_changed .removeInterest ("updateMetaData", this); this .executionContext .getWorldInfos () .removeInterest ("updateWorldInfo", this); } toggleInferProfileAndComponents () { const app = require ("../Application/Window"); app .config .file .inferProfileAndComponents = this .profileAndComponents .inputs .checkbox .prop ("checked"); if (this .profileAndComponents .inputs .checkbox .prop ("checked")) this .profileAndComponents .checkboxRow .addClass ("disabled"); else this .profileAndComponents .checkboxRow .removeClass ("disabled"); } updateProfile () { const profile = this .executionContext .getProfile (); this .profileAndComponents .inputs .profile .find (`option[value=${profile ? profile .name : "Full"}]`).prop ("selected", true); } changeProfile () { const profile = this .browser .getProfile (this .profileAndComponents .inputs .profile .val ()); Editor .setProfile (this .executionContext, profile); } updateComponents () { this .profileAndComponents .components .find ("input[type=checkbox]:checked") .prop ("checked", false); for (const component of this .executionContext .getComponents ()) { this .profileAndComponents .components .find (`input[type=checkbox][component=${component .name}]`) .prop ("checked", true); this .profileAndComponents .components .find (`input[level][component=${component .name}]`) .val (component .level); } } changeComponents () { const components = Array .from (this .profileAndComponents .components .body .children ()) .map (e => { const element = $(e), checkbox = element .find ("input[type=checkbox]"), name = checkbox .attr ("component"), levelInput = element .find ("input[level]"); if (!checkbox .prop ("checked")) return; const level = parseInt (levelInput .val ()), maxLevel = parseInt (levelInput .attr ("level")); return this .browser .getComponent (name, Algorithm .clamp (level, 1, maxLevel) || maxLevel); }) .filter (v => v); Editor .setComponents (this .executionContext, components); } updateUnits () { for (const unit of this .executionContext .getUnits ()) { const inputs = this .units .inputs .get (unit .category); inputs .name .val (unit .name); inputs .conversionFactor .val (unit .conversionFactor); } } getDefaultUnit (category) { const units = Units .find (units => units .category === category) .units, unit = units .find (unit => unit .conversionFactor === 1); return unit; } changeUnitName (event) { const category = $(event .currentTarget) .attr ("list"), name = this .units .inputs .get (category) .name, conversionFactor = this .units .inputs .get (category) .conversionFactor; if (!name .val ()) name .val (this .getDefaultUnit (category) .name); const units = Units .find (units => units .category === category) .units, unit = units .find (unit => unit .name === name .val ()); if (unit) conversionFactor .val (unit .conversionFactor); Editor .updateUnit (this .executionContext, category, name .val (), conversionFactor .val ()); } changeUnitValue (event) { const category = $(event .currentTarget) .attr ("category"), name = this .units .inputs .get (category) .name, conversionFactor = this .units .inputs .get (category) .conversionFactor; Editor .updateUnit (this .executionContext, category, name .val (), conversionFactor .val ()); } sortMetaData (event) { event .preventDefault (); event .stopPropagation (); this .config .file .sortMetaData = !this .config .file .sortMetaData; this .updateMetaDataSort (); this .updateMetaData (); } updateMetaDataSort () { this .metaData .table .head .find (".sort-key") .removeClass ("active") .addClass (this .config .file .sortMetaData ? ["active"] : [ ]) if (this .config .file .sortMetaData) this .metaData .table .body .sortable ("disable"); else this .metaData .table .body .sortable ("enable"); } updateMetaData () { const scrollTop = this .metaData .table .scrollTop (), scrollLeft = this .metaData .table .scrollLeft (), focusInput = this .metaData .table .body .find ("input:focus"), focusRow = focusInput .closest ("tr"); this .metaData .table .body .empty (); const metaData = Array .from (this .executionContext .getMetaDatas ()), rows = [ ]; for (const [index, [key, value]] of metaData .entries ()) { const row = $("<tr></tr>") .attr ("index", index) .append ($("<td></td>") .append ($("<span></span>") .attr ("title", _("Drag to move key/value pair.")) .css ("font-size", "120%") .addClass (["material-icons", "button", "drag"]) .addClass (this .config .file .sortMetaData ? ["disabled"] : [ ]) .text ("drag_handle"))) .append ($("<td></td>") .append ($("<input></input>") .attr ("index", 0) .attr ("placeholder", _("Insert meta key here.")) .val (key) .on ("change", (event) => this .changeMetaData (event, key)))) .append ($("<td></td>") .addClass ("meta-value") .append ($("<input></input>") .attr ("index", 1) .attr ("placeholder", _("Insert meta value here.")) .val (value) .on ("change", (event) => this .changeMetaData (event, key)))) .append ($("<td></td>") .append ($("<span></span>") .attr ("title", _("Delete key/value pair.")) .css ("font-size", "120%") .addClass (["material-icons", "button"]) .text ("delete_forever") .on ("click", (event) => this .removeMetaData (event, key)))); // Add Open Link in Browser button if it matches a link somewhere in value. { const http = /(https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&\/=]*))/, match = value .match (http); if (match) { const column = row .find ("input[index=1]") .parent (); $("<span></span>") .addClass ("open-link") .attr ("title", _("Open link in web browser.")) .css ("font-size", "120%") .addClass (["material-icons", "button"]) .text ("open_in_new") .appendTo (column) .on ("click", () => electron .shell .openExternal (match [1])); } } rows .push (row); } if (this .config .file .sortMetaData) { rows .sort ((a, b) => { const keyA = $($(a) .find ("input") .get (0)) .val () ?? "", keyB = $($(b) .find ("input") .get (0)) .val () ?? ""; return keyA .localeCompare (keyB); }); } this .metaData .table .body .append (rows); if (focusInput .length) { const input = $(this .metaData .table .body .find (`tr[index=${focusRow .attr ("index")}] input`) .get (focusInput .attr ("index"))); input .trigger ("focus"); } $("<tr></tr>") .append ($("<td></td>")) .append ($("<td></td>") .append ($("<input></input>") .attr ("placeholder", _("Add new meta key.")) .on ("change", event => this .changeMetaData (event, "")))) .append ($("<td></td>") .append ($("<input></input>") .prop ("readonly", true) .on ("change", event => this .changeMetaData (event, "")))) .append ($("<td></td>")) .appendTo (this .metaData .table .foot .empty ()); this .metaData .table .scrollTop (scrollTop); this .metaData .table .scrollLeft (scrollLeft); } changeMetaData (event, oldKey) { let metaData = Array .from (this .metaData .table .find ("tr")); if (arguments .length === 2) { const inputs = $(event .target) .closest ("tr") .find ("input"), key = $(inputs .get (0)) .val () ?.trim (); if (key) UndoManager .shared .beginUndo (_("Change Meta Data »%s«"), key); else UndoManager .shared .beginUndo (_("Remove Meta Data »%s«"), oldKey); metaData .sort ((a, b) => $(a) .attr ("index") - $(b) .attr ("index")); } else { UndoManager .shared .beginUndo (_("Reorder Meta Data")); } metaData = metaData .map (element => { const inputs = $(element) .find ("input"), key = $(inputs .get (0)), value = $(inputs .get (1)); return [key .val () ?.trim (), value .val () ?.trim ()]; }) .filter (([key]) => key); Editor .setMetaData (this .executionContext, metaData); UndoManager .shared .endUndo (); } removeMetaData (event, oldKey) { const inputs = $(event .target) .closest ("tr") .find ("input"), key = $(inputs .get (0)); key .val (""); this .changeMetaData (event, oldKey); } updateWorldInfo () { if (this .worldInfoNode) { this .worldInfo .inputs .title .SFStringInput (); this .worldInfo .inputs .info .MFStringTextArea (); } if (this .executionContext .getWorldInfos () .length) { this .worldInfoNode = this .executionContext .getWorldInfos () .at (-1) .getValue (); this .worldInfo .inputs .title .SFStringInput (this .worldInfoNode, "title"); this .worldInfo .inputs .info .MFStringTextArea (this .worldInfoNode, "info"); this .worldInfo .checkboxRow .removeClass ("disabled"); this .worldInfo .inputs .checkbox .prop ("checked", true); } else { this .worldInfoNode = null; this .worldInfo .checkboxRow .addClass ("disabled"); this .worldInfo .inputs .checkbox .prop ("checked", false); this .worldInfo .inputs .title .val (""); this .worldInfo .inputs .info .val (""); } } toggleWorldInfo () { if (this .worldInfo .inputs .checkbox .prop ("checked")) Editor .addWorldInfo (this .executionContext); else Editor .removeWorldInfo (this .executionContext, this .worldInfoNode); } };