Layout Builder - a tool to make it easy to copy GUIs between projects
-
I'm working on the next version of Rhapsody and wanted to make it easy to take my GUIs from one project to another. I came up with this layout builder script which takes the JSON you can get from the widget tree by pressing J and rebuilds all the widgets.
Now you can already do this of course by copying and pasting the JSON from one project to another, but with this script you can make it modular.
So the idea is maybe you have a set of controls for your preset browser, so you can stick them in one js file. Then you have another set of controls for your settings page, you put those in another js file, etc. Then you pass all the files to the layout builder and it will add the components to your GUI and set all the properties.
/* * Author: David Healey * License: CC0 * Last updated: 08/12/2024 */ namespace LayoutBuilder { const data = []; inline function add(arr: Array) { data.concat(arr); } inline function process() { local stack = data.clone(); while (stack.length > 0) { local node = stack.shift(); if (!isDefined(node.type) || !isDefined(node.id)) continue; addComponent(node.id, node); if (!isDefined(node.childComponents)) continue; for (x in node.childComponents) x.parentComponent = node.id; stack.concat(node.childComponents); } } inline function: object addComponent(id: string, properties: JSON) { local c = Content.getComponent(id); if (!isDefined(c)) { local type = properties.type.replace("Scripted").replace("Script"); switch (type) { case "Slider": c = Content.addKnob(id, properties.x, properties.y); break; case "Button": c = Content.addButton(id, properties.x, properties.y); break; case "Table": c = Content.addTable(id, properties.x, properties.y); break; case "ComboBox": c = Content.addComboBox(id, properties.x, properties.y); break; case "Label": c = Content.addLabel(id, properties.x, properties.y); break; case "Image": c = Content.addImage(id, properties.x, properties.y); break; case "Viewport": c = Content.addViewport(id, properties.x, properties.y); break; case "Panel": c = Content.addPanel(id, properties.x, properties.y); break; case "AudioWaveform": c = Content.addAudioWaveform(id, properties.x, properties.y); break; case "SliderPack": c = Content.addSliderPack(id, properties.x, properties.y); break; case "WebView": c = Content.addWebView(id, properties.x, properties.y); break; case "FloatingTile": c = Content.addFloatingTile(id, properties.x, properties.y); break; } } local allProperties = c.getAllProperties(); for (x in properties) { if (!allProperties.contains(x) || ["id"].contains(x)) continue; c.set(x, properties[x]); } return c; } }
Example usage might be:
include("LayoutBuilder.js"); include("Layouts/PresetPanel.js"); include("Layouts/SettingsPage.js"); LayoutBuilder.process();
The PresetPanel.js file might look something like this:
LayoutBuilder.add([ { "type": "ScriptPanel", "id": "pnlPresetBrowserContainer", "x": 0.0, "y": 50.0, "width": 1000.0, "height": 660.0, "parentComponent": "pnlMain", "locked": "1", "visible": "0", "bgColour": 4280098081, "itemColour": 4280098081, "itemColour2": 4294949932, "textColour": 4291611852, "childComponents": [ { "type": "ScriptFloatingTile", "id": "fltPresetBrowser", "x": 50.0, "y": 18.0, "width": 900.0, "height": 550.0, "parentComponent": "pnlPresetBrowserContainer", "ContentType": "PresetBrowser", "Data": "{\n \"ShowSaveButton\": true,\n \"ShowExpansionsAsColumn\": false,\n \"ShowFolderButton\": true,\n \"ShowNotes\": true,\n \"ShowEditButtons\": true,\n \"EditButtonOffset\": 10,\n \"ShowAddButton\": true,\n \"ShowRenameButton\": true,\n \"ShowDeleteButton\": true,\n \"ShowSearchBar\": true,\n \"ShowFavoriteIcon\": true,\n \"FullPathFavorites\": true,\n \"ButtonsInsideBorder\": true,\n \"NumColumns\": 3,\n \"ColumnWidthRatio\": [\n 0.3333333333333333,\n 0.3333333333333333,\n 0.3333333333333333\n ],\n \"ListAreaOffset\": [\n 0,\n 0,\n 0,\n 0\n ],\n \"ColumnRowPadding\": [\n 10,\n 10,\n 10,\n 10\n ],\n \"SearchBarBounds\": [\n 500,\n 8,\n 350,\n 30\n ],\n \"MoreButtonBounds\": [\n 10,\n 8,\n 30,\n 30\n ],\n \"SaveButtonBounds\": [\n 850,\n 8,\n 50,\n 30\n ],\n \"FavoriteButtonBounds\": [\n 55,\n 8,\n 32,\n 30\n ]\n}", "locked": "1", "bgColour": 4280098081, "itemColour": 4280098081, "itemColour2": 4294949932, "textColour": 4291611852 } ] } ]);
The first time you hit compile you might get component not found errors, this is to be expected. So hit compile again and the errors will go away. And after you've ran the script you can remove all the includes if you want.
-
And I just realised that script won't work unless you're using my fork because I added
Array.shift
. Here's an alternative version that usesArray.pop
which is present in Christoph's source./* * Author: David Healey * License: CC0 * Last updated: 08/12/2024 */ namespace LayoutBuilder { const data = []; inline function add(arr: Array) { data.concat(arr); } inline function process() { local stack = data.clone(); while (stack.length > 0) { //local node = stack.shift(); local node = stack.pop(); if (!isDefined(node.type) || !isDefined(node.id)) continue; addComponent(node.id, node); if (!isDefined(node.childComponents)) continue; for (x in node.childComponents) x.parentComponent = node.id; for (i = node.childComponents.length - 1; i >= 0; i--) { stack.push(node.childComponents[i]); } //stack.concat(node.childComponents); } } inline function: object addComponent(id: string, properties: JSON) { local c = Content.getComponent(id); if (!isDefined(c)) { local type = properties.type.replace("Scripted").replace("Script"); switch (type) { case "Slider": c = Content.addKnob(id, properties.x, properties.y); break; case "Button": c = Content.addButton(id, properties.x, properties.y); break; case "Table": c = Content.addTable(id, properties.x, properties.y); break; case "ComboBox": c = Content.addComboBox(id, properties.x, properties.y); break; case "Label": c = Content.addLabel(id, properties.x, properties.y); break; case "Image": c = Content.addImage(id, properties.x, properties.y); break; case "Viewport": c = Content.addViewport(id, properties.x, properties.y); break; case "Panel": c = Content.addPanel(id, properties.x, properties.y); break; case "AudioWaveform": c = Content.addAudioWaveform(id, properties.x, properties.y); break; case "SliderPack": c = Content.addSliderPack(id, properties.x, properties.y); break; case "WebView": c = Content.addWebView(id, properties.x, properties.y); break; case "FloatingTile": c = Content.addFloatingTile(id, properties.x, properties.y); break; } } local allProperties = c.getAllProperties(); for (x in properties) { if (!allProperties.contains(x) || ["id"].contains(x)) continue; c.set(x, properties[x]); } return c; }
-
@d-healey
Looks great. Thank you! :-) -
@d-healey Thank you! I was just trying to figure out how to approach this in my projects.
I will try integrating this method. I was copying the JSONs as you mentioned, but this looks better.Just to be clear:
In your "Layouts/PresetPanel.js" you have a JSON for the components?
And then you have another .js file included for the interface scripting of the Preset Panel (not shown)? -
@VirtualVirgin said in Layout Builder - a tool to make it easy to copy GUIs between projects:
And then you have another .js file included for the interface scripting of the Preset Panel (not shown)?
Yes. The stuff I posted above just adds the components to the interface, it provides no functionality to those components.