add addons and swarm engine skeleton

This commit is contained in:
Henrik Neumann 2025-02-17 19:52:35 +01:00
parent 002e6ae41c
commit 80b6b7daab
110 changed files with 8665 additions and 1 deletions

16
.gitignore vendored
View File

@ -1,3 +1,17 @@
# ---> Godot
# Godot 4+ specific ignores # Godot 4+ specific ignores
.godot/ .godot/
/android/
# Godot-specific ignores
.import/
export.cfg
export_presets.cfg
# Imported translations (automatically generated from CSV files)
*.translation
# Mono-specific ignores
.mono/
data_*/
mono_crash.*.json

View File

@ -0,0 +1,18 @@
Copyright (c) Mikael Hermansson and Godot Jolt contributors.
Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -0,0 +1,91 @@
Godot Jolt incorporates third-party material from the projects listed below.
Godot Engine (https://github.com/godotengine/godot)
Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md).
Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to
deal in the Software without restriction, including without limitation the
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
sell copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN THE SOFTWARE.
godot-cpp (https://github.com/godot-jolt/godot-cpp)
Copyright (c) 2017-present Godot Engine contributors.
Copyright (c) 2022-present Mikael Hermansson.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to
deal in the Software without restriction, including without limitation the
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
sell copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN THE SOFTWARE.
Jolt Physics (https://github.com/godot-jolt/jolt)
Copyright (c) 2021 Jorrit Rouwe.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to
deal in the Software without restriction, including without limitation the
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
sell copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN THE SOFTWARE.
mimalloc (https://github.com/godot-jolt/mimalloc)
Copyright (c) 2018-2021 Microsoft Corporation, Daan Leijen.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to
deal in the Software without restriction, including without limitation the
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
sell copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN THE SOFTWARE.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,41 @@
[godot-jolt]
version = "0.14.0-stable"
build = "3392156fab"
[configuration]
entry_symbol = "godot_jolt_main"
compatibility_minimum = "4.3"
compatibility_maximum = "4.3"
[libraries]
windows.release.single.x86_64 = "windows/godot-jolt_windows-x64.dll"
windows.debug.single.x86_64 = "windows/godot-jolt_windows-x64_editor.dll"
windows.release.single.x86_32 = "windows/godot-jolt_windows-x86.dll"
windows.debug.single.x86_32 = "windows/godot-jolt_windows-x86_editor.dll"
linux.release.single.x86_64 = "linux/godot-jolt_linux-x64.so"
linux.debug.single.x86_64 = "linux/godot-jolt_linux-x64_editor.so"
linux.release.single.x86_32 = "linux/godot-jolt_linux-x86.so"
linux.debug.single.x86_32 = "linux/godot-jolt_linux-x86_editor.so"
macos.release.single = "macos/godot-jolt_macos.framework"
macos.debug.single = "macos/godot-jolt_macos_editor.framework"
ios.release.single = "ios/godot-jolt_ios.framework"
ios.debug.single = "ios/godot-jolt_ios_editor.framework"
android.release.single.arm64 = "android/libgodot-jolt_android-arm64.so"
android.debug.single.arm64 = "android/libgodot-jolt_android-arm64_editor.so"
android.release.single.arm32 = "android/libgodot-jolt_android-arm32.so"
android.debug.single.arm32 = "android/libgodot-jolt_android-arm32_editor.so"
android.release.single.x86_64 = "android/libgodot-jolt_android-x64.so"
android.debug.single.x86_64 = "android/libgodot-jolt_android-x64_editor.so"
android.release.single.x86_32 = "android/libgodot-jolt_android-x86.so"
android.debug.single.x86_32 = "android/libgodot-jolt_android-x86_editor.so"

View File

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>godot-jolt_ios</string>
<key>CFBundleName</key>
<string>Godot Jolt</string>
<key>CFBundleDisplayName</key>
<string>Godot Jolt</string>
<key>CFBundleIdentifier</key>
<string>org.godot-jolt.godot-jolt</string>
<key>NSHumanReadableCopyright</key>
<string>Copyright (c) Mikael Hermansson and Godot Jolt contributors.</string>
<key>CFBundleVersion</key>
<string>0.14.0</string>
<key>CFBundleShortVersionString</key>
<string>0.14.0</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleSupportedPlatforms</key>
<array>
<string>iPhoneOS</string>
</array>
<key>CSResourcesFileMapped</key>
<true/>
<key>DTPlatformName</key>
<string>iphoneos</string>
<key>MinimumOSVersion</key>
<string>12.0</string>
<!--
HACK(mihe): This is to work around a bug in Godot 4.3-beta1, where it treats Framework
bundles the same as XCFramework bundles, and expects there to be an `AvailableLibraries`
entry, which is really only a thing in XCFramework bundles. Note that we also lie about the
binary path having a `.dylib` extension in order for Godot to correctly identify this as a
dynamically linked bundle.
-->
<key>AvailableLibraries</key>
<array>
<dict>
<key>BinaryPath</key>
<string>godot-jolt_ios.dylib</string>
</dict>
</array>
</dict>
</plist>

View File

@ -0,0 +1,51 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>godot-jolt_ios_editor</string>
<key>CFBundleName</key>
<string>Godot Jolt</string>
<key>CFBundleDisplayName</key>
<string>Godot Jolt</string>
<key>CFBundleIdentifier</key>
<string>org.godot-jolt.godot-jolt</string>
<key>NSHumanReadableCopyright</key>
<string>Copyright (c) Mikael Hermansson and Godot Jolt contributors.</string>
<key>CFBundleVersion</key>
<string>0.14.0</string>
<key>CFBundleShortVersionString</key>
<string>0.14.0</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleSupportedPlatforms</key>
<array>
<string>iPhoneOS</string>
</array>
<key>CSResourcesFileMapped</key>
<true/>
<key>DTPlatformName</key>
<string>iphoneos</string>
<key>MinimumOSVersion</key>
<string>12.0</string>
<!--
HACK(mihe): This is to work around a bug in Godot 4.3-beta1, where it treats Framework
bundles the same as XCFramework bundles, and expects there to be an `AvailableLibraries`
entry, which is really only a thing in XCFramework bundles. Note that we also lie about the
binary path having a `.dylib` extension in order for Godot to correctly identify this as a
dynamically linked bundle.
-->
<key>AvailableLibraries</key>
<array>
<dict>
<key>BinaryPath</key>
<string>godot-jolt_ios_editor.dylib</string>
</dict>
</array>
</dict>
</plist>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>godot-jolt_macos</string>
<key>CFBundleName</key>
<string>Godot Jolt</string>
<key>CFBundleDisplayName</key>
<string>Godot Jolt</string>
<key>CFBundleIdentifier</key>
<string>org.godot-jolt.godot-jolt</string>
<key>NSHumanReadableCopyright</key>
<string>Copyright (c) Mikael Hermansson and Godot Jolt contributors.</string>
<key>CFBundleVersion</key>
<string>0.14.0</string>
<key>CFBundleShortVersionString</key>
<string>0.14.0</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleSupportedPlatforms</key>
<array>
<string>MacOSX</string>
</array>
<key>CSResourcesFileMapped</key>
<true/>
<key>DTPlatformName</key>
<string>macosx</string>
<key>LSMinimumSystemVersion</key>
<string>10.12</string>
</dict>
</plist>

View File

@ -0,0 +1,128 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>files</key>
<dict>
<key>Resources/Info.plist</key>
<data>
et0C7sxAlu4eIDcq2ihFQ2BhDSk=
</data>
</dict>
<key>files2</key>
<dict>
<key>Resources/Info.plist</key>
<dict>
<key>hash2</key>
<data>
ZnG0hD4DciikOVWrf1Ai1Qedz9hESuIFvUujZAebHRY=
</data>
</dict>
</dict>
<key>rules</key>
<dict>
<key>^Resources/</key>
<true/>
<key>^Resources/.*\.lproj/</key>
<dict>
<key>optional</key>
<true/>
<key>weight</key>
<real>1000</real>
</dict>
<key>^Resources/.*\.lproj/locversion.plist$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>1100</real>
</dict>
<key>^Resources/Base\.lproj/</key>
<dict>
<key>weight</key>
<real>1010</real>
</dict>
<key>^version.plist$</key>
<true/>
</dict>
<key>rules2</key>
<dict>
<key>.*\.dSYM($|/)</key>
<dict>
<key>weight</key>
<real>11</real>
</dict>
<key>^(.*/)?\.DS_Store$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>2000</real>
</dict>
<key>^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/</key>
<dict>
<key>nested</key>
<true/>
<key>weight</key>
<real>10</real>
</dict>
<key>^.*</key>
<true/>
<key>^Info\.plist$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>20</real>
</dict>
<key>^PkgInfo$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>20</real>
</dict>
<key>^Resources/</key>
<dict>
<key>weight</key>
<real>20</real>
</dict>
<key>^Resources/.*\.lproj/</key>
<dict>
<key>optional</key>
<true/>
<key>weight</key>
<real>1000</real>
</dict>
<key>^Resources/.*\.lproj/locversion.plist$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>1100</real>
</dict>
<key>^Resources/Base\.lproj/</key>
<dict>
<key>weight</key>
<real>1010</real>
</dict>
<key>^[^/]+$</key>
<dict>
<key>nested</key>
<true/>
<key>weight</key>
<real>10</real>
</dict>
<key>^embedded\.provisionprofile$</key>
<dict>
<key>weight</key>
<real>20</real>
</dict>
<key>^version\.plist$</key>
<dict>
<key>weight</key>
<real>20</real>
</dict>
</dict>
</dict>
</plist>

View File

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>godot-jolt_macos_editor</string>
<key>CFBundleName</key>
<string>Godot Jolt</string>
<key>CFBundleDisplayName</key>
<string>Godot Jolt</string>
<key>CFBundleIdentifier</key>
<string>org.godot-jolt.godot-jolt</string>
<key>NSHumanReadableCopyright</key>
<string>Copyright (c) Mikael Hermansson and Godot Jolt contributors.</string>
<key>CFBundleVersion</key>
<string>0.14.0</string>
<key>CFBundleShortVersionString</key>
<string>0.14.0</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleSupportedPlatforms</key>
<array>
<string>MacOSX</string>
</array>
<key>CSResourcesFileMapped</key>
<true/>
<key>DTPlatformName</key>
<string>macosx</string>
<key>LSMinimumSystemVersion</key>
<string>10.12</string>
</dict>
</plist>

View File

@ -0,0 +1,128 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>files</key>
<dict>
<key>Resources/Info.plist</key>
<data>
oIAzxlQz4Hun6JnLVOu9jafYxGE=
</data>
</dict>
<key>files2</key>
<dict>
<key>Resources/Info.plist</key>
<dict>
<key>hash2</key>
<data>
FA6I/u5+Ww0DzXAvawYXs792eum+8Bim8uHBbg98jqY=
</data>
</dict>
</dict>
<key>rules</key>
<dict>
<key>^Resources/</key>
<true/>
<key>^Resources/.*\.lproj/</key>
<dict>
<key>optional</key>
<true/>
<key>weight</key>
<real>1000</real>
</dict>
<key>^Resources/.*\.lproj/locversion.plist$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>1100</real>
</dict>
<key>^Resources/Base\.lproj/</key>
<dict>
<key>weight</key>
<real>1010</real>
</dict>
<key>^version.plist$</key>
<true/>
</dict>
<key>rules2</key>
<dict>
<key>.*\.dSYM($|/)</key>
<dict>
<key>weight</key>
<real>11</real>
</dict>
<key>^(.*/)?\.DS_Store$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>2000</real>
</dict>
<key>^(Frameworks|SharedFrameworks|PlugIns|Plug-ins|XPCServices|Helpers|MacOS|Library/(Automator|Spotlight|LoginItems))/</key>
<dict>
<key>nested</key>
<true/>
<key>weight</key>
<real>10</real>
</dict>
<key>^.*</key>
<true/>
<key>^Info\.plist$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>20</real>
</dict>
<key>^PkgInfo$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>20</real>
</dict>
<key>^Resources/</key>
<dict>
<key>weight</key>
<real>20</real>
</dict>
<key>^Resources/.*\.lproj/</key>
<dict>
<key>optional</key>
<true/>
<key>weight</key>
<real>1000</real>
</dict>
<key>^Resources/.*\.lproj/locversion.plist$</key>
<dict>
<key>omit</key>
<true/>
<key>weight</key>
<real>1100</real>
</dict>
<key>^Resources/Base\.lproj/</key>
<dict>
<key>weight</key>
<real>1010</real>
</dict>
<key>^[^/]+$</key>
<dict>
<key>nested</key>
<true/>
<key>weight</key>
<real>10</real>
</dict>
<key>^embedded\.provisionprofile$</key>
<dict>
<key>weight</key>
<real>20</real>
</dict>
<key>^version\.plist$</key>
<dict>
<key>weight</key>
<real>20</real>
</dict>
</dict>
</dict>
</plist>

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,157 @@
// ReSharper disable once CheckNamespace
namespace GodotResourceGroups
{
using Godot;
using System.Collections.Generic;
using System;
public class ResourceGroup
{
private readonly Resource _wrapped;
private ResourceGroup(Resource wrapped)
{
_wrapped = wrapped;
}
/// <summary>
/// Loads a resource group from the given path.
/// </summary>
public static ResourceGroup Of(string path)
{
var wrapped = GD.Load<Resource>(path);
return Of(wrapped);
}
/// <summary>
/// Creates a typesafe wrapper for the given resource group.
/// </summary>
// ReSharper disable once MemberCanBePrivate.Global
public static ResourceGroup Of(Resource wrapped)
{
if (wrapped == null)
{
throw new ArgumentNullException(nameof(wrapped));
}
if (wrapped.GetScript().As<Script>() is not GDScript gdScript ||
!gdScript.ResourcePath.EndsWith("resource_group.gd"))
{
throw new ArgumentException("Resource is not a resource group");
}
return new ResourceGroup(wrapped);
}
/// <summary>
/// Returns all include patterns in this resource group.
/// </summary>
public List<string> Includes => new(_wrapped.Get("includes").AsStringArray());
/// <summary>
/// Returns all exclude patterns in this resource group.
/// </summary>
public List<string> Excludes => new(_wrapped.Get("excludes").AsStringArray());
/// <summary>
/// Returns all paths in this resource group.
/// </summary>
public List<string> Paths => new(_wrapped.Get("paths").AsStringArray());
/// <summary>
/// Loads all resources in this resource group.
/// </summary>
/// <returns></returns>
public List<Resource> LoadAll() => new(_wrapped.Call("load_all").AsGodotObjectArray<Resource>());
/// <summary>
/// Loads all resources in this resource group in the background. The given callback will be called
/// for each resource that is loaded. The callback will be called from the main thread.
/// </summary>
/// <returns>A <see cref="ResourceGroupBackgroundLoader"/> which can be used to cancel the background loading.</returns>
public ResourceGroupBackgroundLoader LoadAllInBackground(
Action<ResourceGroupBackgroundLoader.ResourceLoadingInfo> callback)
{
var godotCallback =
Callable.From<GodotObject>(it => callback(new ResourceGroupBackgroundLoader.ResourceLoadingInfo(it)));
var wrapped = _wrapped.Call("load_all_in_background", godotCallback);
return new ResourceGroupBackgroundLoader(wrapped.AsGodotObject());
}
/// <summary>
/// Loads all resources that match the given include and exclude specification from this resource group in the background.
/// The given callback will be called for each resource that is loaded. The callback will be called from the main thread.
/// </summary>
/// <returns>A <see cref="ResourceGroupBackgroundLoader"/> which can be used to cancel the background loading.</returns>
public ResourceGroupBackgroundLoader LoadMatchingInBackground(IEnumerable<string> includes,
IEnumerable<string> excludes,
Action<ResourceGroupBackgroundLoader.ResourceLoadingInfo> callback)
{
var godotCallback =
Callable.From<GodotObject>(it => callback(new ResourceGroupBackgroundLoader.ResourceLoadingInfo(it)));
var wrapped = _wrapped.Call("__csharp_load_matching_in_background", ToArray(includes), ToArray(excludes),
godotCallback);
return new ResourceGroupBackgroundLoader(wrapped.AsGodotObject());
}
/// <summary>
/// Loads all items of the group into the given collection. If an item is of the wrong
/// type it will be skipped and an error is printed.
/// </summary>
public void LoadAllInto<T>(ICollection<T> destination)
{
var items = _wrapped.Call("load_all").AsGodotObjectArray<Resource>();
PushInto(items, destination);
}
/// <summary>
/// Returns all paths in this resource group that match the given patterns.
/// </summary>
public List<string> GetMatchingPaths(IEnumerable<string> includes, IEnumerable<string> excludes)
=> new(_wrapped.Call("__csharp_get_matching_paths", ToArray(includes), ToArray(excludes)).AsStringArray());
/// <summary>
/// Returns all resources in this resource group that match the given patterns.
/// </summary>
public List<Resource> LoadMatching(IEnumerable<string> includes, IEnumerable<string> excludes)
=> new(_wrapped.Call("__csharp_load_matching", ToArray(includes), ToArray(excludes))
.AsGodotObjectArray<Resource>());
/// <summary>
/// Loads all resources in this resource group that match the given patterns and stores them
/// into the given collection. If an item is of the wrong type it will be skipped and an
/// error is printed.
/// </summary>
public void LoadMatchingInto<T>(ICollection<T> destination, IEnumerable<string> includes,
IEnumerable<string> excludes)
{
var items = _wrapped.Call("__csharp_load_matching", ToArray(includes), ToArray(excludes))
.AsGodotObjectArray<Resource>();
PushInto(items, destination);
}
private static string[] ToArray(IEnumerable<string> enumerable)
{
return enumerable as string[] ?? new List<string>(enumerable).ToArray();
}
private static void PushInto<T>(IEnumerable<Resource> items, ICollection<T> destination)
{
foreach (var item in items)
{
if (item is T t)
{
destination.Add(t);
}
else
{
GD.PushError("Item ", item, " is not of required type ", typeof(T).Namespace, ". Skipping.");
}
}
}
}
}

View File

@ -0,0 +1,50 @@
using Godot;
// ReSharper disable once CheckNamespace
namespace GodotResourceGroups;
/// <summary>
/// A background loader for resource groups. Use <see cref="ResourceGroup.LoadAllInBackground"/> or
/// <see cref="ResourceGroup.LoadMatchingInBackground"/>to create an instance of this class.
/// </summary>
public class ResourceGroupBackgroundLoader
{
private readonly GodotObject _wrapped;
internal ResourceGroupBackgroundLoader(GodotObject wrapped)
{
_wrapped = wrapped;
}
/// <summary>
/// Cancels the background loading. The callback will not be called anymore after this method returns.
/// </summary>
public void Cancel()
{
_wrapped.Call("cancel");
}
/// <summary>
/// Returns true if the background loading is done. Will also return true if the loading was cancelled.
/// </summary>
public bool IsDone()
{
return _wrapped.Call("is_done").As<bool>();
}
public readonly struct ResourceLoadingInfo
{
private readonly GodotObject _wrapped;
public ResourceLoadingInfo(GodotObject wrapped)
{
_wrapped = wrapped;
}
public bool Success => _wrapped.Get("success").As<bool>();
public string Path => _wrapped.Get("path").As<string>();
public Resource Resource => _wrapped.Get("resource").As<Resource>();
public float Progress => _wrapped.Get("progress").As<float>();
public bool Last => _wrapped.Get("last").As<bool>();
}
}

View File

@ -0,0 +1,62 @@
@tool
extends EditorPlugin
const ResourceScanner = preload("resource_scanner.gd")
const ResourceGroupScanner = preload("resource_group_scanner.gd")
const ResourceGroupsExportPlugin = preload("godot_resource_groups_export_plugin.gd")
const REBUILD_SETTING: StringName = "godot_resource_groups/auto_rebuild"
var _group_scanner: ResourceGroupScanner
var _export_plugin: ResourceGroupsExportPlugin
func _enter_tree():
add_tool_menu_item("Rebuild project resource groups", _rebuild_resource_groups)
_group_scanner = ResourceGroupScanner.new(get_editor_interface().get_resource_filesystem())
# try to get the setting, if it doesn't exist, set it to true
var auto_rebuild: bool = ProjectSettings.get_setting(REBUILD_SETTING, true)
# make sure it is there
ProjectSettings.set_setting("godot_resource_groups/auto_rebuild", auto_rebuild)
# add property info so it shows up in the editor
ProjectSettings.add_property_info({
"name": "godot_resource_groups/auto_rebuild",
"description": "Automatically rebuild resource groups when the project is built.",
"type": TYPE_BOOL,
})
# register the export plugin
_export_plugin = ResourceGroupsExportPlugin.new(_rebuild_resource_groups)
add_export_plugin(_export_plugin)
func _exit_tree():
remove_tool_menu_item("Rebuild project resource groups")
remove_export_plugin(_export_plugin)
func _build() -> bool:
# always read the setting to make sure it is up to date
var auto_rebuild: bool = ProjectSettings.get_setting(REBUILD_SETTING, true)
if auto_rebuild:
_rebuild_resource_groups()
return true
func _rebuild_resource_groups():
var start = Time.get_unix_time_from_system()
var groups = _group_scanner.scan()
for group in groups:
var resource_scanner = ResourceScanner.new(group, get_editor_interface().get_resource_filesystem())
var resource_paths = resource_scanner.scan()
group.paths = resource_paths
ResourceSaver.save(group)
get_editor_interface().get_resource_filesystem().update_file(group.resource_path)
var end = Time.get_unix_time_from_system()
print("Rebuilt resource groups in %.2f seconds." % [end-start])

View File

@ -0,0 +1,14 @@
@tool
extends EditorExportPlugin
var _on_export:Callable
func _init(on_export:Callable):
_on_export = on_export
func _export_begin(features, is_debug, path, flags):
_on_export.call()
func _get_name() -> String:
return "Godot Resource Groups Export Plugin"

View File

@ -0,0 +1,140 @@
@tool
var _include_regexes:Array[RegEx]
var _exclude_regexes:Array[RegEx]
func _init(base_folder:String, include_patterns:Array[String], exclude_patterns:Array[String]):
# compile the include and exclude patterns to regular expressions, so we don't
# have to do it for each file
_include_regexes = []
_exclude_regexes = []
for pattern in include_patterns:
if pattern == "" or pattern == null:
continue
_include_regexes.append(_compile_pattern(base_folder, pattern))
for pattern in exclude_patterns:
if pattern == "" or pattern == null:
continue
_exclude_regexes.append(_compile_pattern(base_folder, pattern))
## Compiles the given pattern to a regular expression.
func _compile_pattern(base_folder:String, pattern:String) -> RegEx:
# ** - matches zero or more characters (including "/")
# * - matches zero or more characters (excluding "/")
# ? - matches one character
# we convert the pattern to a regular expression
# ** becomes .*
# * becomes [^/]* (any number of characters except /)
# ? becomes [^/] (any character except /)
# all other characters are escaped
# the pattern is anchored at the beginning and end of the string
# the pattern is case-sensitive
var regex = "^" + _escape_string(base_folder) + "/"
var i = 0
var len = pattern.length()
while i < len:
var c = pattern[i]
if c == "*":
if i + 1 < len and pattern[i + 1] == "*":
# ** - matches zero or more characters (including "/")
regex += ".*"
i += 2
else:
# * - matches zero or more characters (excluding "/")
regex += "[^\\/]*"
i += 1
elif c == "?":
# ? - matches one character
regex += "[^\\/]"
i += 1
else:
# escape all other characters
regex += _escape_character(c)
i += 1
regex += "$"
var result = RegEx.new()
result.compile(regex)
return result
func _escape_string(c:String) -> String:
var result = ""
for i in len(c):
result += _escape_character(c[i])
return result
## Escapes the given character for use in a regular expression.
## No clue why this is not built-in.
func _escape_character(c:String) -> String:
if c == "\\":
return "\\\\"
elif c == "^":
return "\\^"
elif c == "$":
return "\\$"
elif c == ".":
return "\\."
elif c == "|":
return "\\|"
elif c == "?":
return "\\?"
elif c == "*":
return "\\*"
elif c == "+":
return "\\+"
elif c == "(":
return "\\("
elif c == ")":
return "\\)"
elif c == "{":
return "\\{"
elif c == "}":
return "\\}"
elif c == "[":
return "\\["
elif c == "]":
return "\\]"
elif c == "/":
return "\\/"
else:
return c
func matches(file:String) -> bool:
# the group definition has a list of include and exclude patterns
# if the list of include patterns is empty, all files match
# any file that matches an exclude pattern is excluded
# we allow * as a wildcard for a single path segment
# we allow ** as a wildcard for multiple path segments
if _include_regexes.size() > 0:
var found = false
# the file must match at least one include pattern
for item in _include_regexes:
if item.search(file) != null:
found = true
break
if not found:
if file.contains(".txt"):
print("file ", file , " did not match any regex")
return false
# the file must not match any exclude pattern
for item in _exclude_regexes:
if item.search(file) != null:
if file.contains(".txt"):
print("file ", file , " was excluded ")
return false
return true

View File

@ -0,0 +1,7 @@
[plugin]
name="Godot Resource Groups"
description="A plugin which helps you to conveniently access all resources in your project without having to hardcode any paths."
author="Jan Thomä"
version="0.4.1"
script="godot_resource_groups.gd"

View File

@ -0,0 +1,103 @@
@tool
## A resource group is a set of resources in the project
## that you want to access at runtime.
@icon("resource_group.svg")
class_name ResourceGroup
extends Resource
const PathVerifier = preload("path_verifier.gd")
## The base folder for locating files in this resource group.
@export_dir var base_folder:String = ""
## Files that should be included. Can contain ant-style wildcards:
## ** - matches zero or more characters (including "/")
## * - matches zero or more characters (excluding "/")
## ? - matches one character
@export var includes:Array[String] = []
## Files which should be excluded. Is applied after the include filter.
## Can also contain ant-style wildcards.
@export var excludes:Array[String] = []
## The paths of the project that match this resource group.
@export var paths:Array[String] = []
## Loads all resources in this resource group and returns them.
func load_all() -> Array[Resource]:
var result:Array[Resource] = []
load_all_into(result)
return result
## Loads all resources and stores them into the given array. Allows
## to load resources into typed arrays for better type safety. If
## the item is not of the required type, an error will be printed and
## the item is skipped.
func load_all_into(destination:Array):
for path in paths:
destination.append(load(path))
## Gets all paths of resources inside of this resource group that
## match the given include and exclude criteria
func get_matching_paths(includes:Array[String], excludes:Array[String]) -> Array[String]:
var path_verifier = PathVerifier.new(base_folder, includes, excludes)
return paths.filter(func(it): return path_verifier.matches(it))
## Loads all resources in this resource group that match the given
## include and exclude criteria
func load_matching(includes:Array[String], excludes:Array[String]) -> Array[Resource]:
var result:Array[Resource] = []
load_matching_into(result, includes, excludes)
return result
## Loads all resources in this resource group that match the given
## include and exclude criteria and stores them into the given array.
## Allows to load resources into typed arrays for better type safety. If
## the item is not of the required type, an error will be printed and
## the item is skipped.
func load_matching_into(destination:Array, includes:Array[String], excludes:Array[String]):
var matching_paths = get_matching_paths(includes, excludes)
for path in matching_paths:
destination.append(load(path))
## Loads all resources in this resource group in background. Returns
## a ResourceGroupBackgroundLoader object which can be used to check the
## status and collect the results. Will call the on_resource_loaded callable for each
## loaded resource.
func load_all_in_background(on_resource_loaded:Callable) -> ResourceGroupBackgroundLoader:
return ResourceGroupBackgroundLoader.new(paths, on_resource_loaded)
## Loads all resources in this resource group that match the given
## include and exclude criteria in background. ResourceGroupBackgroundLoader object which can be used to check the
## status and collect the results. Will call the on_resource_loaded callable for each
## loaded resource.
func load_matching_in_background(includes:Array[String], excludes:Array[String], on_resource_loaded:Callable) \
-> ResourceGroupBackgroundLoader:
return ResourceGroupBackgroundLoader.new(get_matching_paths(includes, excludes), on_resource_loaded)
#### CSHARP Specifics ####
# Workaround for C# interop not being able to properly convert arrays into Godot land.
func __csharp_get_matching_paths(includes:Array, excludes:Array) -> Array[String]:
return get_matching_paths(__to_string_array(includes), __to_string_array(excludes))
# Workaround for C# interop not being able to properly convert arrays into Godot land.
func __csharp_load_matching(includes:Array, excludes:Array) -> Array[Resource]:
return load_matching(__to_string_array(includes), __to_string_array(excludes))
# Workaround for C# interop not being able to properly convert arrays into Godot land.
func __csharp_load_matching_in_background(includes:Array, excludes:Array, on_resource_loaded:Callable) -> ResourceGroupBackgroundLoader:
return load_matching_in_background(__to_string_array(includes), __to_string_array(excludes), on_resource_loaded)
# Converts an untyped array to a string array.
func __to_string_array(array:Array) -> Array[String]:
var result:Array[String] = []
for item in array:
result.append(item)
return result

View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="100%" height="100%" viewBox="0 0 128 128" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" xml:space="preserve" xmlns:serif="http://www.serif.com/" style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:1.5;">
<g id="Cube" transform="matrix(1,0,0,1,-25.7158,-7.96899)">
<path d="M68.796,39.855L86.269,49.736L68.796,59.618L51.323,49.736L68.796,39.855" style="fill:rgb(193,185,236);stroke:black;stroke-width:0.15px;"/>
<path d="M51.323,49.736L51.323,69.499L68.796,79.38L68.796,59.618L51.323,49.736" style="fill:rgb(149,136,223);stroke:black;stroke-width:0.15px;"/>
<path d="M86.269,49.736L68.796,59.618L68.796,79.38L86.269,69.499L86.269,49.736Z" style="fill:rgb(193,185,236);stroke:black;stroke-width:0.15px;"/>
</g>
<g id="Cube1" serif:id="Cube" transform="matrix(1,0,0,1,16.124,-7.96899)">
<path d="M68.796,39.855L86.269,49.736L68.796,59.618L51.323,49.736L68.796,39.855" style="fill:rgb(193,185,236);stroke:black;stroke-width:0.15px;"/>
<path d="M51.323,49.736L51.323,69.499L68.796,79.38L68.796,59.618L51.323,49.736" style="fill:rgb(149,136,223);stroke:black;stroke-width:0.15px;"/>
<path d="M86.269,49.736L68.796,59.618L68.796,79.38L86.269,69.499L86.269,49.736Z" style="fill:rgb(193,185,236);stroke:black;stroke-width:0.15px;"/>
</g>
<g id="Cube2" serif:id="Cube" transform="matrix(1,0,0,1,-4.32539,4.5168)">
<path d="M68.796,39.855L86.269,49.736L68.796,59.618L51.323,49.736L68.796,39.855" style="fill:rgb(193,185,236);stroke:black;stroke-width:0.15px;"/>
<path d="M51.323,49.736L51.323,69.499L68.796,79.38L68.796,59.618L51.323,49.736" style="fill:rgb(149,136,223);stroke:black;stroke-width:0.15px;"/>
<path d="M86.269,49.736L68.796,59.618L68.796,79.38L86.269,69.499L86.269,49.736Z" style="fill:rgb(193,185,236);stroke:black;stroke-width:0.15px;"/>
</g>
<g id="Cube3" serif:id="Cube" transform="matrix(1,0,0,1,-25.7158,16.7339)">
<path d="M68.796,39.855L86.269,49.736L68.796,59.618L51.323,49.736L68.796,39.855" style="fill:rgb(193,185,236);stroke:black;stroke-width:0.15px;"/>
<path d="M51.323,49.736L51.323,69.499L68.796,79.38L68.796,59.618L51.323,49.736" style="fill:rgb(149,136,223);stroke:black;stroke-width:0.15px;"/>
<path d="M86.269,49.736L68.796,59.618L68.796,79.38L86.269,69.499L86.269,49.736Z" style="fill:rgb(193,185,236);stroke:black;stroke-width:0.15px;"/>
</g>
<g id="Cube4" serif:id="Cube" transform="matrix(1,0,0,1,18.0258,16.7339)">
<path d="M68.796,39.855L86.269,49.736L68.796,59.618L51.323,49.736L68.796,39.855" style="fill:rgb(193,185,236);stroke:black;stroke-width:0.15px;"/>
<path d="M51.323,49.736L51.323,69.499L68.796,79.38L68.796,59.618L51.323,49.736" style="fill:rgb(149,136,223);stroke:black;stroke-width:0.15px;"/>
<path d="M86.269,49.736L68.796,59.618L68.796,79.38L86.269,69.499L86.269,49.736Z" style="fill:rgb(193,185,236);stroke:black;stroke-width:0.15px;"/>
</g>
<g transform="matrix(1,0,0,1.05859,-2.81137,-0.554868)">
<path d="M118.078,27.093C118.078,19.076 111.188,12.568 102.703,12.568L30.92,12.568C22.435,12.568 15.545,19.076 15.545,27.093L15.545,94.871C15.545,102.887 22.435,109.395 30.92,109.395L102.703,109.395C111.188,109.395 118.078,102.887 118.078,94.871L118.078,27.093Z" style="fill:none;stroke:rgb(193,185,236);stroke-width:2.04px;"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

@ -0,0 +1,37 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bkc024iv5jdeu"
path="res://.godot/imported/resource_group.svg-4ab15486a9423c0a5fbcfc8f79bd7fc4.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/godot_resource_groups/resource_group.svg"
dest_files=["res://.godot/imported/resource_group.svg-4ab15486a9423c0a5fbcfc8f79bd7fc4.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false

View File

@ -0,0 +1,126 @@
class_name ResourceGroupBackgroundLoader
var _open:Array[String] = []
var _callback:Callable
var _cancelled:bool = false
var _total:int = 0
var _finished:int = 0
## A loaded resource. This will be given as argument
## to the callback function
class ResourceLoadingInfo:
## Whether loading of this resource was successful
var success:bool
## The path from which the resource was loaded
var path:String
## The loaded resource. Is null when success is false
var resource:Resource
## The overall progress of the background loading, can
## be used to drive a progress indicator
var progress:float
## Whether this has been the last resource
## of the
var last:bool
## Ctor. Takes an array of paths to load.
func _init(paths:Array[String], callback:Callable):
_callback = callback
# copy all into the open array and reverse the order
# so we can use the array as a stack
_open.append_array(paths)
_open.reverse()
# make note of the total amount of things
_total = _open.size()
# start loading
_call_deferred(_next)
## Cancels the currently running background loading process
## as soon as possible. If the process is already finished,
## nothing will happen. This function will immediately return.
func cancel():
_cancelled = true
## Checks if the background loading process is done.
func is_done() -> bool:
return _open.is_empty() or _cancelled
## Looks like call_deferred does not work properly when not in a node
## context (it doesn't seem to defer there...), so we roll our own here.
func _call_deferred(callable:Callable, args:Array = []):
await Engine.get_main_loop().process_frame
callable.callv(args)
## Starts the loading process for the next file in background.
func _next():
# if we're done or the process was cancelled, stop it here.
if _open.is_empty() or _cancelled:
return
# fetch the next path and ask the resource loader to load it.
var path = _open.pop_back()
var result = ResourceLoader.load_threaded_request(path)
if result != OK:
push_warning("Unable to load path ", path , " return code ", result)
_call_deferred(_fetch, [path])
## Tries to fetch a file currently being loaded in background.
func _fetch(path:String):
var status = ResourceLoader.load_threaded_get_status(path)
match (status):
ResourceLoader.THREAD_LOAD_INVALID_RESOURCE:
push_warning("Loading resource at ", path , " failed. Invalid resource. Ignoring and moving on.")
_failed(path)
ResourceLoader.THREAD_LOAD_FAILED:
push_warning("Loading resource at ", path , " failed. Ignoring and moving on.")
_failed(path)
# if its ready, ship it to the callback and move on
ResourceLoader.THREAD_LOAD_LOADED:
var resource = ResourceLoader.load_threaded_get(path)
_succeeded(path, resource)
# we're still loading, so try again next frame
ResourceLoader.THREAD_LOAD_IN_PROGRESS:
_call_deferred(_fetch, [path])
# Called when a path loading ultimately failed. Informs the callback
# and moves on.
func _failed(path:String):
_finished += 1
_call_callback(false, path, null)
_call_deferred(_next)
## Called when a path loading ultimately succeeded. Informs the callback
## and moves on.
func _succeeded(path:String, resource:Resource):
_finished += 1
_call_callback(true, path, resource)
_call_deferred(_next)
## Called when an operation is finished. Will invoke the callback.
func _call_callback(success:bool, path:String, resource:Resource):
# don't call the callback anymore if the process was cancelled
if _cancelled:
return
var progress:float = 1.0 if _total == 0 else float(_finished) / float(_total)
var result = ResourceLoadingInfo.new()
result.success = success
result.path = path
result.resource = resource
result.progress = progress
result.last = _open.is_empty()
_callback.call(result)

View File

@ -0,0 +1,33 @@
@tool
var _file_system:EditorFileSystem
func _init(file_system:EditorFileSystem) -> void:
_file_system = file_system
## The resource group scanner finds all resource groups currently
## in the project.
## Scans the whole project for resources that match the
## group definition.
func scan() -> Array[Resource]:
var result:Array[Resource] = []
_scan(_file_system.get_filesystem(), result)
return result
func _scan(folder:EditorFileSystemDirectory, results:Array[Resource]):
# get all files in the folder
for i in folder.get_file_count():
if folder.get_file_type(i) == "Resource":
var path = folder.get_file_path(i)
if path.ends_with(".tres"):
var resource = ResourceLoader.load(path)
if resource is ResourceGroup:
results.append(resource)
# for each file first check if it matches the group definition, before trying to load it
for j in folder.get_subdir_count():
_scan(folder.get_subdir(j), results)

View File

@ -0,0 +1,61 @@
@tool
const PathVerifier = preload("path_verifier.gd")
## The resource scanner scans the project for resources matching the given
## definition and returns a list of matching resources.
var _group:ResourceGroup
var _verifier:PathVerifier
var _file_system:EditorFileSystem
## Ctor.
func _init(group:ResourceGroup, file_system:EditorFileSystem):
_group = group
_verifier = PathVerifier.new(_group.base_folder, _group.includes, _group.excludes)
_file_system = file_system
## Scans the whole project for resources that match the
## group definition.
func scan() -> Array[String]:
var result:Array[String] = []
var folder = _group.base_folder
if folder == "":
push_warning("In resource group '" + _group.resource_path + "': Base folder is not set. Resource group will be empty.")
return result
var root = _file_system.get_filesystem_path(folder)
if root == null:
push_warning("In resource group '" + _group.resource_path + "': Base folder '" + folder + "' does not exist. Resource group will be empty.")
return result
_scan(root, result)
return result
func _scan(folder:EditorFileSystemDirectory, results:Array[String]):
# get all files in the folder
# for each file first check if it matches the group definition, before trying to load it
for i in folder.get_file_count():
var full_name = folder.get_file_path(i)
if _matches_group_definition(full_name):
if ResourceLoader.exists(full_name):
results.append(full_name)
else:
push_warning("In resource group '" + _group.resource_path + "': File '" + full_name + "' exists, but is not a supported Godot resource. It will be ignored.")
# recurse into subfolders
for j in folder.get_subdir_count():
_scan(folder.get_subdir(j), results)
func _matches_group_definition(file:String) -> bool:
# Skip import files
if file.ends_with(".import"):
return false
return _verifier.matches(file)

View File

@ -0,0 +1,9 @@
# MIT License
Copyright 2022 Gennady "Don Tnowe" Krupenyov
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View File

@ -0,0 +1,6 @@
@tool
extends Control
func _ready():
modulate = get_theme_color("accent_color", "Editor")

View File

@ -0,0 +1,12 @@
@tool
extends Button
@export var icon_name := "Node" :
set(v):
icon_name = v
if has_theme_icon(v, "EditorIcons"):
icon = get_theme_icon(v, "EditorIcons")
func _ready():
self.icon_name = (icon_name)

View File

@ -0,0 +1,69 @@
@tool
extends EditorResourcePicker
signal on_resources_dropped(resources : Array)
var _prepared_for_drop := false
var _drop_hint_label : Label
func _ready():
resource_changed.connect(_on_resource_changed)
_drop_hint_label = Label.new()
_drop_hint_label.text = "[Drop Here to Add!]"
_drop_hint_label.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_drop_hint_label.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
_drop_hint_label.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
_drop_hint_label.hide()
add_child(_drop_hint_label)
func set_prepared_for_drop(state : bool):
for x in get_children(true):
if not x is Popup and (not x is Label):
x.visible = not state
_drop_hint_label.visible = state
custom_minimum_size = size if state else Vector2.ZERO
_prepared_for_drop = state
func _can_drop_data(at_position: Vector2, data: Variant) -> bool:
var data_drop_type : StringName = data.get(&"type", &"")
if data_drop_type != &"files" or data_drop_type != &"resource":
return true
set_prepared_for_drop(true)
return false
func _drop_data(at_position: Vector2, data: Variant):
var data_drop_type : StringName = data.get(&"type", &"")
var new_array : Array[Resource] = []
if data_drop_type == &"files":
for x in data.files:
new_array.append(load(x))
if data_drop_type == &"resource":
new_array.append(data.resource)
if new_array.size() == 0:
return
edited_resource = new_array[0]
on_resources_dropped.emit(new_array)
func _input(event: InputEvent):
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT:
if !event.pressed:
set_prepared_for_drop(false)
if event is InputEventMouseMotion and not _prepared_for_drop:
# _can_drop_data() is only called when hovering over the picker. Items must be hidden before that.
if Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT) and not get_global_rect().has_point(event.global_position):
set_prepared_for_drop(true)
func _on_resource_changed(new_resource : Resource):
on_resources_dropped.emit([new_resource])

View File

@ -0,0 +1,526 @@
@tool
extends Control
signal grid_updated()
const TablesPluginSettingsClass := preload("res://addons/resources_spreadsheet_view/settings_grid.gd")
@onready var node_folder_path : LineEdit = $"HeaderContentSplit/VBoxContainer/HBoxContainer/HBoxContainer/Path"
@onready var node_recent_paths : OptionButton = $"HeaderContentSplit/VBoxContainer/HBoxContainer/HBoxContainer2/RecentPaths"
@onready var node_table_root : GridContainer = $"HeaderContentSplit/MarginContainer/FooterContentSplit/Panel/Scroll/MarginContainer/TableGrid"
@onready var node_columns : HBoxContainer = $"HeaderContentSplit/VBoxContainer/Columns/Columns"
@onready var node_page_manager : Control = $"HeaderContentSplit/VBoxContainer/HBoxContainer3/Pages"
@onready var node_class_filter : Control = $"HeaderContentSplit/VBoxContainer/Search/Search/Class"
@onready var _on_cell_gui_input : Callable = $"InputHandler"._on_cell_gui_input
@onready var _selection := $"SelectionManager"
var editor_interface : Object
var editor_plugin : EditorPlugin
var current_path := ""
var save_data_path : String = get_script().resource_path.get_base_dir() + "/saved_state.json"
var sorting_by := &""
var sorting_reverse := false
var columns : Array[StringName] = []
var column_types : Array[int] = []
var column_hints : Array[int] = []
var column_hint_strings : Array[PackedStringArray] = []
var rows := []
var remembered_paths := {}
var remembered_paths_total_count := 0
var table_functions_dict := {}
var search_cond : Callable
var io : RefCounted
var first_row := 0
var last_row := 0
func _ready():
editor_interface.get_resource_filesystem().filesystem_changed.connect(_on_filesystem_changed)
if FileAccess.file_exists(save_data_path):
var file := FileAccess.open(save_data_path, FileAccess.READ)
var as_text := file.get_as_text()
var as_var := JSON.parse_string(as_text)
node_recent_paths.load_paths(as_var.get(&"recent_paths", []))
node_columns.column_properties = as_var.get(&"column_properties", {})
table_functions_dict = as_var.get(&"table_functions", {})
for x in $"HeaderContentSplit/VBoxContainer/Search/Search".get_children():
if x.has_method(&"load_saved_functions"):
x.load_saved_functions(table_functions_dict)
# Legacy compat: old hidden column format
var legacy_hidden_columns : Dictionary = as_var.get(&"hidden_columns", {})
for k in legacy_hidden_columns:
for k2 in legacy_hidden_columns[k]:
node_columns.column_properties[k] = node_columns.column_properties.get(k, {})
node_columns.column_properties[k][k2] = { &"visibility" : 0 }
if node_recent_paths.recent_paths.size() >= 1:
display_folder(node_recent_paths.recent_paths[-1], &"resource_name", false, true)
func save_data():
if (
node_recent_paths.recent_paths.is_empty()
and node_columns.column_properties.is_empty()
and table_functions_dict.is_empty()
):
return
var file := FileAccess.open(save_data_path, FileAccess.WRITE)
file.store_string(JSON.stringify(
{
&"recent_paths" : node_recent_paths.recent_paths,
&"column_properties" : node_columns.column_properties,
&"table_functions" : table_functions_dict,
}
, " "))
func _on_filesystem_changed():
if current_path == "":
return
var file_total_count := _get_file_count_recursive(current_path)
if file_total_count != remembered_paths_total_count:
refresh()
else:
for k in remembered_paths:
if !is_instance_valid(remembered_paths[k]):
continue
if remembered_paths[k].resource_path != k:
var res = remembered_paths[k]
remembered_paths.erase(k)
remembered_paths[res.resource_path] = res
refresh()
break
func _get_file_count_recursive(path : String) -> int:
var editor_fs : EditorFileSystem = editor_interface.get_resource_filesystem()
var path_dir := editor_fs.get_filesystem_path(path)
if !path_dir: return 0
var file_total_count := 0
var folder_stack : Array[EditorFileSystemDirectory] = [path_dir]
while folder_stack.size() > 0:
path_dir = folder_stack.pop_back()
file_total_count += path_dir.get_file_count()
for i in path_dir.get_subdir_count():
folder_stack.append(path_dir.get_subdir(i))
return file_total_count
func display_folder(folderpath : String, sort_by : StringName = "", sort_reverse : bool = false, force_rebuild : bool = false, is_echo : bool = false):
if folderpath == "":
# You wouldn't just wanna edit ALL resources in the project, that's a long time to load!
return
if sort_by == "":
sort_by = &"resource_path"
if folderpath.get_extension() == "":
folderpath = folderpath.trim_suffix("/") + "/"
if folderpath.ends_with(".tres") and !(load(folderpath) is ResourceTablesImport):
folderpath = folderpath.get_base_dir() + "/"
node_recent_paths.add_path_to_recent(folderpath)
node_folder_path.text = folderpath
_load_resources_from_path(folderpath, sort_by, sort_reverse)
_update_visible_rows(force_rebuild or current_path != folderpath)
current_path = folderpath
remembered_paths_total_count = _get_file_count_recursive(folderpath)
node_columns.update()
grid_updated.emit()
$"HeaderContentSplit/MarginContainer/FooterContentSplit/Panel/Label".visible = rows.size() == 0
$"HeaderContentSplit/MarginContainer/FooterContentSplit/Panel/Label".text = "No rows visible?\nThis might happen when sorting by a property that isn't here anymore.\nTry clicking a column header to sort again!\n\nIt's also possible that your Filter expression is filtering them out."
func display_resources(resource_array : Array):
if sorting_by == "":
sorting_by = &"resource_path"
current_path = ""
node_recent_paths.select(-1)
rows = []
for x in resource_array:
insert_row_sorted(x, rows, sorting_by, sorting_reverse)
fill_property_data_many(rows)
_update_visible_rows()
node_columns.update()
grid_updated.emit()
func refresh(force_rebuild : bool = true):
if current_path == "":
display_resources(rows)
else:
display_folder(current_path, sorting_by, sorting_reverse, force_rebuild)
func _load_resources_from_path(path : String, sort_by : StringName, sort_reverse : bool):
if path.ends_with("/"):
io = ResourceTablesEditFormatTres.new()
else:
var loaded := load(path)
if loaded is ResourceTablesImport:
io = loaded.view_script.new()
node_class_filter.hide()
else:
io = ResourceTablesEditFormatTres.new()
io.editor_view = self
remembered_paths.clear()
rows = io.import_from_path(path, insert_row_sorted, sort_by, sort_reverse)
func _update_visible_rows(force_rebuild : bool = true):
node_page_manager.update_page_count(rows)
if columns.size() == 0:
return
if force_rebuild or columns != node_columns.columns:
for x in node_table_root.get_children():
x.free()
node_columns.columns = columns
var cells_left_to_free : int = node_table_root.get_child_count() - (last_row - first_row) * columns.size()
while cells_left_to_free > 0:
node_table_root.get_child(0).free()
cells_left_to_free -= 1
var color_rows : bool = ProjectSettings.get_setting(TablesPluginSettingsClass.PREFIX + "color_rows")
for i in last_row - first_row:
_update_row(first_row + i, color_rows)
func fill_property_data(res : Resource):
columns.clear()
column_types.clear()
column_hints.clear()
column_hint_strings.clear()
var column_values := []
var i := -1
for x in res.get_property_list():
if x[&"usage"] & PROPERTY_USAGE_EDITOR != 0 and x[&"name"] != "script":
i += 1
columns.append(x[&"name"])
column_types.append(x[&"type"])
column_hints.append(x[&"hint"])
column_hint_strings.append(x[&"hint_string"].split(","))
column_values.append(io.get_value(res, columns[i]))
_selection.initialize_editors(column_values, column_types, column_hints)
func fill_property_data_many(resources : Array):
node_class_filter.fill(resources)
columns.clear()
column_types.clear()
column_hints.clear()
column_hint_strings.clear()
var column_values := []
var i := -1
var found_props := {}
for x in resources:
if x == null: continue
i += 1
if not search_cond.is_null() and not search_cond.call(x, i):
continue
if not node_class_filter.filter(x):
continue
for y in x.get_property_list():
found_props[y[&"name"]] = y
y[&"owner_object"] = x
i = -1
for x in found_props.values():
if x[&"usage"] & PROPERTY_USAGE_EDITOR != 0 and x[&"name"] != "script":
i += 1
columns.append(x[&"name"])
column_types.append(x[&"type"])
column_hints.append(x[&"hint"])
column_hint_strings.append(x[&"hint_string"].split(","))
column_values.append(io.get_value(x[&"owner_object"], columns[i]))
_selection.initialize_editors(column_values, column_types, column_hints)
func insert_row_sorted(res : Resource, loaded_rows : Array, sort_by : StringName, sort_reverse : bool):
if not search_cond.is_null() and not search_cond.call(res, loaded_rows.size()):
return
if not sort_by in res or not node_class_filter.filter(res):
return
var sort_value = res[sort_by]
for i in loaded_rows.size():
if sort_reverse == compare_values(sort_value, loaded_rows[i][sort_by]):
loaded_rows.insert(i, res)
return
remembered_paths[res.resource_path] = res
loaded_rows.append(res)
func compare_values(a, b) -> bool:
if a == null or b == null: return b == null
if a is Color:
return a.h > b.h if a.h != b.h else a.v > b.v
if a is Resource:
return a.resource_path > b.resource_path
if a is Array:
return a.size() > b.size()
return a > b
func column_can_solo_open(column_index : int) -> bool:
return (
column_types[column_index] == TYPE_OBJECT
or (column_types[column_index] == TYPE_ARRAY and column_hint_strings[column_index][0].begins_with("24"))
)
func column_solo_open(column_index : int):
display_folder(current_path.trim_suffix("/") + "::" + columns[column_index])
func _set_sorting(sort_by : StringName):
var sort_reverse : bool = !(sorting_by != sort_by or sorting_reverse)
sorting_reverse = sort_reverse
sorting_by = sort_by
refresh()
func _update_row(row_index : int, color_rows : bool = true):
var current_node : Control
var next_color := Color.WHITE
var column_editors : Array = _selection.column_editors
var shortened_path : String = rows[row_index].resource_path.get_file().trim_suffix(".tres")
for i in columns.size():
if node_table_root.get_child_count() <= (row_index - first_row) * columns.size() + i:
current_node = column_editors[i].create_cell(self)
current_node.gui_input.connect(_on_cell_gui_input.bind(current_node))
node_table_root.add_child(current_node)
else:
current_node = node_table_root.get_child((row_index - first_row) * columns.size() + i)
current_node.tooltip_text = (
columns[i].capitalize()
+ "\n---\n"
+ "Of " + shortened_path
)
if columns[i] in rows[row_index]:
current_node.mouse_filter = MOUSE_FILTER_STOP
current_node.modulate.a = 1.0
else:
# Empty cell, can't click, property doesn't exist.
current_node.mouse_filter = MOUSE_FILTER_IGNORE
current_node.modulate.a = 0.0
continue
if columns[i] == &"resource_path":
column_editors[i].set_value(current_node, shortened_path)
else:
var cell_value = io.get_value(rows[row_index], columns[i])
if cell_value != null or column_types[i] == TYPE_OBJECT:
column_editors[i].set_value(current_node, cell_value)
if color_rows and column_types[i] == TYPE_COLOR:
next_color = cell_value
column_editors[i].set_color(current_node, next_color)
func get_selected_column() -> int:
return _selection.get_cell_column(_selection.edited_cells[0])
func select_column(column_index : int):
_selection.deselect_all_cells()
_selection.select_cell(Vector2i(column_index, 0))
_selection.select_cells_to(Vector2i(column_index, rows.size() - 1))
func set_edited_cells_values_text(new_cell_values : Array):
var column_editor : Object = _selection.column_editors[get_selected_column()]
# Duplicated here since if using text editing, edited_cells_text needs to modified
# but here, it would be converted from a String breaking editing
var new_cell_values_converted := new_cell_values.duplicate()
for i in new_cell_values.size():
new_cell_values_converted[i] = column_editor.from_text(new_cell_values[i])
set_edited_cells_values(new_cell_values_converted)
for i in new_cell_values.size():
var i_pos : Vector2i = _selection.edited_cells[i]
var update_cell : Control = _selection.get_cell_node_from_position(i_pos)
if update_cell == null:
continue
column_editor.set_value(update_cell, new_cell_values[i])
func set_edited_cells_values(new_cell_values : Array):
var edited_rows : Array = _selection.get_edited_rows()
var column : int = _selection.get_cell_column(_selection.edited_cells[0])
var edited_cells_resources := _get_row_resources(edited_rows)
editor_plugin.undo_redo.create_action("Set Cell Values")
editor_plugin.undo_redo.add_undo_method(
self,
&"_update_resources",
edited_cells_resources.duplicate(),
edited_rows.duplicate(),
column,
get_edited_cells_values()
)
editor_plugin.undo_redo.add_undo_method(
_selection,
&"_update_selected_cells_text"
)
editor_plugin.undo_redo.add_do_method(
self,
&"_update_resources",
edited_cells_resources.duplicate(),
edited_rows.duplicate(),
column,
new_cell_values.duplicate()
)
editor_plugin.undo_redo.commit_action(true)
_selection._update_selected_cells_text()
func rename_row(row, new_name):
if !has_row_names(): return
io.rename_row(row, new_name)
refresh()
func duplicate_selected_rows(new_name : String):
io.duplicate_rows(_get_row_resources(_selection.get_edited_rows()), new_name)
refresh()
func delete_selected_rows():
io.delete_rows(_get_row_resources(_selection.get_edited_rows()))
refresh()
refresh.call_deferred()
func has_row_names():
return io.has_row_names()
func get_last_selected_row():
return rows[_selection.get_cell_row(_selection.edited_cells[-1])]
func get_edited_cells_values() -> Array:
var cells : Array = _selection.edited_cells.duplicate()
var column_index : int = _selection.get_cell_column(cells[0])
var result := []
result.resize(cells.size())
for i in cells.size():
result[i] = io.get_value(rows[_selection.get_cell_row(cells[i])], columns[column_index])
return result
func _update_resources(update_rows : Array, update_row_indices : Array[int], update_column : int, values : Array):
var column_editor : Object = _selection.column_editors[update_column]
for i in update_rows.size():
var row := update_row_indices[i]
io.set_value(
update_rows[i],
columns[update_column],
values[i],
row
)
var update_cell : Control = _selection.get_cell_node_from_position(Vector2i(update_column, row))
if update_cell == null:
continue
column_editor.set_value(update_cell, values[i])
var row_script : Object = update_rows[i].get_script()
if row_script != null && row_script.is_tool():
for column_i in columns.size():
if column_i == update_column:
continue
var update_cell_c : Control = _selection.get_cell_node_from_position(Vector2i(column_i, row))
_selection.column_editors[column_i].set_value(update_cell_c, update_rows[i].get(columns[column_i]))
if values[i] == null:
continue
if column_types[update_column] == TYPE_COLOR:
for j in columns.size() - update_column:
if j != 0 and column_types[j + update_column] == TYPE_COLOR:
break
_selection.column_editors[j + update_column].set_color(
_selection.get_cell_node_from_position(Vector2i(update_column + j, row)),
values[i]
)
node_columns._update_column_sizes()
io.save_entries(rows, update_row_indices)
func _on_path_text_submitted(new_text : String = ""):
if new_text != "":
current_path = new_text
display_folder(new_text, "", false, true)
else:
refresh()
func _on_FileDialog_dir_selected(dir : String):
node_folder_path.text = dir
display_folder(dir)
func _get_row_resources(row_indices) -> Array:
var arr := []
arr.resize(row_indices.size())
for i in arr.size():
arr[i] = rows[row_indices[i]]
return arr
func _on_File_pressed():
node_folder_path.get_parent().get_parent().visible = !node_folder_path.get_parent().get_parent().visible
func _on_SearchProcess_pressed():
$"HeaderContentSplit/VBoxContainer/Search".visible = !$"HeaderContentSplit/VBoxContainer/Search".visible

View File

@ -0,0 +1,594 @@
[gd_scene load_steps=32 format=3 uid="uid://5pic6fxx6et3"]
[ext_resource type="Script" path="res://addons/resources_spreadsheet_view/editor_view.gd" id="1_wfx75"]
[ext_resource type="Script" path="res://addons/resources_spreadsheet_view/editor_color_setter.gd" id="2_t2s7k"]
[ext_resource type="Script" path="res://addons/resources_spreadsheet_view/editor_icon_button.gd" id="3_7ja2l"]
[ext_resource type="Script" path="res://addons/resources_spreadsheet_view/main_screen/recent_paths.gd" id="4_umoob"]
[ext_resource type="Script" path="res://addons/resources_spreadsheet_view/main_screen/expression_textfield.gd" id="5_faq75"]
[ext_resource type="Script" path="res://addons/resources_spreadsheet_view/main_screen/table_pages.gd" id="5_ka2yn"]
[ext_resource type="Script" path="res://addons/resources_spreadsheet_view/main_screen/class_filter.gd" id="6_2v7d5"]
[ext_resource type="Script" path="res://addons/resources_spreadsheet_view/main_screen/column_header_manager.gd" id="6_emnmd"]
[ext_resource type="PackedScene" uid="uid://d1s6oihqedvo5" path="res://addons/resources_spreadsheet_view/main_screen/table_header.tscn" id="7_3dx0v"]
[ext_resource type="PackedScene" uid="uid://ddqak780cwwfj" path="res://addons/resources_spreadsheet_view/typed_editors/dock_enum_array.tscn" id="8_234wn"]
[ext_resource type="PackedScene" uid="uid://c3a2cip8ffccv" path="res://addons/resources_spreadsheet_view/typed_editors/dock_array.tscn" id="9_nts08"]
[ext_resource type="PackedScene" uid="uid://b3a3bo6cfyh5t" path="res://addons/resources_spreadsheet_view/typed_editors/dock_color.tscn" id="10_nsma2"]
[ext_resource type="PackedScene" uid="uid://gtbf7b0wptv" path="res://addons/resources_spreadsheet_view/typed_editors/dock_number.tscn" id="11_q1ao4"]
[ext_resource type="PackedScene" uid="uid://rww3gpl052bn" path="res://addons/resources_spreadsheet_view/typed_editors/dock_texture.tscn" id="12_4kr6q"]
[ext_resource type="PackedScene" uid="uid://dhunxgcae6h1" path="res://addons/resources_spreadsheet_view/settings_grid.tscn" id="13_as1sh"]
[ext_resource type="PackedScene" uid="uid://p6x03dbvhnqw" path="res://addons/resources_spreadsheet_view/typed_editors/dock_dict.tscn" id="13_il556"]
[ext_resource type="Script" path="res://addons/resources_spreadsheet_view/main_screen/input_handler.gd" id="14_2t57a"]
[ext_resource type="PackedScene" uid="uid://b413igx28kkvb" path="res://addons/resources_spreadsheet_view/import_export/import_export_dialog.tscn" id="14_3p12b"]
[ext_resource type="Script" path="res://addons/resources_spreadsheet_view/main_screen/selection_manager.gd" id="15_mx6qn"]
[ext_resource type="Script" path="res://addons/resources_spreadsheet_view/typed_cells/cell_editor_enum_array.gd" id="16_p7n52"]
[ext_resource type="Script" path="res://addons/resources_spreadsheet_view/typed_cells/cell_editor_array.gd" id="17_sofdw"]
[ext_resource type="Script" path="res://addons/resources_spreadsheet_view/typed_cells/cell_editor_color.gd" id="18_oeewr"]
[ext_resource type="Script" path="res://addons/resources_spreadsheet_view/typed_cells/cell_editor_bool.gd" id="19_7x44x"]
[ext_resource type="Script" path="res://addons/resources_spreadsheet_view/typed_cells/cell_editor_dict.gd" id="19_oeuko"]
[ext_resource type="Script" path="res://addons/resources_spreadsheet_view/typed_cells/cell_editor_enum.gd" id="20_swsbn"]
[ext_resource type="Script" path="res://addons/resources_spreadsheet_view/typed_cells/cell_editor_resource.gd" id="21_58wf8"]
[ext_resource type="Script" path="res://addons/resources_spreadsheet_view/typed_cells/cell_editor_string.gd" id="22_bni8r"]
[ext_resource type="PackedScene" uid="uid://b51hnttsie7k5" path="res://addons/resources_spreadsheet_view/main_screen/selection_actions.tscn" id="23_m53sx"]
[ext_resource type="Script" path="res://addons/resources_spreadsheet_view/typed_cells/cell_editor_number.gd" id="28_a0j2e"]
[sub_resource type="Gradient" id="Gradient_8kp6w"]
offsets = PackedFloat32Array(0, 0.995413, 1)
colors = PackedColorArray(1, 1, 1, 0.490196, 1, 1, 1, 0.0458716, 1, 1, 1, 0)
[sub_resource type="GradientTexture2D" id="GradientTexture2D_18il8"]
gradient = SubResource("Gradient_8kp6w")
[node name="Control" type="MarginContainer"]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
size_flags_horizontal = 3
size_flags_vertical = 3
focus_neighbor_left = NodePath(".")
focus_neighbor_top = NodePath(".")
focus_neighbor_right = NodePath(".")
focus_neighbor_bottom = NodePath(".")
focus_next = NodePath(".")
focus_previous = NodePath(".")
focus_mode = 2
theme_override_constants/margin_left = 3
theme_override_constants/margin_right = 3
theme_override_constants/margin_bottom = 5
script = ExtResource("1_wfx75")
metadata/_edit_lock_ = true
[node name="HeaderContentSplit" type="VBoxContainer" parent="."]
layout_mode = 2
[node name="VBoxContainer" type="VBoxContainer" parent="HeaderContentSplit"]
layout_mode = 2
[node name="MenuStrip" type="HBoxContainer" parent="HeaderContentSplit/VBoxContainer"]
layout_mode = 2
[node name="File" type="Button" parent="HeaderContentSplit/VBoxContainer/MenuStrip"]
layout_mode = 2
tooltip_text = "Settings"
focus_mode = 0
toggle_mode = true
button_pressed = true
text = "File"
flat = true
[node name="SearchProcess" type="Button" parent="HeaderContentSplit/VBoxContainer/MenuStrip"]
layout_mode = 2
tooltip_text = "Settings"
focus_mode = 0
toggle_mode = true
button_pressed = true
text = "Filter/Process"
flat = true
[node name="VisibleCols" type="MenuButton" parent="HeaderContentSplit/VBoxContainer/MenuStrip"]
layout_mode = 2
text = "Shown Columns"
[node name="VSeparator" type="Control" parent="HeaderContentSplit/VBoxContainer/MenuStrip"]
layout_mode = 2
size_flags_horizontal = 3
[node name="Settings" type="Button" parent="HeaderContentSplit/VBoxContainer/MenuStrip"]
layout_mode = 2
tooltip_text = "Settings"
focus_mode = 0
text = "Settings"
flat = true
[node name="VSeparator2" type="VSeparator" parent="HeaderContentSplit/VBoxContainer/MenuStrip"]
layout_mode = 2
[node name="Info" type="Button" parent="HeaderContentSplit/VBoxContainer/MenuStrip"]
layout_mode = 2
focus_mode = 0
text = "About"
flat = true
[node name="HBoxContainer" type="HSplitContainer" parent="HeaderContentSplit/VBoxContainer"]
layout_mode = 2
split_offset = -249
[node name="HBoxContainer" type="HBoxContainer" parent="HeaderContentSplit/VBoxContainer/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
theme_override_constants/separation = 0
[node name="ColorRect4" type="ColorRect" parent="HeaderContentSplit/VBoxContainer/HBoxContainer/HBoxContainer"]
modulate = Color(0, 0, 0, 1)
custom_minimum_size = Vector2(6, 18)
layout_mode = 2
script = ExtResource("2_t2s7k")
[node name="TextureRect" type="TextureRect" parent="HeaderContentSplit/VBoxContainer/HBoxContainer/HBoxContainer/ColorRect4"]
layout_mode = 1
anchors_preset = 11
anchor_left = 1.0
anchor_right = 1.0
anchor_bottom = 1.0
offset_right = 48.0
grow_horizontal = 0
grow_vertical = 2
texture = SubResource("GradientTexture2D_18il8")
expand_mode = 1
[node name="ColorRect3" type="Control" parent="HeaderContentSplit/VBoxContainer/HBoxContainer/HBoxContainer"]
custom_minimum_size = Vector2(2, 0)
layout_mode = 2
[node name="Label" type="Label" parent="HeaderContentSplit/VBoxContainer/HBoxContainer/HBoxContainer"]
layout_mode = 2
text = "Resource Folder:"
[node name="Path" type="LineEdit" parent="HeaderContentSplit/VBoxContainer/HBoxContainer/HBoxContainer"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
caret_blink = true
[node name="SelectDir" type="Button" parent="HeaderContentSplit/VBoxContainer/HBoxContainer/HBoxContainer"]
layout_mode = 2
tooltip_text = "Open Folder"
script = ExtResource("3_7ja2l")
icon_name = "Folder"
[node name="DeletePath" type="Button" parent="HeaderContentSplit/VBoxContainer/HBoxContainer/HBoxContainer"]
layout_mode = 2
tooltip_text = "Remove Path from Recent"
script = ExtResource("3_7ja2l")
icon_name = "Remove"
[node name="HBoxContainer2" type="HBoxContainer" parent="HeaderContentSplit/VBoxContainer/HBoxContainer"]
layout_mode = 2
[node name="Label2" type="Label" parent="HeaderContentSplit/VBoxContainer/HBoxContainer/HBoxContainer2"]
layout_mode = 2
text = "Open Recent:"
[node name="RecentPaths" type="OptionButton" parent="HeaderContentSplit/VBoxContainer/HBoxContainer/HBoxContainer2"]
unique_name_in_owner = true
layout_mode = 2
size_flags_horizontal = 3
clip_text = true
fit_to_longest_item = false
script = ExtResource("4_umoob")
[node name="ImportExport" type="Button" parent="HeaderContentSplit/VBoxContainer/HBoxContainer/HBoxContainer2"]
layout_mode = 2
text = "Import/Export CSV..."
script = ExtResource("3_7ja2l")
icon_name = "TextFile"
[node name="Search" type="VBoxContainer" parent="HeaderContentSplit/VBoxContainer"]
layout_mode = 2
theme_override_constants/separation = 0
[node name="HBoxContainer" type="HBoxContainer" parent="HeaderContentSplit/VBoxContainer/Search"]
layout_mode = 2
[node name="ColorRect4" type="ColorRect" parent="HeaderContentSplit/VBoxContainer/Search/HBoxContainer"]
modulate = Color(0, 0, 0, 1)
custom_minimum_size = Vector2(6, 18)
layout_mode = 2
size_flags_vertical = 5
script = ExtResource("2_t2s7k")
[node name="TextureRect" type="TextureRect" parent="HeaderContentSplit/VBoxContainer/Search/HBoxContainer/ColorRect4"]
layout_mode = 1
anchors_preset = 11
anchor_left = 1.0
anchor_right = 1.0
anchor_bottom = 1.0
offset_right = 48.0
grow_horizontal = 0
grow_vertical = 2
texture = SubResource("GradientTexture2D_18il8")
expand_mode = 1
[node name="Label" type="Label" parent="HeaderContentSplit/VBoxContainer/Search/HBoxContainer"]
layout_mode = 2
text = "GDScript Filter and Process"
[node name="HSeparator" type="HSeparator" parent="HeaderContentSplit/VBoxContainer/Search/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
[node name="Search" type="HBoxContainer" parent="HeaderContentSplit/VBoxContainer/Search"]
layout_mode = 2
[node name="ColorRect2" type="ColorRect" parent="HeaderContentSplit/VBoxContainer/Search/Search"]
modulate = Color(0, 0, 0, 1)
custom_minimum_size = Vector2(6, 18)
layout_mode = 2
size_flags_vertical = 5
script = ExtResource("2_t2s7k")
[node name="TextureRect" type="TextureRect" parent="HeaderContentSplit/VBoxContainer/Search/Search/ColorRect2"]
layout_mode = 1
anchors_preset = 11
anchor_left = 1.0
anchor_right = 1.0
anchor_bottom = 1.0
offset_right = 48.0
grow_horizontal = 0
grow_vertical = 2
texture = SubResource("GradientTexture2D_18il8")
expand_mode = 1
[node name="Label" type="Label" parent="HeaderContentSplit/VBoxContainer/Search/Search"]
layout_mode = 2
text = "Condition:"
[node name="Label2" type="Label" parent="HeaderContentSplit/VBoxContainer/Search/Search"]
layout_mode = 2
tooltip_text = "Enter an expression. The table only show rows where the expression returns `true`.
You can use `res.<property_name>` to get a property, and `index` to get row number. Hit ENTER to run the search.
Try out these:
- (res.number_property > 0 and res.number_property < 100)
- (res.text_property != \\\"\\\")
- (\\\"a\\\" in res.text_property)
- (index < 5)"
mouse_filter = 0
mouse_default_cursor_shape = 16
text = "(?)"
[node name="Filter" type="HBoxContainer" parent="HeaderContentSplit/VBoxContainer/Search/Search"]
layout_mode = 2
size_flags_horizontal = 3
script = ExtResource("5_faq75")
editor_view_path = NodePath("../../../../..")
title = "func f(res : Resource, index : int):"
default_text = "true"
default_text_ml = "return true"
function_save_key = "filter"
[node name="VSeparator" type="VSeparator" parent="HeaderContentSplit/VBoxContainer/Search/Search"]
layout_mode = 2
[node name="Class" type="HBoxContainer" parent="HeaderContentSplit/VBoxContainer/Search/Search"]
layout_mode = 2
script = ExtResource("6_2v7d5")
[node name="Label5" type="Label" parent="HeaderContentSplit/VBoxContainer/Search/Search/Class"]
layout_mode = 2
text = "Class:"
[node name="Class" type="OptionButton" parent="HeaderContentSplit/VBoxContainer/Search/Search/Class"]
layout_mode = 2
clip_text = true
fit_to_longest_item = false
[node name="Label" type="Label" parent="HeaderContentSplit/VBoxContainer/Search/Search/Class"]
layout_mode = 2
text = "Sub:"
[node name="Subclasses" type="CheckBox" parent="HeaderContentSplit/VBoxContainer/Search/Search/Class"]
layout_mode = 2
tooltip_text = "Include Subclasses"
button_pressed = true
[node name="VSeparator2" type="VSeparator" parent="HeaderContentSplit/VBoxContainer/Search/Search/Class"]
layout_mode = 2
[node name="Label3" type="Label" parent="HeaderContentSplit/VBoxContainer/Search/Search"]
layout_mode = 2
text = "Process:"
[node name="Label4" type="Label" parent="HeaderContentSplit/VBoxContainer/Search/Search"]
layout_mode = 2
tooltip_text = "Enter an expression. The values in selected cells will be replaced with calculated new values.
You can use `value` to get the cell's value, `res.<property_name>` to get a property, `row_index` to get row number
and `cell_index` to get the cell's selection order. Hit ENTER to run the search.
These are some valid expressions:
- (res.property1 + res.property2)
- (res.property1.replace(\\\"old_string\\\", \\\"new_string\\\"))
- (load(\\\"res://path/to_resource.tres\\\"))
Don't forget quotation marks on strings and str() on non-strings."
mouse_filter = 0
mouse_default_cursor_shape = 16
text = "(?)"
[node name="Process" type="HBoxContainer" parent="HeaderContentSplit/VBoxContainer/Search/Search"]
layout_mode = 2
size_flags_horizontal = 3
script = ExtResource("5_faq75")
editor_view_path = NodePath("../../../../..")
mode = 1
title = "func f(value : Var, res : Resource, all_res : Array[Resource], row_index : int):"
default_text = "value"
default_text_ml = "return value"
function_save_key = "process"
[node name="HBoxContainer3" type="HBoxContainer" parent="HeaderContentSplit/VBoxContainer"]
layout_mode = 2
[node name="Label" type="Label" parent="HeaderContentSplit/VBoxContainer/HBoxContainer3"]
layout_mode = 2
text = "Grid"
[node name="Refresh" type="Button" parent="HeaderContentSplit/VBoxContainer/HBoxContainer3"]
layout_mode = 2
tooltip_text = "Refresh"
script = ExtResource("3_7ja2l")
icon_name = "Loop"
[node name="Pages" type="HBoxContainer" parent="HeaderContentSplit/VBoxContainer/HBoxContainer3"]
layout_mode = 2
size_flags_horizontal = 3
script = ExtResource("5_ka2yn")
[node name="Label" type="Label" parent="HeaderContentSplit/VBoxContainer/HBoxContainer3/Pages"]
layout_mode = 2
text = "Page:"
[node name="Scroll" type="ScrollContainer" parent="HeaderContentSplit/VBoxContainer/HBoxContainer3/Pages"]
layout_mode = 2
size_flags_horizontal = 3
follow_focus = true
vertical_scroll_mode = 0
[node name="Pagelist" type="HBoxContainer" parent="HeaderContentSplit/VBoxContainer/HBoxContainer3/Pages/Scroll"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="Label2" type="Label" parent="HeaderContentSplit/VBoxContainer/HBoxContainer3/Pages"]
layout_mode = 2
text = "Rows per page:"
[node name="LineEdit" type="SpinBox" parent="HeaderContentSplit/VBoxContainer/HBoxContainer3/Pages"]
layout_mode = 2
size_flags_vertical = 4
min_value = 2.0
max_value = 300.0
value = 50.0
[node name="Sep" type="Control" parent="HeaderContentSplit/VBoxContainer"]
layout_mode = 2
[node name="Columns" type="Control" parent="HeaderContentSplit/VBoxContainer"]
clip_contents = true
layout_mode = 2
[node name="Columns" type="HBoxContainer" parent="HeaderContentSplit/VBoxContainer/Columns"]
layout_mode = 0
theme_override_constants/separation = 0
script = ExtResource("6_emnmd")
table_header_scene = ExtResource("7_3dx0v")
[node name="Sep2" type="Control" parent="HeaderContentSplit/VBoxContainer"]
visible = false
layout_mode = 2
[node name="MarginContainer" type="MarginContainer" parent="HeaderContentSplit"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
mouse_filter = 2
[node name="FooterContentSplit" type="VBoxContainer" parent="HeaderContentSplit/MarginContainer"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="Panel" type="MarginContainer" parent="HeaderContentSplit/MarginContainer/FooterContentSplit"]
layout_mode = 2
size_flags_vertical = 3
mouse_filter = 2
[node name="Panel" type="Panel" parent="HeaderContentSplit/MarginContainer/FooterContentSplit/Panel"]
layout_mode = 2
[node name="Scroll" type="ScrollContainer" parent="HeaderContentSplit/MarginContainer/FooterContentSplit/Panel"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="MarginContainer" type="MarginContainer" parent="HeaderContentSplit/MarginContainer/FooterContentSplit/Panel/Scroll"]
layout_mode = 2
[node name="TableGrid" type="GridContainer" parent="HeaderContentSplit/MarginContainer/FooterContentSplit/Panel/Scroll/MarginContainer"]
layout_mode = 2
theme_override_constants/h_separation = 0
theme_override_constants/v_separation = 0
[node name="Label" type="Label" parent="HeaderContentSplit/MarginContainer/FooterContentSplit/Panel"]
self_modulate = Color(1, 1, 1, 0.498039)
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
text = "No folder selected!
Please select a folder to edit using the text field or Open button above.
Then, Shift+Click or Ctrl+Click cells to edit them using the keyboard,
Inspector dock or this screen's bottom panels.
To find out keybindings available, open the \"About\" menu."
horizontal_alignment = 1
vertical_alignment = 1
[node name="Footer" type="VBoxContainer" parent="HeaderContentSplit/MarginContainer/FooterContentSplit"]
layout_mode = 2
[node name="PropertyEditors" type="VBoxContainer" parent="HeaderContentSplit/MarginContainer/FooterContentSplit/Footer"]
unique_name_in_owner = true
layout_mode = 2
[node name="EditEnumArray" parent="HeaderContentSplit/MarginContainer/FooterContentSplit/Footer/PropertyEditors" instance=ExtResource("8_234wn")]
visible = false
layout_mode = 2
[node name="EditArray" parent="HeaderContentSplit/MarginContainer/FooterContentSplit/Footer/PropertyEditors" instance=ExtResource("9_nts08")]
visible = false
layout_mode = 2
[node name="EditDict" parent="HeaderContentSplit/MarginContainer/FooterContentSplit/Footer/PropertyEditors" instance=ExtResource("13_il556")]
visible = false
layout_mode = 2
[node name="EditColor" parent="HeaderContentSplit/MarginContainer/FooterContentSplit/Footer/PropertyEditors" instance=ExtResource("10_nsma2")]
visible = false
layout_mode = 2
[node name="EditNumber" parent="HeaderContentSplit/MarginContainer/FooterContentSplit/Footer/PropertyEditors" instance=ExtResource("11_q1ao4")]
visible = false
layout_mode = 2
[node name="EditTexture" parent="HeaderContentSplit/MarginContainer/FooterContentSplit/Footer/PropertyEditors" instance=ExtResource("12_4kr6q")]
visible = false
layout_mode = 2
[node name="Control" type="Control" parent="."]
layout_mode = 2
mouse_filter = 2
metadata/_edit_lock_ = true
[node name="FileDialog" type="FileDialog" parent="Control"]
title = "Open"
size = Vector2i(800, 500)
min_size = Vector2i(800, 400)
ok_button_text = "Open"
mode_overrides_title = false
file_mode = 3
filters = PackedStringArray("*.tres")
[node name="FileDialogText" type="FileDialog" parent="Control"]
title = "Open or Create Text File"
size = Vector2i(800, 500)
min_size = Vector2i(800, 400)
ok_button_text = "Open"
mode_overrides_title = false
file_mode = 3
filters = PackedStringArray("*.csv")
[node name="Info" type="AcceptDialog" parent="Control"]
title = "About"
size = Vector2i(800, 500)
[node name="MarginContainer" type="MarginContainer" parent="Control/Info"]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = 8.0
offset_top = 8.0
offset_right = -8.0
offset_bottom = -49.0
grow_horizontal = 2
grow_vertical = 2
[node name="Panel" type="Panel" parent="Control/Info/MarginContainer"]
layout_mode = 2
[node name="RichTextLabel" type="RichTextLabel" parent="Control/Info/MarginContainer"]
layout_mode = 2
bbcode_enabled = true
text = "Edit Resources as Table 2
\"Welp, it is what it sounds like!\"
Possible inputs:
- Ctrl + Click / Cmd + Click - Select multiple cells in one column
- Shift + Click - Select all cells between A and B in one column
- Up / Down / Shift+Tab / Tab - move cell selection up/down/left/right
- Left/Right - Move cursor along cell text
- Backspace/Delete - Erase text Left / Right from cursor
- Home/End - Move cursor to start/end of cell
- Ctrl + <move/erase> / Cmd + <move/erase> - Move through / Erase whole word
- Ctrl/Cmd + C/V - Copy cells / Paste text into cells
- Ctrl/Cmd + (Shift) + Z - The Savior
If clipboard contains as many lines as there are cells selected, each line is pasted into a separate cell.
Made by Don Tnowe. 2022.
https://twitter.com/don_tnowe
Issues and contribution:
https://github.com/don-tnowe/godot-resources-as-sheets-plugin"
[node name="Settings" type="AcceptDialog" parent="Control"]
title = "Settings"
size = Vector2i(500, 300)
min_size = Vector2i(500, 300)
[node name="Settings" parent="Control/Settings" instance=ExtResource("13_as1sh")]
[node name="ImportExport" type="Window" parent="Control"]
process_mode = 3
initial_position = 4
size = Vector2i(600, 400)
visible = false
transient = true
exclusive = true
min_size = Vector2i(600, 400)
[node name="ImportExport" parent="Control/ImportExport" instance=ExtResource("14_3p12b")]
physics_interpolation_mode = 0
[node name="SelectionActions" parent="Control" instance=ExtResource("23_m53sx")]
visible = false
layout_mode = 2
offset_left = -506.0
offset_top = 65.0
offset_right = -426.0
offset_bottom = 117.0
[node name="InputHandler" type="Node" parent="."]
script = ExtResource("14_2t57a")
[node name="SelectionManager" type="Control" parent="."]
layout_mode = 2
mouse_filter = 2
script = ExtResource("15_mx6qn")
cell_editor_classes = Array[Script]([ExtResource("16_p7n52"), ExtResource("19_oeuko"), ExtResource("17_sofdw"), ExtResource("18_oeewr"), ExtResource("19_7x44x"), ExtResource("20_swsbn"), ExtResource("21_58wf8"), ExtResource("28_a0j2e"), ExtResource("22_bni8r")])
metadata/_edit_lock_ = true
[connection signal="grid_updated" from="." to="HeaderContentSplit/VBoxContainer/HBoxContainer3/Pages" method="_on_grid_updated"]
[connection signal="gui_input" from="." to="InputHandler" method="_gui_input"]
[connection signal="pressed" from="HeaderContentSplit/VBoxContainer/MenuStrip/File" to="." method="_on_File_pressed"]
[connection signal="pressed" from="HeaderContentSplit/VBoxContainer/MenuStrip/SearchProcess" to="." method="_on_SearchProcess_pressed"]
[connection signal="about_to_popup" from="HeaderContentSplit/VBoxContainer/MenuStrip/VisibleCols" to="HeaderContentSplit/VBoxContainer/Columns/Columns" method="_on_visible_cols_about_to_popup"]
[connection signal="pressed" from="HeaderContentSplit/VBoxContainer/MenuStrip/Settings" to="Control/Settings" method="popup_centered"]
[connection signal="pressed" from="HeaderContentSplit/VBoxContainer/MenuStrip/Info" to="Control/Info" method="popup_centered"]
[connection signal="text_submitted" from="HeaderContentSplit/VBoxContainer/HBoxContainer/HBoxContainer/Path" to="." method="_on_path_text_submitted"]
[connection signal="pressed" from="HeaderContentSplit/VBoxContainer/HBoxContainer/HBoxContainer/SelectDir" to="Control/FileDialog" method="popup_centered"]
[connection signal="pressed" from="HeaderContentSplit/VBoxContainer/HBoxContainer/HBoxContainer/DeletePath" to="HeaderContentSplit/VBoxContainer/HBoxContainer/HBoxContainer2/RecentPaths" method="remove_selected_path_from_recent"]
[connection signal="pressed" from="HeaderContentSplit/VBoxContainer/HBoxContainer/HBoxContainer2/ImportExport" to="Control/FileDialogText" method="popup_centered"]
[connection signal="pressed" from="HeaderContentSplit/VBoxContainer/HBoxContainer3/Refresh" to="." method="_on_path_text_submitted"]
[connection signal="value_changed" from="HeaderContentSplit/VBoxContainer/HBoxContainer3/Pages/LineEdit" to="HeaderContentSplit/VBoxContainer/HBoxContainer3/Pages" method="_on_LineEdit_value_changed"]
[connection signal="dir_selected" from="Control/FileDialog" to="." method="_on_FileDialog_dir_selected"]
[connection signal="file_selected" from="Control/FileDialog" to="." method="_on_FileDialog_dir_selected"]
[connection signal="dir_selected" from="Control/FileDialogText" to="Control/ImportExport/ImportExport" method="_on_file_selected"]
[connection signal="file_selected" from="Control/FileDialogText" to="Control/ImportExport/ImportExport" method="_on_file_selected"]
[connection signal="files_selected" from="Control/FileDialogText" to="Control/ImportExport/ImportExport" method="_on_files_selected"]
[connection signal="close_requested" from="Control/ImportExport" to="Control/ImportExport" method="hide"]
[connection signal="cells_rightclicked" from="SelectionManager" to="Control/SelectionActions" method="_on_grid_cells_rightclicked"]
[connection signal="cells_selected" from="SelectionManager" to="Control/SelectionActions" method="_on_grid_cells_selected"]

View File

@ -0,0 +1,36 @@
class_name ResourceTablesEditFormat
extends RefCounted
var editor_view : Control
## Override to define reading behaviour.
func get_value(entry, key : String):
pass
## Override to define writing behaviour. This is NOT supposed to save - use `save_entries`.
func set_value(entry, key : String, value, index : int):
pass
## Override to define how the data gets saved.
func save_entries(all_entries : Array, indices : Array):
pass
## Override to allow editing rows from the Inspector.
func create_resource(entry) -> Resource:
return Resource.new()
## Override to define duplication behaviour. `name_input` should be a suffix if multiple entries, and full name if one.
func duplicate_rows(rows : Array, name_input : String):
pass
## Override to define removal behaviour.
func delete_rows(rows : Array):
pass
## Override with `return true` if `resource_path` is defined and the Rename butoon should show.
func has_row_names():
return false
## Override to define import behaviour. Must return the `rows` value for the editor view.
func import_from_path(folderpath : String, insert_func : Callable, sort_by : String, sort_reverse : bool = false) -> Array:
return []

View File

@ -0,0 +1,88 @@
class_name ResourceTablesEditFormatCsv
extends ResourceTablesEditFormatTres
var import_data : ResourceTablesImport
var csv_rows := []
var resource_original_positions := {}
func get_value(entry, key : String):
return entry.get(key)
func set_value(entry, key : String, value, index : int):
entry.set(key, value)
csv_rows[resource_original_positions[entry]] = import_data.resource_to_strings(entry)
func save_entries(all_entries : Array, indices : Array, repeat : bool = true):
if timer == null or timer.time_left <= 0.0:
var space_after_delimiter := import_data.delimeter.ends_with(" ")
var file := FileAccess.open(import_data.edited_path, FileAccess.WRITE)
for x in csv_rows:
if space_after_delimiter:
for i in x.size():
if i == 0: continue
x[i] = " " + x[i]
file.store_csv_line(x, import_data.delimeter[0])
if repeat:
timer = editor_view.get_tree().create_timer(3.0)
timer.timeout.connect(save_entries.bind(all_entries, indices, false))
func create_resource(entry) -> Resource:
return entry
func duplicate_rows(rows : Array, name_input : String):
for x in rows:
var new_res = x.duplicate()
var index : int = resource_original_positions[x]
csv_rows.insert(index, import_data.resource_to_strings(new_res))
_bump_row_indices(index + 1, 1)
resource_original_positions[new_res] = index + 1
save_entries([], [])
func delete_rows(rows):
for x in rows:
var index : int = resource_original_positions[x]
csv_rows.remove_at(index)
_bump_row_indices(index, -1)
resource_original_positions.erase(x)
save_entries([], [])
func has_row_names():
return false
func _bump_row_indices(from : int, increment : int = 1):
for k in resource_original_positions:
if resource_original_positions[k] >= from:
resource_original_positions[k] += increment
func import_from_path(path : String, insert_func : Callable, sort_by : String, sort_reverse : bool = false) -> Array:
import_data = load(path)
var file := FileAccess.open(import_data.edited_path, FileAccess.READ)
csv_rows = ResourceTablesImportFormatCsv.import_as_arrays(import_data)
var rows := []
var res : Resource
resource_original_positions.clear()
for i in csv_rows.size():
if import_data.remove_first_row and i == 0:
continue
res = import_data.strings_to_resource(csv_rows[i], "")
res.resource_path = ""
insert_func.call(res, rows, sort_by, sort_reverse)
resource_original_positions[res] = i
editor_view.fill_property_data(rows[0])
return rows

View File

@ -0,0 +1,116 @@
class_name ResourceTablesEditFormatTres
extends ResourceTablesEditFormat
var timer : SceneTreeTimer
func get_value(entry, key : String):
return entry[key]
func set_value(entry, key : String, value, index : int):
entry[key] = value
func save_entries(all_entries : Array, indices : Array, repeat : bool = true):
# No need to save. Resources are saved with Ctrl+S
# (likely because plugin.edit_resource is called to show inspector)
return
func create_resource(entry) -> Resource:
return entry
func duplicate_rows(rows : Array, name_input : String):
if rows.size() == 1:
var new_row = rows[0].duplicate()
new_row.resource_path = rows[0].resource_path.get_base_dir() + "/" + name_input + ".tres"
ResourceSaver.save(new_row)
return
var new_row
for x in rows:
new_row = x.duplicate()
new_row.resource_path = x.resource_path.get_basename() + name_input + ".tres"
ResourceSaver.save(new_row)
func rename_row(row, new_name : String):
var new_row = row
DirAccess.open("res://").remove(row.resource_path)
new_row.resource_path = row.resource_path.get_base_dir() + "/" + new_name + ".tres"
ResourceSaver.save(new_row)
func delete_rows(rows):
for x in rows:
DirAccess.open("res://").remove(x.resource_path)
func has_row_names():
return true
func import_from_path(folderpath : String, insert_func : Callable, sort_by : String, sort_reverse : bool = false) -> Array:
var solo_property := ""
var solo_property_split : Array[String] = []
if folderpath.contains("::"):
var found_at := folderpath.find("::")
solo_property = folderpath.substr(found_at + "::".length()).trim_suffix("/")
folderpath = folderpath.left(found_at)
for x in solo_property.split("::"):
solo_property_split.append(x)
var rows := []
var dir := DirAccess.open(folderpath)
if dir == null: return []
var file_stack : Array[String] = []
var folder_stack : Array[String] = [folderpath]
while folder_stack.size() > 0:
folderpath = folder_stack.pop_back()
for x in DirAccess.get_files_at(folderpath):
file_stack.append(folderpath.path_join(x))
for x in DirAccess.get_directories_at(folderpath):
folder_stack.append(folderpath.path_join(x))
var loaded_res_unique := {}
for x in file_stack:
if !x.ends_with(".tres"):
continue
if solo_property.is_empty():
loaded_res_unique[load(x)] = true
else:
_append_soloed_property(load(x), loaded_res_unique, solo_property_split)
for x in loaded_res_unique.keys():
if x == null: continue
insert_func.call(x, rows, sort_by, sort_reverse)
editor_view.fill_property_data_many(loaded_res_unique.keys())
return rows
func _append_soloed_property(current_res : Resource, result : Dictionary, solo_property_split : Array[String], solo_property_split_idx : int = -solo_property_split.size()):
var soloed_value = current_res[solo_property_split[solo_property_split_idx]]
if solo_property_split_idx == -1:
if soloed_value is Resource:
result[soloed_value] = true
elif soloed_value is Array:
for x in soloed_value:
result[x] = true
else:
if soloed_value is Resource:
_append_soloed_property(soloed_value, result, solo_property_split, solo_property_split_idx + 1)
elif soloed_value is Array:
for x in soloed_value:
_append_soloed_property(x, result, solo_property_split, solo_property_split_idx + 1)

View File

@ -0,0 +1,32 @@
class_name ResourceTablesExportFormatCsv
extends RefCounted
static func can_edit_path(path : String):
return path.ends_with(".csv")
static func export_to_file(entries_array : Array, column_names : Array, into_path : String, import_data : ResourceTablesImport):
var file := FileAccess.open(into_path, FileAccess.WRITE)
var line := PackedStringArray()
var space_after_delimiter := import_data.delimeter.ends_with(" ")
import_data.prop_names = column_names
import_data.prop_types = import_data.get_resource_property_types(entries_array[0], column_names)
import_data.resource_path = ""
line.resize(column_names.size())
if import_data.remove_first_row:
for j in column_names.size():
line[j] = String(column_names[j])
if space_after_delimiter and j != 0:
line[j] = " " + line[j]
file.store_csv_line(line, import_data.delimeter[0])
for i in entries_array.size():
for j in column_names.size():
line[j] = import_data.property_to_string((entries_array[i].get(column_names[j])), j)
if space_after_delimiter and j != 0:
line[j] = " " + line[j]
file.store_csv_line(line, import_data.delimeter[0])

View File

@ -0,0 +1,58 @@
class_name ResourceTablesImportFormatCsv
extends RefCounted
static func can_edit_path(path : String):
return path.ends_with(".csv")
static func get_properties(entries, import_data):
return Array(entries[0])
static func import_as_arrays(import_data) -> Array:
var file := FileAccess.open(import_data.edited_path, FileAccess.READ)
import_data.delimeter = ";"
var text_lines : Array[PackedStringArray] = [file.get_line().split(import_data.delimeter)]
var space_after_delimeter := false
var line := text_lines[0]
if line.size() == 0:
return []
if line.size() == 1:
import_data.delimeter = ","
line = line[0].split(import_data.delimeter)
text_lines[0] = line
if line.size() <= 1:
return []
if line[1].begins_with(" "):
for i in line.size():
line[i] = line[i].trim_prefix(" ")
text_lines[0] = line
space_after_delimeter = true
import_data.delimeter += " "
while !file.eof_reached():
line = file.get_csv_line(import_data.delimeter[0])
if space_after_delimeter:
for i in line.size():
line[i] = line[i].trim_prefix(" ")
if line.size() == text_lines[0].size():
text_lines.append(line)
elif line.size() != 1:
line.resize(text_lines[0].size())
text_lines.append(line)
var entries := []
entries.resize(text_lines.size())
for i in entries.size():
entries[i] = text_lines[i]
import_data.prop_names = entries[0]
return entries

View File

@ -0,0 +1,237 @@
@tool
extends Control
@export var prop_list_item_scene : PackedScene
@export var formats_export : Array[Script]
@export var formats_import : Array[Script]
@onready var editor_view := $"../../.."
@onready var filename_options := $"Import/Margins/Scroll/Box/Grid/UseAsFilename"
@onready var classname_field := $"Import/Margins/Scroll/Box/Grid/Classname"
@onready var script_path_field := $"Import/Margins/Scroll/Box/Grid/HBoxContainer/LineEdit"
@onready var prop_list := $"Import/Margins/Scroll/Box"
@onready var format_settings := $"Import/Margins/Scroll/Box/StyleSettingsI"
@onready var file_dialog := $"../../FileDialogText"
var format_extension := ".csv"
var entries := []
var import_data : ResourceTablesImport
func _ready():
hide()
show()
get_parent().min_size = Vector2(600, 400)
get_parent().size = Vector2(600, 400)
func _on_file_selected(path : String):
if !FileAccess.file_exists(path):
if path.get_extension() != "":
# Path is a file path: replace extension
path = path.get_basename() + ".csv"
else:
# Path is a directory: add filename, add extension
path = path.path_join(editor_view.current_path.trim_suffix("/").get_file()) + ".csv"
FileAccess.open(path, FileAccess.WRITE)
import_data = null
for x in DirAccess.get_files_at(path.get_base_dir()):
if !x.ends_with(".tres"):
continue
var found_res := load(path.get_base_dir().path_join(x))
if !(found_res is ResourceTablesImport):
continue
import_data = found_res
_load_settings_from_import_file(found_res)
for format_x in formats_import:
if format_x.new().can_edit_path(path):
entries = format_x.new().import_as_arrays(import_data)
break
if import_data == null:
import_data = ResourceTablesImport.new()
import_data.initialize(path)
for format_x in formats_import:
if format_x.new().can_edit_path(path):
entries = format_x.new().import_as_arrays(import_data)
classname_field.text = import_data.edited_path.get_file().get_basename()\
.capitalize().replace(" ", "")
import_data.script_classname = classname_field.text
_load_property_names_from_textfile(path)
_create_prop_editors()
$"Import/Margins/Scroll/Box/StyleSettingsI"._send_signal()
await get_tree().process_frame
get_parent().popup_centered()
position = Vector2.ZERO
func _on_files_selected(paths : PackedStringArray):
_on_file_selected(paths[0])
func _load_property_names_from_textfile(path : String):
var prop_types := import_data.prop_types
prop_types.resize(import_data.prop_names.size())
prop_types.fill(4)
for i in import_data.prop_names.size():
import_data.prop_names[i] = entries[0][i]\
.replace("\"", "")\
.replace(" ", "_")\
.replace("-", "_")\
.replace(".", "_")\
.replace(",", "_")\
.replace("\t", "_")\
.replace("/", "_")\
.replace("\\", "_")\
.to_lower()
# Don't guess Ints automatically - further rows might have floats
if entries[1][i].is_valid_float():
prop_types[i] = ResourceTablesImport.PropType.FLOAT
elif entries[1][i].begins_with("res://"):
prop_types[i] = ResourceTablesImport.PropType.OBJECT
else:
prop_types[i] = ResourceTablesImport.PropType.STRING
filename_options.clear()
for i in import_data.prop_names.size():
filename_options.add_item(import_data.prop_names[i], i)
func _load_settings_from_import_file(from_file : ResourceTablesImport):
filename_options.clear()
for i in import_data.prop_names.size():
filename_options.add_item(import_data.prop_names[i], i)
if import_data.new_script != null:
classname_field.text = import_data.new_script.get_global_name()
script_path_field.text = from_file.new_script.resource_path
format_settings.set_format_array(import_data.enum_format)
func _create_prop_editors():
for x in prop_list.get_children():
if !x is GridContainer: x.free()
for i in import_data.prop_names.size():
var new_node := prop_list_item_scene.instantiate()
prop_list.add_child(new_node)
new_node.display(import_data.prop_names[i], import_data.prop_types[i])
new_node.connect_all_signals(self, i)
func _generate_class(save_script = true):
save_script = true # Built-ins didn't work in 3.x, won't change because dont wanna test rn
import_data.new_script = import_data.generate_script(entries, save_script)
if save_script:
import_data.new_script.resource_path = import_data.edited_path.get_basename() + ".gd"
ResourceSaver.save(import_data.new_script)
# Because when instanced, objects have a copy of the script
import_data.new_script = load(import_data.edited_path.get_basename() + ".gd")
func _on_import_to_tres_pressed():
if script_path_field.text != "":
import_data.load_external_script(load(script_path_field.text))
if import_data.new_script == null:
_generate_class()
DirAccess.open("res://").make_dir_recursive(import_data.edited_path.get_basename())
import_data.prop_used_as_filename = import_data.prop_names[filename_options.selected]
var new_res : Resource
for i in entries.size():
if import_data.remove_first_row and i == 0:
continue
new_res = import_data.strings_to_resource(entries[i], editor_view.current_path)
ResourceSaver.save(new_res)
await get_tree().process_frame
await get_tree().process_frame
editor_view.refresh()
close()
func _on_import_edit_pressed():
if import_data.new_script == null:
_generate_class(false)
import_data.prop_used_as_filename = ""
import_data.save()
await get_tree().process_frame
editor_view.display_folder(import_data.resource_path)
editor_view.node_columns.hidden_columns[editor_view.current_path] = {
"resource_path" : true,
"resource_local_to_scene" : true,
}
editor_view.save_data()
await get_tree().process_frame
editor_view.refresh()
close()
func _on_export_csv_pressed():
var exported_cols : Array = editor_view.columns.duplicate()
exported_cols.erase(&"resource_local_to_scene")
for x in editor_view.node_columns.hidden_columns[editor_view.current_path].keys():
exported_cols.erase(x)
ResourceTablesExportFormatCsv.export_to_file(editor_view.rows, exported_cols, import_data.edited_path, import_data)
await get_tree().process_frame
editor_view.refresh()
close()
# Input controls
func _on_classname_field_text_changed(new_text : String):
import_data.script_classname = new_text.replace(" ", "")
func _on_remove_first_row_toggled(button_pressed : bool):
import_data.remove_first_row = button_pressed
# $"Export/Box2/Button".button_pressed = true
$"Export/Box3/CheckBox".button_pressed = button_pressed
func _on_list_item_type_selected(type : int, index : int):
import_data.prop_types[index] = type
func _on_list_item_name_changed(name : String, index : int):
import_data.prop_names[index] = name.replace(" ", "")
func _on_export_delimeter_pressed(del : String):
import_data.delimeter = del + import_data.delimeter.substr(1)
func _on_export_space_toggled(button_pressed : bool):
import_data.delimeter = (
import_data.delimeter[0]
if !button_pressed else
import_data.delimeter + " "
)
func _on_enum_format_changed(case, delimiter, bool_yes, bool_no):
import_data.enum_format = [case, delimiter, bool_yes, bool_no]
func close():
get_parent().hide()

View File

@ -0,0 +1,235 @@
[gd_scene load_steps=8 format=3 uid="uid://b413igx28kkvb"]
[ext_resource type="Script" path="res://addons/resources_spreadsheet_view/import_export/import_export_dialog.gd" id="1"]
[ext_resource type="Script" path="res://addons/resources_spreadsheet_view/import_export/formats_export/export_csv.gd" id="2_33c6s"]
[ext_resource type="Script" path="res://addons/resources_spreadsheet_view/import_export/formats_import/import_csv.gd" id="2_fxayt"]
[ext_resource type="PackedScene" uid="uid://b8llymigbprh6" path="res://addons/resources_spreadsheet_view/import_export/property_list_item.tscn" id="2_xfhmf"]
[ext_resource type="PackedScene" uid="uid://ckhf3bqy2rqjr" path="res://addons/resources_spreadsheet_view/import_export/import_export_enum_format.tscn" id="4"]
[ext_resource type="Script" path="res://addons/resources_spreadsheet_view/editor_icon_button.gd" id="5_k5rhi"]
[sub_resource type="ButtonGroup" id="ButtonGroup_080hd"]
[node name="TabContainer" type="TabContainer"]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
current_tab = 0
script = ExtResource("1")
prop_list_item_scene = ExtResource("2_xfhmf")
formats_export = Array[Script]([ExtResource("2_33c6s")])
formats_import = Array[Script]([ExtResource("2_fxayt")])
[node name="Import" type="VBoxContainer" parent="."]
layout_mode = 2
mouse_filter = 2
metadata/_tab_index = 0
[node name="Margins" type="MarginContainer" parent="Import"]
layout_mode = 2
size_flags_vertical = 3
[node name="Scroll" type="ScrollContainer" parent="Import/Margins"]
layout_mode = 2
horizontal_scroll_mode = 0
[node name="Box" type="VBoxContainer" parent="Import/Margins/Scroll"]
layout_mode = 2
size_flags_horizontal = 3
[node name="Grid" type="GridContainer" parent="Import/Margins/Scroll/Box"]
layout_mode = 2
columns = 2
[node name="Label" type="Label" parent="Import/Margins/Scroll/Box/Grid"]
layout_mode = 2
text = "Column Used as Filename:"
[node name="UseAsFilename" type="OptionButton" parent="Import/Margins/Scroll/Box/Grid"]
layout_mode = 2
size_flags_horizontal = 3
[node name="Label4" type="Label" parent="Import/Margins/Scroll/Box/Grid"]
layout_mode = 2
text = "Use Script:"
[node name="HBoxContainer" type="HBoxContainer" parent="Import/Margins/Scroll/Box/Grid"]
layout_mode = 2
[node name="LineEdit" type="LineEdit" parent="Import/Margins/Scroll/Box/Grid/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
placeholder_text = "(create new GDScript)"
editable = false
[node name="Button2" type="Button" parent="Import/Margins/Scroll/Box/Grid/HBoxContainer"]
layout_mode = 2
script = ExtResource("5_k5rhi")
icon_name = "Clear"
[node name="Button" type="Button" parent="Import/Margins/Scroll/Box/Grid/HBoxContainer"]
layout_mode = 2
script = ExtResource("5_k5rhi")
icon_name = "Folder"
[node name="FileDialog" type="FileDialog" parent="Import/Margins/Scroll/Box/Grid/HBoxContainer"]
title = "Choose Script..."
initial_position = 4
size = Vector2i(800, 500)
ok_button_text = "Open"
mode_overrides_title = false
file_mode = 0
[node name="Control" type="Control" parent="Import/Margins/Scroll/Box/Grid"]
layout_mode = 2
[node name="Label5" type="Label" parent="Import/Margins/Scroll/Box/Grid"]
self_modulate = Color(1, 0.84375, 0, 1)
layout_mode = 2
text = "WARNING: Importing with a pre-existing Script will not import datatypes not selectible below."
horizontal_alignment = 1
autowrap_mode = 3
[node name="Label2" type="Label" parent="Import/Margins/Scroll/Box/Grid"]
visible = false
layout_mode = 2
text = "Class Name"
[node name="Classname" type="LineEdit" parent="Import/Margins/Scroll/Box/Grid"]
visible = false
layout_mode = 2
caret_blink = true
caret_blink_interval = 0.5
[node name="Label3" type="Label" parent="Import/Margins/Scroll/Box/Grid"]
layout_mode = 2
text = "Flags:"
[node name="Flags" type="VBoxContainer" parent="Import/Margins/Scroll/Box/Grid"]
layout_mode = 2
[node name="RemoveFirstRow" type="CheckBox" parent="Import/Margins/Scroll/Box/Grid/Flags"]
layout_mode = 2
text = "First row contains property names"
[node name="HSeparator" type="HSeparator" parent="Import/Margins/Scroll/Box"]
layout_mode = 2
[node name="StyleSettingsI" parent="Import/Margins/Scroll/Box" instance=ExtResource("4")]
layout_mode = 2
[node name="Box" type="HBoxContainer" parent="Import"]
layout_mode = 2
mouse_filter = 2
alignment = 1
[node name="Ok2" type="Button" parent="Import/Box"]
layout_mode = 2
text = "Confirm and edit"
[node name="Ok" type="Button" parent="Import/Box"]
layout_mode = 2
text = "Convert to Resources and edit"
[node name="Cancel" type="Button" parent="Import/Box"]
layout_mode = 2
text = "Cancel"
[node name="Control" type="Control" parent="Import"]
layout_mode = 2
mouse_filter = 2
[node name="Export" type="VBoxContainer" parent="."]
visible = false
layout_mode = 2
metadata/_tab_index = 1
[node name="Info" type="Label" parent="Export"]
layout_mode = 2
size_flags_vertical = 0
text = "The currently edited folder will be exported into the selected file.
Rows hidden by the filter will NOT be exported, and order follows the current sorting key. Rows on non-selected pages will not be removed.
Hidden columns will NOT be exported."
autowrap_mode = 2
[node name="HSeparator" type="HSeparator" parent="Export"]
layout_mode = 2
[node name="Box" type="HBoxContainer" parent="Export"]
layout_mode = 2
alignment = 1
[node name="Label2" type="Label" parent="Export/Box"]
layout_mode = 2
size_flags_horizontal = 3
text = "Delimiter:"
[node name="Button" type="Button" parent="Export/Box"]
layout_mode = 2
toggle_mode = true
button_group = SubResource("ButtonGroup_080hd")
text = "Comma (,)"
[node name="Button2" type="Button" parent="Export/Box"]
layout_mode = 2
toggle_mode = true
button_group = SubResource("ButtonGroup_080hd")
text = "Semicolon (;)"
[node name="Button3" type="Button" parent="Export/Box"]
layout_mode = 2
toggle_mode = true
button_group = SubResource("ButtonGroup_080hd")
text = "Tab"
[node name="CheckBox" type="CheckBox" parent="Export/Box"]
layout_mode = 2
text = "With space after"
[node name="Box3" type="HBoxContainer" parent="Export"]
layout_mode = 2
[node name="CheckBox" type="CheckBox" parent="Export/Box3"]
layout_mode = 2
text = "First row contains property names (CSV)"
[node name="StyleSettingsE" parent="Export" instance=ExtResource("4")]
layout_mode = 2
[node name="Control" type="Control" parent="Export"]
layout_mode = 2
size_flags_vertical = 3
[node name="Box2" type="HBoxContainer" parent="Export"]
layout_mode = 2
alignment = 1
[node name="Button" type="Button" parent="Export/Box2"]
layout_mode = 2
text = "Export to CSV"
[node name="Cancel" type="Button" parent="Export/Box2"]
layout_mode = 2
text = "Cancel"
[node name="Control2" type="Control" parent="Export"]
layout_mode = 2
[connection signal="pressed" from="Import/Margins/Scroll/Box/Grid/HBoxContainer/Button2" to="Import/Margins/Scroll/Box/Grid/HBoxContainer/LineEdit" method="set_text" binds= [""]]
[connection signal="pressed" from="Import/Margins/Scroll/Box/Grid/HBoxContainer/Button" to="Import/Margins/Scroll/Box/Grid/HBoxContainer/FileDialog" method="popup_centered"]
[connection signal="file_selected" from="Import/Margins/Scroll/Box/Grid/HBoxContainer/FileDialog" to="Import/Margins/Scroll/Box/Grid/HBoxContainer/LineEdit" method="set_text"]
[connection signal="text_changed" from="Import/Margins/Scroll/Box/Grid/Classname" to="." method="_on_classname_field_text_changed"]
[connection signal="toggled" from="Import/Margins/Scroll/Box/Grid/Flags/RemoveFirstRow" to="." method="_on_remove_first_row_toggled"]
[connection signal="format_changed" from="Import/Margins/Scroll/Box/StyleSettingsI" to="." method="_on_enum_format_changed"]
[connection signal="format_changed" from="Import/Margins/Scroll/Box/StyleSettingsI" to="Export/StyleSettingsE" method="_on_format_changed"]
[connection signal="pressed" from="Import/Box/Ok2" to="." method="_on_import_edit_pressed"]
[connection signal="pressed" from="Import/Box/Ok" to="." method="_on_import_to_tres_pressed"]
[connection signal="pressed" from="Import/Box/Cancel" to="." method="close"]
[connection signal="toggled" from="Export/Box/CheckBox" to="." method="_on_export_space_toggled"]
[connection signal="toggled" from="Export/Box3/CheckBox" to="." method="_on_remove_first_row_toggled"]
[connection signal="format_changed" from="Export/StyleSettingsE" to="." method="_on_enum_format_changed"]
[connection signal="pressed" from="Export/Box2/Button" to="." method="_on_export_csv_pressed"]
[connection signal="pressed" from="Export/Box2/Cancel" to="." method="close"]

View File

@ -0,0 +1,30 @@
@tool
extends GridContainer
signal format_changed(case : int, delimiter : String, bool_yes : String, bool_no: String)
func set_format_array(format : Array):
_on_format_changed(format[0], format[1], format[2], format[3])
format_changed.emit(format[0], format[1], format[2], format[3])
func set_format(case : int, delimiter : String, bool_yes : String, bool_no: String):
_on_format_changed(case, delimiter, bool_yes, bool_no)
format_changed.emit(case, delimiter, bool_yes, bool_no)
func _send_signal(arg1 = null):
format_changed.emit(
$"HBoxContainer/Case".selected,
[" ", "_", "-"][$"HBoxContainer/Separator".selected],
$"HBoxContainer2/True".text,
$"HBoxContainer2/False".text
)
func _on_format_changed(case : int, delimiter : String, bool_yes : String, bool_no: String):
$"HBoxContainer/Case".selected = case
$"HBoxContainer/Separator".selected = [" ", "_", "-"].find(delimiter)
$"HBoxContainer2/True".text = bool_yes
$"HBoxContainer2/False".text = bool_no

View File

@ -0,0 +1,68 @@
[gd_scene load_steps=2 format=3 uid="uid://ckhf3bqy2rqjr"]
[ext_resource type="Script" path="res://addons/resources_spreadsheet_view/import_export/import_export_enum_format.gd" id="1"]
[node name="EnumFormat" type="GridContainer"]
columns = 2
script = ExtResource("1")
[node name="Label3" type="Label" parent="."]
layout_mode = 2
size_flags_horizontal = 3
text = "Enum word case/separator"
[node name="HBoxContainer" type="HBoxContainer" parent="."]
layout_mode = 2
size_flags_horizontal = 3
[node name="Case" type="OptionButton" parent="HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
clip_text = true
selected = 2
item_count = 4
popup/item_0/text = "all lower"
popup/item_1/text = "caps Except First"
popup/item_1/id = 1
popup/item_2/text = "Caps Every Word"
popup/item_2/id = 2
popup/item_3/text = "ALL CAPS"
popup/item_3/id = 3
[node name="Separator" type="OptionButton" parent="HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_stretch_ratio = 0.5
clip_text = true
selected = 0
item_count = 3
popup/item_0/text = "Space \" \""
popup/item_1/text = "Underscore \"_\""
popup/item_1/id = 1
popup/item_2/text = "Kebab \"-\""
popup/item_2/id = 2
[node name="Label4" type="Label" parent="."]
layout_mode = 2
size_flags_horizontal = 3
text = "Boolean True/False"
[node name="HBoxContainer2" type="HBoxContainer" parent="."]
layout_mode = 2
size_flags_horizontal = 3
[node name="True" type="LineEdit" parent="HBoxContainer2"]
layout_mode = 2
size_flags_horizontal = 3
text = "Yes"
[node name="False" type="LineEdit" parent="HBoxContainer2"]
layout_mode = 2
size_flags_horizontal = 3
text = "No"
[connection signal="mouse_entered" from="Label3" to="." method="_on_Label3_mouse_entered"]
[connection signal="item_selected" from="HBoxContainer/Case" to="." method="_send_signal"]
[connection signal="item_selected" from="HBoxContainer/Separator" to="." method="_send_signal"]
[connection signal="text_changed" from="HBoxContainer2/True" to="." method="_send_signal"]
[connection signal="text_changed" from="HBoxContainer2/False" to="." method="_send_signal"]

View File

@ -0,0 +1,12 @@
@tool
extends HBoxContainer
func display(name : String, type : int):
$"LineEdit".text = name
$"OptionButton".selected = type
func connect_all_signals(to : Object, index : int, prefix : String = "_on_list_item_"):
$"LineEdit".text_changed.connect(Callable(to, prefix + "name_changed").bind(index))
$"OptionButton".item_selected.connect(Callable(to, prefix + "type_selected").bind(index))

View File

@ -0,0 +1,34 @@
[gd_scene load_steps=2 format=3 uid="uid://b8llymigbprh6"]
[ext_resource type="Script" path="res://addons/resources_spreadsheet_view/import_export/property_list_item.gd" id="1"]
[node name="Entry" type="HBoxContainer"]
script = ExtResource("1")
[node name="LineEdit" type="LineEdit" parent="."]
layout_mode = 2
size_flags_horizontal = 3
size_flags_stretch_ratio = 0.5
text = "1"
[node name="OptionButton" type="OptionButton" parent="."]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 5
size_flags_stretch_ratio = 0.25
item_count = 7
fit_to_longest_item = false
popup/item_0/text = "Bool"
popup/item_0/id = 1
popup/item_1/text = "Integer Number"
popup/item_1/id = 2
popup/item_2/text = "Floating Point Number"
popup/item_2/id = 3
popup/item_3/text = "String/Other"
popup/item_3/id = 4
popup/item_4/text = "Color"
popup/item_4/id = 14
popup/item_5/text = "Resource Path"
popup/item_5/id = 17
popup/item_6/text = "Enumeration"
popup/item_6/id = 101

View File

@ -0,0 +1,334 @@
@tool
class_name ResourceTablesImport
extends Resource
enum PropType {
BOOL,
INT,
FLOAT,
STRING,
COLOR,
OBJECT,
ENUM,
MAX,
}
enum NameCasing {
ALL_LOWER,
CAPS_WORD_EXCEPT_FIRST,
CAPS_WORD,
ALL_CAPS,
}
const SUFFIX := "_table_import.tres"
const TYPE_MAP := {
TYPE_STRING : PropType.STRING,
TYPE_FLOAT : PropType.FLOAT,
TYPE_BOOL : PropType.BOOL,
TYPE_INT : PropType.INT,
TYPE_OBJECT : PropType.OBJECT,
TYPE_COLOR : PropType.COLOR,
}
@export var prop_types : Array
@export var prop_names : Array
@export var edited_path := "res://"
@export var prop_used_as_filename := ""
@export var script_classname := ""
@export var remove_first_row := true
@export var new_script : Script
@export var view_script : Script = ResourceTablesEditFormatCsv
@export var delimeter := ";"
@export var enum_format : Array = [NameCasing.CAPS_WORD, " ", "Yes", "No"]
@export var uniques : Dictionary
func initialize(path):
edited_path = path
prop_types = []
prop_names = []
func save():
resource_path = edited_path.get_basename() + SUFFIX
ResourceSaver.save.call_deferred(self)
func string_to_property(string : String, col_index : int):
match prop_types[col_index]:
PropType.STRING:
return string
PropType.BOOL:
string = string.to_lower()
if string == enum_format[2].to_lower(): return true
if string == enum_format[3].to_lower(): return false
return !string in ["no", "disabled", "-", "false", "absent", "wrong", "off", "0", ""]
PropType.FLOAT:
return string.to_float()
PropType.INT:
return string.to_int()
PropType.COLOR:
return Color(string)
PropType.OBJECT:
return load(string)
PropType.ENUM:
if string == "":
return int(uniques[col_index]["N_A"])
else:
return int(uniques[col_index][string.capitalize().replace(" ", "_").to_upper()])
func property_to_string(value, col_index : int) -> String:
if value == null: return ""
if prop_types[col_index] is PackedStringArray:
return prop_types[col_index][value].capitalize()
match prop_types[col_index]:
PropType.STRING:
return str(value)
PropType.BOOL:
return enum_format[2] if value else enum_format[3]
PropType.FLOAT, PropType.INT:
return str(value)
PropType.COLOR:
return value.to_html()
PropType.OBJECT:
return value.resource_path
PropType.ENUM:
var dict = uniques[col_index]
for k in dict:
if dict[k] == value:
return change_name_to_format(k, enum_format[0], enum_format[1])
return str(value)
func create_property_line_for_prop(col_index : int) -> String:
var result : String = "@export var " + prop_names[col_index] + " :"
match prop_types[col_index]:
PropType.STRING:
return result + "= \"\"\r\n"
PropType.BOOL:
return result + "= false\r\n"
PropType.FLOAT:
return result + "= 0.0\r\n"
PropType.INT:
return result + "= 0\r\n"
PropType.COLOR:
return result + "= Color.WHITE\r\n"
PropType.OBJECT:
return result + " Resource\r\n"
PropType.ENUM:
return result + " %s\r\n" % _escape_forbidden_enum_names(prop_names[col_index].capitalize().replace(" ", ""))
# return result.replace(
# "@export var",
# "@export_enum(" + _escape_forbidden_enum_names(
# prop_names[col_index].capitalize()\
# .replace(" ", "")
# ) + ") var"
# ) + "= 0\r\n"
return ""
func _escape_forbidden_enum_names(string : String) -> String:
if ClassDB.class_exists(string):
return string + "_"
# Not in ClassDB, but are engine types and can be property names
if string in [
"Color", "String", "Plane", "Projection",
"Basis", "Transform", "Variant",
]:
return string + "_"
return string
func create_enum_for_prop(col_index) -> String:
var result := (
"enum "
+ _escape_forbidden_enum_names(
prop_names[col_index].capitalize().replace(" ", "")
) + " {\r\n"
)
for k in uniques[col_index]:
result += (
"\t"
+ k # Enum Entry
+ " = "
+ str(uniques[col_index][k]) # Value
+ ",\r\n"
)
result += "\tMAX,\r\n}\r\n\r\n"
return result
func generate_script(entries, has_classname = true) -> GDScript:
var source := ""
# if has_classname and script_classname != "":
# source = "class_name " + script_classname + " \r\nextends Resource\r\n\r\n"
#
# else:
source = "extends Resource\r\n\r\n"
# Enums
uniques = get_uniques(entries)
for i in prop_types.size():
if prop_types[i] == PropType.ENUM:
source += create_enum_for_prop(i)
# Properties
for i in prop_names.size():
if (prop_names[i] != "resource_path") and (prop_names[i] != "resource_name"):
source += create_property_line_for_prop(i)
var created_script : GDScript = GDScript.new()
created_script.source_code = source
created_script.reload()
return created_script
func load_external_script(script_res : Script):
new_script = script_res
var result := {}
for x in script_res.get_script_property_list():
if x.hint != PROPERTY_HINT_ENUM or x.type != TYPE_INT:
continue
var cur_value := ""
var result_for_prop := {}
result[prop_names.find(x.name)] = result_for_prop
var hint_arr : Array = x.hint_string.split(",")
for current_hint in hint_arr.size():
var colon_found : int = hint_arr[current_hint].rfind(":")
cur_value = hint_arr[current_hint]
if cur_value == "":
cur_value = "N_A"
if colon_found != -1:
var value_split := cur_value.split(":")
result_for_prop[value_split[1].to_upper()] = value_split[0]
else:
result_for_prop[cur_value.to_upper()] = result_for_prop.size()
uniques = result
func strings_to_resource(strings : Array, destination_path : String) -> Resource:
if destination_path == "":
destination_path = edited_path.get_base_dir().path_join("import/")
DirAccess.make_dir_recursive_absolute(destination_path)
var new_path : String = strings[prop_names.find(prop_used_as_filename)].trim_suffix(".tres") + ".tres"
if !FileAccess.file_exists(new_path):
new_path = destination_path.path_join(new_path) + ".tres"
var new_res : Resource
if FileAccess.file_exists(new_path):
new_res = load(new_path)
else:
new_path = strings[prop_names.find(prop_used_as_filename)].trim_suffix(".tres") + ".tres"
if !new_path.begins_with("res://"):
new_path = destination_path.path_join(new_path)
new_res = new_script.new()
new_res.resource_path = new_path
for j in min(prop_names.size(), strings.size()):
new_res.set(prop_names[j], string_to_property(strings[j], j))
if prop_used_as_filename != "":
new_res.resource_path = new_path
return new_res
func resource_to_strings(res : Resource):
var strings := []
strings.resize(prop_names.size())
for i in prop_names.size():
strings[i] = property_to_string(res.get(prop_names[i]), i)
return PackedStringArray(strings)
func get_uniques(entries : Array) -> Dictionary:
var result := {}
for i in prop_types.size():
if prop_types[i] == PropType.ENUM:
var cur_value := ""
result[i] = {}
for j in entries.size():
if j == 0 and remove_first_row: continue
cur_value = entries[j][i].capitalize().to_upper().replace(" ", "_")
if cur_value == "":
cur_value = "N_A"
if !result[i].has(cur_value):
result[i][cur_value] = result[i].size()
return result
static func change_name_to_format(name : String, case : int, delim : String):
var string := name.capitalize().replace(" ", delim)
if case == NameCasing.ALL_LOWER:
return string.to_lower()
if case == NameCasing.CAPS_WORD_EXCEPT_FIRST:
return string[0].to_lower() + string.substr(1)
if case == NameCasing.CAPS_WORD:
return string
if case == NameCasing.ALL_CAPS:
return string.to_upper()
static func get_resource_property_types(res : Resource, properties : Array) -> Array:
var result := []
result.resize(properties.size())
result.fill(PropType.STRING)
var cur_type := 0
for x in res.get_property_list():
var found := properties.find(x["name"])
if found == -1: continue
if x["usage"] & PROPERTY_USAGE_EDITOR != 0:
if x["hint"] == PROPERTY_HINT_ENUM:
var enum_values : PackedStringArray = x["hint_string"].split(",")
for i in enum_values.size():
var index_found : int = enum_values[i].find(":")
if index_found == -1: continue
enum_values[i] = enum_values[i].left(index_found)
result[found] = enum_values
else:
result[found] = TYPE_MAP.get(x["type"], PropType.STRING)
return result

View File

@ -0,0 +1,123 @@
@tool
extends Control
@onready var editor_view := $"../../../../.."
@onready var node_options := $"Class"
@onready var node_subclasses_check := $"Subclasses"
var found_builtins : Array[String] = []
var found_scripts : Array[Script] = []
var selected_builtin := &""
var selected_script : Script
var selected_is_valid := false
var include_subclasses := true
func _ready():
node_options.item_selected.connect(_on_item_selected)
node_subclasses_check.toggled.connect(_on_subclasses_toggled)
func fill(resources : Array):
node_options.clear()
found_scripts.clear()
found_builtins.clear()
var class_set := {}
for x in resources:
class_set[x.get_script()] = true
class_set[x.get_class()] = true
class_set.erase(null)
class_set.erase("Resource")
for k in class_set:
if k is String:
found_builtins.append(k)
if k is Script:
found_scripts.append(k)
# Add builtins, then script classes, in order.
node_options.add_item("<all>")
for x in found_builtins:
node_options.add_item(x)
node_options.set_item_icon(-1, get_theme_icon(x, "EditorIcons"))
for x in found_scripts:
node_options.add_item(x.resource_path.get_file().get_basename().to_pascal_case())
node_options.set_item_icon(-1, get_theme_icon("Script", "EditorIcons"))
node_options.add_item("")
# Filter is disabled if the already selected class is not in the set.
if not class_set.has(selected_script) and not class_set.has(selected_builtin):
selected_is_valid = false
# When the list is cleared, text and icon are cleared too. Setting to -1 explicitly allows changing icon and label
node_options.selected = -1
if not selected_is_valid:
node_options.set_item_icon(-1, null)
elif selected_builtin == &"" or selected_builtin == &"Resource":
node_options.set_item_icon(-1, get_theme_icon("Script", "EditorIcons"))
else:
node_options.set_item_icon(-1, get_theme_icon(selected_builtin, "EditorIcons"))
show()
func clear():
selected_is_valid = false
func filter(resource : Resource) -> bool:
if not selected_is_valid:
return true
if resource.get_class() != selected_builtin:
if include_subclasses and selected_script == null:
var cur_class := StringName(resource.get_class())
while cur_class != &"Object":
cur_class = ClassDB.get_parent_class(cur_class)
if cur_class == selected_builtin:
return true
return false
if selected_script != null and resource.get_script() != selected_script:
if include_subclasses:
var cur_class : Script = resource.get_script()
while cur_class != null:
cur_class = cur_class.get_base_script()
if cur_class == selected_script:
return true
return false
return true
func _on_item_selected(index : int):
if index == 0:
selected_builtin = &""
selected_script = null
selected_is_valid = false
elif index <= found_builtins.size():
selected_builtin = found_builtins[index - 1]
selected_script = null
selected_is_valid = true
elif index <= found_builtins.size() + found_scripts.size():
selected_script = found_scripts[index - found_builtins.size() - 1]
selected_builtin = selected_script.get_instance_base_type()
selected_is_valid = true
node_options.tooltip_text = "Selected: %s" % node_options.get_item_text(index)
editor_view.refresh()
func _on_subclasses_toggled(state : bool):
include_subclasses = state
editor_view.refresh()

View File

@ -0,0 +1,176 @@
@tool
extends Control
const TablesPluginSettingsClass := preload("res://addons/resources_spreadsheet_view/settings_grid.gd")
@export var table_header_scene : PackedScene
@onready var editor_view : Control = $"../../../.."
@onready var hide_columns_button : MenuButton = $"../../MenuStrip/VisibleCols"
@onready var grid : GridContainer = $"../../../MarginContainer/FooterContentSplit/Panel/Scroll/MarginContainer/TableGrid"
var column_properties := {}
var columns := []:
set(v):
columns = v
for x in get_children():
remove_child(x)
x.queue_free()
var new_node : Control
for x in v:
new_node = table_header_scene.instantiate()
new_node.manager = self
add_child(new_node)
new_node.set_label(x)
new_node.get_node("Button").pressed.connect(editor_view._set_sorting.bind(x))
func _ready():
hide_columns_button\
.get_popup()\
.id_pressed\
.connect(_on_visible_cols_id_pressed)
$"../../../MarginContainer/FooterContentSplit/Panel/Scroll"\
.get_h_scroll_bar()\
.value_changed\
.connect(_on_h_scroll_changed)
func update():
_update_hidden_columns()
_update_column_sizes()
func hide_column(column_index : int):
set_column_property(column_index, &"visibility", 0)
editor_view.save_data()
update()
func set_column_property(column_index : int, property_key : StringName, property_value):
var dict := column_properties
if !dict.has(editor_view.current_path):
dict[editor_view.current_path] = {}
dict = dict[editor_view.current_path]
if !dict.has(columns[column_index]):
dict[columns[column_index]] = {}
dict = dict[columns[column_index]]
dict[property_key] = property_value
func get_column_property(column_index : int, property_key : StringName, property_default = null):
var dict := column_properties
if !dict.has(editor_view.current_path):
return property_default
dict = dict[editor_view.current_path]
if !dict.has(columns[column_index]):
return property_default
dict = dict[columns[column_index]]
return dict.get(property_key, property_default)
func select_column(column_index : int):
editor_view.select_column(column_index)
func _update_column_sizes():
if grid.get_child_count() == 0:
return
await get_tree().process_frame
var column_headers := get_children()
if grid.get_child_count() < column_headers.size(): return
if column_headers.size() != columns.size():
editor_view.refresh()
return
var clip_text : bool = ProjectSettings.get_setting(TablesPluginSettingsClass.PREFIX + "clip_headers")
var min_width := 0
var cell : Control
for i in column_headers.size():
var header = column_headers[i]
cell = grid.get_child(i)
header.get_child(0).clip_text = clip_text
header.custom_minimum_size.x = 0
cell.custom_minimum_size.x = 0
header.size.x = 0
min_width = max(header.size.x, cell.size.x)
header.custom_minimum_size.x = min_width
cell.custom_minimum_size.x = header.get_minimum_size().x
header.size.x = min_width
grid.hide()
grid.show()
hide()
show()
await get_tree().process_frame
# Abort if the node has been deleted since.
if !is_instance_valid(column_headers[0]):
return
get_parent().custom_minimum_size.y = column_headers[0].size.y
for i in column_headers.size():
column_headers[i].position.x = grid.get_child(i).position.x
column_headers[i].size.x = grid.get_child(i).size.x
func _update_hidden_columns():
var current_path : String = editor_view.current_path
var rows_shown : int = editor_view.last_row - editor_view.first_row
if !column_properties.has(current_path):
column_properties[current_path] = {
"resource_local_to_scene" : { &"visibility" : 0 },
"resource_name" : { &"visibility" : 0 },
}
editor_view.save_data()
var visible_column_count := 0
for i in columns.size():
var column_visible : bool = get_column_property(i, &"visibility", 1) != 0
get_child(i).visible = column_visible
for j in rows_shown:
grid.get_child(j * columns.size() + i).visible = column_visible
if column_visible:
visible_column_count += 1
grid.columns = visible_column_count
func _on_h_scroll_changed(value):
position.x = -value
func _on_visible_cols_about_to_popup():
var popup := hide_columns_button.get_popup()
popup.clear()
popup.hide_on_checkable_item_selection = false
for i in columns.size():
popup.add_check_item(columns[i].capitalize(), i)
popup.set_item_checked(i, get_column_property(i, &"visibility", 1) != 0)
func _on_visible_cols_id_pressed(id : int):
var popup := hide_columns_button.get_popup()
if popup.is_item_checked(id):
popup.set_item_checked(id, false)
set_column_property(id, &"visibility", 0)
else:
popup.set_item_checked(id, true)
set_column_property(id, &"visibility", 1)
editor_view.save_data()
update()

View File

@ -0,0 +1,173 @@
@tool
extends Control
@export var editor_view_path : NodePath
@export_enum("Filter", "Process", "Sort") var mode := 0
@export var title := ""
@export var default_text := "":
set(v):
default_text = v
if _textfield == null:
await ready
_textfield.text = v
@export_multiline var default_text_ml := "":
set(v):
default_text_ml = v
if _textfield_ml == null:
await ready
_textfield_ml.text = v
@export var function_save_key := ""
var _textfield : LineEdit
var _textfield_ml : TextEdit
var _togglable_popup : PopupPanel
var _saved_function_index_label : Label
var _saved_functions : Array = []
var _saved_function_selected := -1
func load_saved_functions(func_dict : Dictionary):
if !func_dict.has(function_save_key):
func_dict[function_save_key] = [default_text_ml]
_saved_functions = func_dict[function_save_key]
_on_saved_function_selected(0)
func _ready():
var toggle_button := Button.new()
var popup_box := VBoxContainer.new()
var popup_buttons_box := HBoxContainer.new()
var title_label := Label.new()
var submit_button := Button.new()
var move_label := Label.new()
var move_button_l := Button.new()
var move_button_r := Button.new()
_textfield = LineEdit.new()
_togglable_popup = PopupPanel.new()
_textfield_ml = TextEdit.new()
_saved_function_index_label = Label.new()
add_child(_textfield)
add_child(toggle_button)
_textfield.add_child(_togglable_popup)
_togglable_popup.add_child(popup_box)
popup_box.add_child(title_label)
popup_box.add_child(_textfield_ml)
popup_box.add_child(popup_buttons_box)
popup_buttons_box.add_child(submit_button)
popup_buttons_box.add_child(move_label)
popup_buttons_box.add_child(move_button_l)
popup_buttons_box.add_child(_saved_function_index_label)
popup_buttons_box.add_child(move_button_r)
title_label.text = title
toggle_button.icon = get_theme_icon("Collapse", "EditorIcons")
toggle_button.pressed.connect(_on_expand_pressed)
_textfield.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_textfield.text_submitted.connect(_on_text_submitted.unbind(1))
_textfield_ml.size_flags_horizontal = Control.SIZE_EXPAND_FILL
_textfield_ml.size_flags_vertical = Control.SIZE_EXPAND_FILL
submit_button.text = "Run multiline!"
submit_button.size_flags_horizontal = Control.SIZE_EXPAND_FILL
submit_button.pressed.connect(_on_text_submitted)
move_label.text = "Choose saved:"
move_button_l.icon = get_theme_icon("PagePrevious", "EditorIcons")
move_button_l.pressed.connect(_on_saved_function_bumped.bind(-1))
_on_saved_function_selected(0)
move_button_r.icon = get_theme_icon("PageNext", "EditorIcons")
move_button_r.pressed.connect(_on_saved_function_bumped.bind(+1))
func _on_expand_pressed():
_togglable_popup.popup(Rect2i(_textfield.get_screen_position(), Vector2(size.x, 256.0)))
func _on_text_submitted():
[_table_filter, _table_process][mode].call()
_saved_functions[_saved_function_selected] = _textfield_ml.text
get_node(editor_view_path).save_data.call_deferred()
func _get_script_source_code(first_line : String):
var new_text := ""
if !_togglable_popup.visible:
new_text = _textfield.text
if new_text == "":
new_text = default_text
return first_line + "\treturn " + new_text
else:
new_text = _textfield_ml.text
if new_text == "":
new_text = default_text_ml
var text_split := new_text.split("\n")
for i in text_split.size():
text_split[i] = "\t" + text_split[i]
return first_line + "\n".join(text_split)
func _table_filter():
var new_script := GDScript.new()
new_script.source_code = _get_script_source_code("static func can_show(res, index):\n")
new_script.reload()
var editor_view := get_node(editor_view_path)
editor_view.search_cond = new_script.can_show
editor_view.refresh()
func _table_process():
var new_script := GDScript.new()
new_script.source_code = _get_script_source_code("static func get_result(value, res, all_res, row_index):\n")
new_script.reload()
var editor_view := get_node(editor_view_path)
var new_script_instance := new_script.new()
var values : Array = editor_view.get_edited_cells_values()
var edited_rows : Array[int] = editor_view._selection.get_edited_rows()
var edited_resources := edited_rows.map(func(x): return editor_view.rows[x])
for i in values.size():
values[i] = new_script_instance.get_result(values[i], editor_view.rows[edited_rows[i]], edited_resources, i)
editor_view.set_edited_cells_values(values)
func _on_saved_function_selected(new_index : int):
if new_index < 0:
new_index = 0
if _saved_function_selected == _saved_functions.size() - 1 and _textfield_ml.text == default_text_ml:
_saved_functions.resize(_saved_functions.size() - 1)
elif _saved_function_selected >= 0:
_saved_functions[_saved_function_selected] = _textfield_ml.text
_saved_function_selected = new_index
if new_index >= _saved_functions.size():
_saved_functions.resize(new_index + 1)
for i in _saved_functions.size():
if _saved_functions[i] == null:
_saved_functions[i] = default_text_ml
_textfield_ml.text = _saved_functions[new_index]
_saved_function_index_label.text = "%d/%d" % [new_index + 1, _saved_functions.size()]
get_node(editor_view_path).save_data.call_deferred()
func _on_saved_function_bumped(increment : int):
_on_saved_function_selected(_saved_function_selected + increment)

View File

@ -0,0 +1,148 @@
@tool
extends Node
const TablesPluginEditorViewClass := preload("res://addons/resources_spreadsheet_view/editor_view.gd")
const TablesPluginSelectionManagerClass := preload("res://addons/resources_spreadsheet_view/main_screen/selection_manager.gd")
const TextEditingUtilsClass := preload("res://addons/resources_spreadsheet_view/text_editing_utils.gd")
@onready var editor_view : TablesPluginEditorViewClass = get_parent()
@onready var selection : TablesPluginSelectionManagerClass = get_node("../SelectionManager")
func _on_cell_gui_input(event : InputEvent, cell_node : Control):
var cell := selection.get_cell_node_position(cell_node)
if event is InputEventMouseButton:
editor_view.grab_focus()
if event.button_index == MOUSE_BUTTON_RIGHT and event.pressed:
if !cell in selection.edited_cells:
selection.deselect_all_cells()
selection.select_cell(cell)
selection.rightclick_cells()
if event.button_index == MOUSE_BUTTON_LEFT and event.pressed:
if Input.is_key_pressed(KEY_CTRL):
if cell in selection.edited_cells:
selection.deselect_cell(cell)
else:
selection.select_cell(cell)
elif Input.is_key_pressed(KEY_SHIFT):
selection.select_cells_to(cell)
else:
selection.deselect_all_cells()
selection.select_cell(cell)
func _gui_input(event : InputEvent):
if event is InputEventMouseButton:
if event.button_index == MOUSE_BUTTON_RIGHT and event.is_pressed():
selection.rightclick_cells()
if event.button_index == MOUSE_BUTTON_LEFT:
editor_view.grab_focus()
if !event.pressed:
selection.deselect_all_cells()
func _input(event : InputEvent):
if !event is InputEventKey or !event.pressed:
return
if !editor_view.has_focus() or selection.edited_cells.size() == 0:
return
if event.keycode == KEY_CTRL or event.keycode == KEY_SHIFT:
# Modifier keys do not get processed.
return
# Ctrl + Z (before, and instead of, committing the action!)
if Input.is_key_pressed(KEY_CTRL):
if event.keycode == KEY_Z or event.keycode == KEY_Y:
return
_key_specific_action(event)
editor_view.grab_focus()
editor_view.editor_interface.get_resource_filesystem().scan()
func _key_specific_action(event : InputEvent):
var column := selection.get_cell_column(selection.edited_cells[0])
var ctrl_pressed := Input.is_key_pressed(KEY_CTRL)
# BETWEEN-CELL NAVIGATION
var grid_move_offset := (10 if ctrl_pressed else 1)
if event.keycode == KEY_UP:
_move_selection_on_grid(0, -grid_move_offset)
elif event.keycode == KEY_DOWN:
_move_selection_on_grid(0, +grid_move_offset)
elif Input.is_key_pressed(KEY_SHIFT) and event.keycode == KEY_TAB:
_move_selection_on_grid(-grid_move_offset, 0)
elif event.keycode == KEY_TAB:
_move_selection_on_grid(+grid_move_offset, 0)
elif ctrl_pressed and event.keycode == KEY_C:
TextEditingUtilsClass.multi_copy(selection.edited_cells_text)
get_viewport().set_input_as_handled()
# Ctrl + V
elif ctrl_pressed and event.keycode == KEY_V and editor_view.columns[column] != "resource_path":
selection.clipboard_paste()
get_viewport().set_input_as_handled()
# TEXT CARET MOVEMENT
var caret_move_offset := TextEditingUtilsClass.get_caret_movement_from_key(event.keycode)
if TextEditingUtilsClass.multi_move_caret(caret_move_offset, selection.edited_cells_text, selection.edit_cursor_positions, ctrl_pressed):
selection.queue_redraw()
return
# The following actions do not work on non-editable cells.
if !selection.column_editors[column].is_text() or editor_view.columns[column] == "resource_path":
return
# ERASING
elif event.keycode == KEY_BACKSPACE:
editor_view.set_edited_cells_values_text(TextEditingUtilsClass.multi_erase_left(
selection.edited_cells_text, selection.edit_cursor_positions, ctrl_pressed
))
elif event.keycode == KEY_DELETE:
editor_view.set_edited_cells_values_text(TextEditingUtilsClass.multi_erase_right(
selection.edited_cells_text, selection.edit_cursor_positions, ctrl_pressed
))
get_viewport().set_input_as_handled()
# And finally, text typing.
elif event.keycode == KEY_ENTER:
editor_view.set_edited_cells_values_text(TextEditingUtilsClass.multi_input(
"\n", selection.edited_cells_text, selection.edit_cursor_positions
))
elif event.unicode != 0 and event.unicode != 127:
editor_view.set_edited_cells_values_text(TextEditingUtilsClass.multi_input(
char(event.unicode), selection.edited_cells_text, selection.edit_cursor_positions
))
selection.queue_redraw()
func _move_selection_on_grid(move_h : int, move_v : int):
var selected_cells := selection.edited_cells.duplicate()
var num_columns := editor_view.columns.size()
var num_rows := editor_view.rows.size()
var new_child_pos := Vector2i(0, 0)
for i in selected_cells.size():
new_child_pos = Vector2i(
clamp(selected_cells[i].x + move_h, 0, num_columns - 1),
clamp(selected_cells[i].y + move_v, 0, num_rows - 1),
)
selected_cells[i] = new_child_pos
editor_view.grab_focus()
selection.deselect_all_cells()
selection.select_cells(selected_cells)

View File

@ -0,0 +1,54 @@
@tool
extends OptionButton
@onready var editor_view := $"../../../../.."
var recent_paths := []
func _ready():
item_selected.connect(_on_item_selected)
func load_paths(paths):
clear()
for x in paths:
add_path_to_recent(x, true)
selected = 0
func add_path_to_recent(path : String, is_loading : bool = false):
if path in recent_paths: return
var idx_in_array := recent_paths.find(path)
if idx_in_array != -1:
remove_item(idx_in_array)
recent_paths.remove_at(idx_in_array)
recent_paths.append(path)
add_item(path)
select(get_item_count() - 1)
if !is_loading:
editor_view.save_data()
func remove_selected_path_from_recent():
if get_item_count() == 0:
return
var idx_in_array := selected
recent_paths.remove_at(idx_in_array)
remove_item(idx_in_array)
if get_item_count() != 0:
select(0)
editor_view.display_folder(recent_paths[0])
editor_view.save_data()
func _on_item_selected(index : int):
editor_view.current_path = recent_paths[index]
editor_view.node_folder_path.text = recent_paths[index]
editor_view.refresh()

View File

@ -0,0 +1,191 @@
@tool
extends MarginContainer
enum {
EDITBOX_DUPLICATE = 1,
EDITBOX_RENAME,
EDITBOX_DELETE,
}
const TextEditingUtilsClass := preload("res://addons/resources_spreadsheet_view/text_editing_utils.gd")
const TablesPluginSettingsClass := preload("res://addons/resources_spreadsheet_view/settings_grid.gd")
@onready var editor_view := $"../.."
@onready var selection := $"../../SelectionManager"
@onready var editbox_node := $"Control/ColorRect/Popup"
@onready var editbox_label : Label = editbox_node.get_node("Panel/VBoxContainer/Label")
@onready var editbox_input : LineEdit = editbox_node.get_node("Panel/VBoxContainer/LineEdit")
var cell : Control
var editbox_action : int
func _ready():
editbox_input.get_node("../..").add_theme_stylebox_override(
"panel",
get_theme_stylebox(&"Content", &"EditorStyles")
)
editbox_input.text_submitted.connect(func(_new_text): _on_editbox_accepted())
close()
func _on_grid_cells_rightclicked(cells):
open(cells)
func _on_grid_cells_selected(cells):
open(cells, true, true)
func open(cells : Array, pin_to_cell : bool = false, from_leftclick : bool = false):
if cells.size() == 0 or (from_leftclick and !ProjectSettings.get_setting(TablesPluginSettingsClass.PREFIX + "context_menu_on_leftclick")):
hide()
cell = null
return
if pin_to_cell:
cell = selection.get_cell_node_from_position(cells[-1])
set_deferred(&"global_position", Vector2(
cell.global_position.x + cell.size.x,
cell.global_position.y
))
else:
cell = null
set_deferred(&"global_position", get_global_mouse_position() + Vector2.ONE)
show()
size = Vector2.ZERO
top_level = true
$"Control2/Label".text = str(cells.size()) + (" Cells" if cells.size() % 10 != 1 else " Cell")
$"GridContainer/Rename".visible = editor_view.has_row_names()
$"GridContainer/SoloOpen".visible = editor_view.column_can_solo_open(editor_view.get_selected_column())
func close():
pass
func _input(event : InputEvent):
if !editor_view.is_visible_in_tree():
close()
return
if event is InputEventMouseButton and event.is_pressed():
close()
return
if event is InputEventKey:
if event.is_pressed() and event.is_command_or_control_pressed():
global_position = get_global_mouse_position() + Vector2.ONE
if cell != null:
global_position = Vector2(
cell.global_position.x + cell.size.x,
cell.global_position.y
)
# Dupe
if event.keycode == KEY_D:
_on_Duplicate_pressed()
return
# Rename
if event.keycode == KEY_R:
_on_Rename_pressed()
return
func _on_Duplicate_pressed():
_show_editbox(EDITBOX_DUPLICATE)
func _on_CbCopy_pressed():
TextEditingUtilsClass.multi_copy(selection.edited_cells_text)
func _on_CbPaste_pressed():
selection.clipboard_paste()
func _on_Rename_pressed():
_show_editbox(EDITBOX_RENAME)
func _on_Delete_pressed():
_show_editbox(EDITBOX_DELETE)
func _on_SoloOpen_pressed():
var resources_to_open_unique := {}
for x in editor_view.get_edited_cells_values():
if x is Array:
for y in x:
resources_to_open_unique[y] = true
if x is Resource:
resources_to_open_unique[x] = true
if resources_to_open_unique.size() > 0:
editor_view.display_resources(resources_to_open_unique.keys())
func _show_editbox(action):
editbox_action = action
match action:
EDITBOX_DUPLICATE:
if !editor_view.has_row_names():
_on_editbox_accepted()
return
if selection.edited_cells.size() == 1:
editbox_label.text = "Input new row's name..."
editbox_input.text = editor_view.get_last_selected_row()\
.resource_path.get_file().get_basename()
else:
editbox_label.text = "Input suffix to append to names..."
editbox_input.text = ""
EDITBOX_RENAME:
editbox_label.text = "Input new name for row..."
editbox_input.text = editor_view.get_last_selected_row()\
.resource_path.get_file().get_basename()
EDITBOX_DELETE:
editbox_label.text = "Really delete selected rows? (Irreversible!!!)"
editbox_input.text = editor_view.get_last_selected_row()\
.resource_path.get_file().get_basename()
show()
editbox_input.grab_focus()
editbox_input.caret_column = 999999999
editbox_node.size = Vector2.ZERO
editbox_node.show()
$"Control/ColorRect".show()
$"Control/ColorRect".top_level = true
$"Control/ColorRect".size = get_viewport_rect().size * 4.0
editbox_node.global_position = (
global_position
+ size * 0.5
- editbox_node.get_child(0).size * 0.5
)
func _on_editbox_closed():
editbox_node.hide()
$"Control/ColorRect".hide()
func _on_editbox_accepted():
match(editbox_action):
EDITBOX_DUPLICATE:
editor_view.duplicate_selected_rows(editbox_input.text)
EDITBOX_RENAME:
editor_view.rename_row(editor_view.get_last_selected_row(), editbox_input.text)
EDITBOX_DELETE:
editor_view.delete_selected_rows()
_on_editbox_closed()

View File

@ -0,0 +1,191 @@
[gd_scene load_steps=7 format=3 uid="uid://b51hnttsie7k5"]
[ext_resource type="Script" path="res://addons/resources_spreadsheet_view/editor_icon_button.gd" id="1"]
[ext_resource type="Script" path="res://addons/resources_spreadsheet_view/main_screen/selection_actions.gd" id="1_qv6ov"]
[ext_resource type="Script" path="res://addons/resources_spreadsheet_view/editor_color_setter.gd" id="2_a4ihj"]
[sub_resource type="Image" id="Image_pjk43"]
data = {
"data": PackedByteArray(0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0),
"format": "LumAlpha8",
"height": 16,
"mipmaps": false,
"width": 16
}
[sub_resource type="ImageTexture" id="2"]
image = SubResource("Image_pjk43")
[sub_resource type="StyleBoxFlat" id="StyleBoxFlat_r2l2b"]
content_margin_left = 4.0
content_margin_top = 4.0
content_margin_right = 4.0
content_margin_bottom = 4.0
bg_color = Color(1, 0.365, 0.365, 1)
draw_center = false
border_width_left = 2
border_width_top = 2
border_width_right = 2
border_width_bottom = 2
corner_detail = 1
[node name="SelectionActions" type="MarginContainer"]
offset_right = 80.0
offset_bottom = 52.0
size_flags_horizontal = 9
mouse_filter = 2
script = ExtResource("1_qv6ov")
[node name="Control2" type="Panel" parent="."]
layout_mode = 2
mouse_filter = 2
[node name="ColorRect" type="ColorRect" parent="Control2"]
show_behind_parent = true
layout_mode = 0
mouse_filter = 2
[node name="ColorRect2" type="ColorRect" parent="Control2"]
modulate = Color(0, 0, 0, 1)
show_behind_parent = true
layout_mode = 1
anchors_preset = 9
anchor_bottom = 1.0
offset_left = -2.0
grow_vertical = 2
mouse_filter = 2
script = ExtResource("2_a4ihj")
[node name="Label" type="Label" parent="Control2"]
layout_mode = 0
offset_top = -26.0
offset_right = 57.0
text = "Actions"
vertical_alignment = 2
[node name="ColorRect3" type="Panel" parent="Control2/Label"]
show_behind_parent = true
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = -2.0
offset_top = 2.0
offset_right = 2.0
grow_horizontal = 2
grow_vertical = 2
mouse_filter = 2
[node name="GridContainer" type="GridContainer" parent="."]
layout_mode = 2
size_flags_horizontal = 9
size_flags_vertical = 9
mouse_filter = 2
columns = 3
[node name="Duplicate" type="Button" parent="GridContainer"]
layout_mode = 2
tooltip_text = "Duplicate Selected Rows (Ctrl+D)"
mouse_filter = 1
icon = SubResource("2")
script = ExtResource("1")
icon_name = "Duplicate"
[node name="CbCopy" type="Button" parent="GridContainer"]
layout_mode = 2
tooltip_text = "Copy to Clipboard (Ctrl+C)"
mouse_filter = 1
icon = SubResource("2")
script = ExtResource("1")
icon_name = "ActionCopy"
[node name="CbPaste" type="Button" parent="GridContainer"]
layout_mode = 2
tooltip_text = "Paste Clipboard (Ctrl+V)"
mouse_filter = 1
icon = SubResource("2")
script = ExtResource("1")
icon_name = "ActionPaste"
[node name="Rename" type="Button" parent="GridContainer"]
layout_mode = 2
tooltip_text = "Rename Selected Rows (Ctrl+R)"
mouse_filter = 1
icon = SubResource("2")
script = ExtResource("1")
icon_name = "Edit"
[node name="Delete" type="Button" parent="GridContainer"]
layout_mode = 2
tooltip_text = "DELETE Selected Rows"
mouse_filter = 1
icon = SubResource("2")
script = ExtResource("1")
icon_name = "Remove"
[node name="SoloOpen" type="Button" parent="GridContainer"]
layout_mode = 2
tooltip_text = "Open selected cells as new table"
mouse_filter = 1
icon = SubResource("2")
script = ExtResource("1")
icon_name = "Object"
[node name="Control" type="Control" parent="."]
layout_mode = 2
mouse_filter = 2
[node name="ColorRect" type="ColorRect" parent="Control"]
visible = false
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
offset_right = -80.0
offset_bottom = -52.0
grow_horizontal = 2
grow_vertical = 2
color = Color(0, 0, 0, 0.498039)
[node name="Popup" type="MarginContainer" parent="Control/ColorRect"]
layout_mode = 0
offset_top = 100.0
offset_right = 140.0
offset_bottom = 196.0
[node name="Panel" type="PanelContainer" parent="Control/ColorRect/Popup"]
layout_mode = 2
mouse_filter = 2
theme_override_styles/panel = SubResource("StyleBoxFlat_r2l2b")
[node name="VBoxContainer" type="VBoxContainer" parent="Control/ColorRect/Popup/Panel"]
layout_mode = 2
mouse_filter = 2
[node name="Label" type="Label" parent="Control/ColorRect/Popup/Panel/VBoxContainer"]
layout_mode = 2
text = "Input new name..."
[node name="LineEdit" type="LineEdit" parent="Control/ColorRect/Popup/Panel/VBoxContainer"]
layout_mode = 2
[node name="HBoxContainer" type="HBoxContainer" parent="Control/ColorRect/Popup/Panel/VBoxContainer"]
layout_mode = 2
alignment = 1
[node name="Button" type="Button" parent="Control/ColorRect/Popup/Panel/VBoxContainer/HBoxContainer"]
layout_mode = 2
text = "OK"
[node name="Button2" type="Button" parent="Control/ColorRect/Popup/Panel/VBoxContainer/HBoxContainer"]
layout_mode = 2
text = "Cancel"
[connection signal="pressed" from="GridContainer/Duplicate" to="." method="_on_Duplicate_pressed"]
[connection signal="pressed" from="GridContainer/CbCopy" to="." method="_on_CbCopy_pressed"]
[connection signal="pressed" from="GridContainer/CbPaste" to="." method="_on_CbPaste_pressed"]
[connection signal="pressed" from="GridContainer/Rename" to="." method="_on_Rename_pressed"]
[connection signal="pressed" from="GridContainer/Delete" to="." method="_on_Delete_pressed"]
[connection signal="pressed" from="GridContainer/SoloOpen" to="." method="_on_SoloOpen_pressed"]
[connection signal="pressed" from="Control/ColorRect/Popup/Panel/VBoxContainer/HBoxContainer/Button" to="." method="_on_editbox_accepted"]
[connection signal="pressed" from="Control/ColorRect/Popup/Panel/VBoxContainer/HBoxContainer/Button2" to="." method="_on_editbox_closed"]

View File

@ -0,0 +1,302 @@
@tool
extends Control
signal cells_selected(cells_positions)
signal cells_rightclicked(cells_positions)
const EditorViewClass := preload("res://addons/resources_spreadsheet_view/editor_view.gd")
const TextEditingUtilsClass := preload("res://addons/resources_spreadsheet_view/text_editing_utils.gd")
@export var cell_editor_classes : Array[Script] = []
@onready var node_property_editors : VBoxContainer = $"../HeaderContentSplit/MarginContainer/FooterContentSplit/Footer/PropertyEditors"
@onready var scrollbar : ScrollContainer = $"../HeaderContentSplit/MarginContainer/FooterContentSplit/Panel/Scroll"
@onready var editor_view : EditorViewClass = get_parent()
var edited_cells : Array = []
var edited_cells_text : Array = []
var edit_cursor_positions : Array[int] = []
var all_cell_editors : Array = []
var column_editors : Array[Object] = []
var inspector_resource : Resource
func _ready():
# Load cell editors and instantiate them
for x in cell_editor_classes:
all_cell_editors.append(x.new())
all_cell_editors[all_cell_editors.size() - 1].hint_strings_array = editor_view.column_hint_strings
get_parent()\
.editor_interface\
.get_inspector()\
.property_edited\
.connect(_on_inspector_property_edited)
scrollbar.get_h_scroll_bar().value_changed.connect(queue_redraw.unbind(1), CONNECT_DEFERRED)
scrollbar.get_v_scroll_bar().value_changed.connect(queue_redraw.unbind(1), CONNECT_DEFERRED)
if ProjectSettings.get_setting(editor_view.TablesPluginSettingsClass.PREFIX + "fold_docks", false):
for x in node_property_editors.get_children():
x.resize_set_hidden(true)
func _draw():
var font := get_theme_font("font", "Label")
var font_size := get_theme_font_size("font", "Label")
var label_padding_left := 2.0
var newline_char := 10
if edit_cursor_positions.size() != edited_cells.size():
return
for i in edited_cells.size():
if edit_cursor_positions[i] >= edited_cells_text[i].length():
continue
var cell : Control = get_cell_node_from_position(edited_cells[i])
var caret_rect := TextEditingUtilsClass.get_caret_rect(edited_cells_text[i], edit_cursor_positions[i], font, font_size, label_padding_left, 2.0)
caret_rect.position += cell.global_position - global_position
draw_rect(caret_rect, Color(1, 1, 1, 0.5))
func initialize_editors(column_values, column_types, column_hints):
_set_visible_selected(false)
column_editors.clear()
for i in column_values.size():
for x in all_cell_editors:
if x.can_edit_value(column_values[i], column_types[i], column_hints[i], i):
column_editors.append(x)
break
func deselect_all_cells():
_set_visible_selected(false)
edited_cells.clear()
edited_cells_text.clear()
edit_cursor_positions.clear()
_selection_changed()
func deselect_cell(cell : Vector2i):
var idx := edited_cells.find(cell)
if idx == -1: return
edited_cells.remove_at(idx)
if edited_cells_text.size() != 0:
edited_cells_text.remove_at(idx)
edit_cursor_positions.remove_at(idx)
var cell_node := get_cell_node_from_position(cell)
if cell_node != null:
column_editors[get_cell_column(cell)].set_selected(cell_node, false)
_selection_changed()
func select_cell(cell : Vector2i):
var column_index := get_cell_column(cell)
if edited_cells.size() == 0 or edited_cells[0].x == cell.x:
_add_cell_to_selection(cell)
_try_open_docks(cell)
inspector_resource = editor_view.rows[get_cell_row(cell)]
editor_view.editor_plugin.get_editor_interface().edit_resource(inspector_resource)
_selection_changed()
func select_cells(cells : Array):
var last_selectible := Vector2i(-1, -1)
var started_empty := edited_cells.size() == 0
for x in cells:
if started_empty or edited_cells[0].x != x.x:
_add_cell_to_selection(x)
if get_cell_node_from_position(x) != null:
last_selectible = x
if last_selectible != Vector2i(-1, -1):
select_cell(last_selectible)
func select_cells_to(cell : Vector2i):
var column_index := get_cell_column(cell)
if edited_cells.size() == 0 or column_index != get_cell_column(edited_cells[-1]):
return
var row_start := get_cell_row(edited_cells[-1])
var row_end := get_cell_row(cell)
var edge_shift := -1 if row_start > row_end else 1
row_start += edge_shift
row_end += edge_shift
var column_editor := column_editors[column_index]
for i in range(row_start, row_end, edge_shift):
var cur_cell := Vector2i(column_index, i)
var cur_cell_node := get_cell_node_from_position(cur_cell)
if cur_cell not in edited_cells:
edited_cells.append(cur_cell)
var cur_cell_value = editor_view.io.get_value(editor_view.rows[cur_cell.y], editor_view.columns[cur_cell.x])
var cur_cell_text : String = column_editor.to_text(cur_cell_value)
edited_cells_text.append(cur_cell_text)
edit_cursor_positions.append(cur_cell_text.length())
if cur_cell_node == null or !cur_cell_node.visible or cur_cell_node.mouse_filter == MOUSE_FILTER_IGNORE:
# When showing several classes, empty cells will be non-selectable.
continue
column_editors[column_index].set_selected(cur_cell_node, true)
_selection_changed()
func rightclick_cells():
cells_rightclicked.emit(edited_cells)
func is_cell_node_selected(cell : Control) -> bool:
return get_cell_node_position(cell) in edited_cells
func is_cell_selected(cell : Vector2i) -> bool:
return cell in edited_cells
func can_select_cell(cell : Vector2i) -> bool:
if edited_cells.size() == 0:
return true
if (
get_cell_column(cell)
!= get_cell_column(edited_cells[0])
):
return false
return !cell in edited_cells
func get_cell_node_from_position(cell_pos : Vector2i) -> Control:
var cell_index := (cell_pos.y - editor_view.first_row) * editor_view.columns.size() + cell_pos.x
if cell_index < 0 or cell_index >= editor_view.node_table_root.get_child_count():
return null
return editor_view.node_table_root.get_child(cell_index)
func get_cell_node_position(cell : Control) -> Vector2i:
var col_count := editor_view.columns.size()
var cell_index := cell.get_index()
return Vector2i(cell_index % col_count, cell_index / col_count + editor_view.first_row)
func get_cell_column(cell : Vector2i) -> int:
return cell.x
func get_cell_row(cell : Vector2i) -> int:
return cell.y
func get_edited_rows() -> Array[int]:
var rows : Array[int] = []
rows.resize(edited_cells.size())
for i in rows.size():
rows[i] = get_cell_row(edited_cells[i])
return rows
func clipboard_paste():
if column_editors[edited_cells[0].x].is_text():
editor_view.set_edited_cells_values(
TextEditingUtilsClass.multi_paste(
edited_cells_text,
edit_cursor_positions,
)
)
elif DisplayServer.clipboard_has():
var values := []
values.resize(edited_cells.size())
var pasted_lines := DisplayServer.clipboard_get().replace("\r", "").split("\n")
var paste_each_line := pasted_lines.size() == values.size()
for i in values.size():
values[i] = str_to_var(
pasted_lines[i] if paste_each_line else DisplayServer.clipboard_get()
)
editor_view.set_edited_cells_values(values)
func _selection_changed():
queue_redraw()
cells_selected.emit(edited_cells)
func _set_visible_selected(state : bool):
for x in edited_cells:
var cell_node := get_cell_node_from_position(x)
if cell_node != null:
column_editors[get_cell_column(x)].set_selected(cell_node, state)
func _add_cell_to_selection(cell : Vector2i):
edited_cells.append(cell)
var column_editor := column_editors[get_cell_column(cell)]
var cell_node := get_cell_node_from_position(cell)
if cell_node != null:
column_editor.set_selected(cell_node, true)
var cell_value = editor_view.io.get_value(editor_view.rows[cell.y], editor_view.columns[cell.x])
var text_value : String = column_editor.to_text(cell_value)
edited_cells_text.append(text_value)
edit_cursor_positions.append(text_value.length())
func _update_selected_cells_text():
if edited_cells_text.size() == 0:
return
var column_editor := column_editors[get_cell_column(edited_cells[0])]
for i in edited_cells.size():
edited_cells_text[i] = column_editor.to_text(editor_view.io.get_value(
editor_view.rows[edited_cells[i].y],
editor_view.columns[edited_cells[i].x],
))
edit_cursor_positions[i] = edited_cells_text[i].length()
func _try_open_docks(cell : Vector2i):
var column_index := get_cell_column(cell)
var row = editor_view.rows[get_cell_row(cell)]
var column := editor_view.columns[column_index]
var type := editor_view.column_types[column_index]
var hints := editor_view.column_hints[column_index]
for x in node_property_editors.get_children():
x.visible = x.try_edit_value(editor_view.io.get_value(row, column), type, hints)
x.get_node(x.path_property_name).text = column
func _on_inspector_property_edited(property : String):
if !editor_view.is_visible_in_tree(): return
if inspector_resource != editor_view.editor_plugin.get_editor_interface().get_inspector().get_edited_object():
return
if editor_view.columns[get_cell_column(edited_cells[0])] != property:
var columns := editor_view.columns
var previously_edited := edited_cells.duplicate()
var new_column := columns.find(property)
deselect_all_cells()
for i in previously_edited.size():
_add_cell_to_selection(Vector2i(new_column, previously_edited[i].y))
var values := []
values.resize(edited_cells.size())
values.fill(inspector_resource[property])
editor_view.set_edited_cells_values.call_deferred(values)
_try_open_docks(edited_cells[0])

View File

@ -0,0 +1,72 @@
@tool
extends HBoxContainer
var manager : Control
func set_label(label : String):
$"Button".text = label.capitalize()
$"Button".tooltip_text = label + "\nClick to sort."
func _ready():
$"Button".gui_input.connect(_on_main_gui_input)
$"Button2".about_to_popup.connect(_on_about_to_popup)
$"Button2".get_popup().id_pressed.connect(_on_list_id_pressed)
func _on_about_to_popup():
var menu_popup : PopupMenu = $"Button2".get_popup()
menu_popup.clear()
menu_popup.add_item("Select All", 0)
menu_popup.add_item("Hide", 1)
if !manager.editor_view.column_can_solo_open(get_index()):
menu_popup.add_item("(not a Resource property)", 2)
menu_popup.set_item_disabled(2, true)
menu_popup.add_separator("", 3)
else:
menu_popup.add_item("Open Sub-Resources of Column", 2)
if manager.editor_view.get_edited_cells_values().size() == 0 or manager.editor_view.get_selected_column() != get_index():
menu_popup.add_item("(none selected)", 3)
menu_popup.set_item_disabled(3, true)
else:
menu_popup.add_item("Open Sub-Resources in Selection", 3)
func _on_main_gui_input(event : InputEvent):
if event is InputEventMouseButton and event.pressed:
var popup : Popup = $"Button2".get_popup()
if event.button_index == MOUSE_BUTTON_RIGHT:
_on_about_to_popup()
popup.visible = !popup.visible
popup.size = Vector2.ZERO
popup.position = Vector2i(get_global_mouse_position()) + get_viewport().position
else:
popup.visible = false
func _on_list_id_pressed(id : int):
match id:
0:
manager.select_column(get_index())
1:
manager.hide_column(get_index())
2:
manager.editor_view.column_solo_open(get_index())
3:
var resources_to_open_unique := {}
for x in manager.editor_view.get_edited_cells_values():
if x is Array:
for y in x:
resources_to_open_unique[y] = true
if x is Resource:
resources_to_open_unique[x] = true
if resources_to_open_unique.size() > 0:
manager.editor_view.display_resources(resources_to_open_unique.keys())

View File

@ -0,0 +1,23 @@
[gd_scene load_steps=3 format=3 uid="uid://d1s6oihqedvo5"]
[ext_resource type="Script" path="res://addons/resources_spreadsheet_view/main_screen/table_header.gd" id="1_5fd1m"]
[ext_resource type="Script" path="res://addons/resources_spreadsheet_view/editor_icon_button.gd" id="2_0ymob"]
[node name="Header" type="HBoxContainer"]
offset_right = 179.0
offset_bottom = 31.0
size_flags_horizontal = 3
script = ExtResource("1_5fd1m")
[node name="Button" type="Button" parent="."]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 0
text = "resource_name"
clip_text = true
[node name="Button2" type="MenuButton" parent="."]
layout_mode = 2
size_flags_horizontal = 9
script = ExtResource("2_0ymob")
icon_name = "ArrowDown"

View File

@ -0,0 +1,115 @@
@tool
extends HBoxContainer
@onready var node_editor_view_root : Control = $"../../../.."
var rows_per_page := 50
var current_page := 0
func update_page_count(array : Array) -> int:
var page_count : int = (node_editor_view_root.rows.size() - 1) / rows_per_page + 1
node_editor_view_root.first_row = min(current_page, page_count) * rows_per_page
node_editor_view_root.last_row = min(node_editor_view_root.first_row + rows_per_page, array.size())
return page_count
func _on_grid_updated():
visible = true
var page_count := update_page_count(node_editor_view_root.rows)
var pagelist_node := $"Scroll/Pagelist"
for x in pagelist_node.get_children():
x.queue_free()
var button_group := ButtonGroup.new()
var btns := []
btns.resize(page_count)
for i in page_count:
var btn := Button.new()
btns[i] = btn
btn.toggle_mode = true
btn.button_group = button_group
btn.text = str(i + 1)
btn.pressed.connect(_on_button_pressed.bind(btn))
btn.size_flags_vertical = SIZE_SHRINK_CENTER
pagelist_node.add_child(btn)
var pagelist_line := HSeparator.new()
pagelist_line.size_flags_horizontal = SIZE_EXPAND_FILL
pagelist_node.add_child(pagelist_line)
btns[current_page].button_pressed = true
var sort_property : StringName = node_editor_view_root.sorting_by
if sort_property == "": sort_property = "resource_path"
var sort_type : int = node_editor_view_root.column_types[node_editor_view_root.columns.find(sort_property)]
var property_values := []
property_values.resize(page_count)
if(node_editor_view_root.rows.size() == 0):
return
for i in page_count:
property_values[i] = node_editor_view_root.rows[i * rows_per_page].get(sort_property)
if sort_type == TYPE_FLOAT or sort_type == TYPE_INT:
for i in page_count:
btns[i].text = str(property_values[i])
elif sort_type == TYPE_COLOR:
for i in page_count:
btns[i].self_modulate = property_values[i] * 0.75 + Color(0.25, 0.25, 0.25, 1.0)
elif sort_type == TYPE_STRING:
var strings := []
strings.resize(page_count)
for i in page_count:
strings[i] = property_values[i].get_file()
if strings[i] == "":
strings[i] = str(i)
_fill_buttons_with_prefixes(btns, strings, page_count)
elif sort_type == TYPE_OBJECT:
var strings := []
strings.resize(page_count + 1)
for i in page_count:
if is_instance_valid(property_values[i]):
strings[i] = property_values[i].resource_path.get_file()
_fill_buttons_with_prefixes(btns, strings, page_count)
func _fill_buttons_with_prefixes(btns : Array, strings : Array, page_count : int):
for i in page_count:
if strings[i] == null:
continue
if i == 0:
btns[0].text = strings[0][0]
continue
for j in strings[i].length():
if strings[i].unicode_at(j) != strings[i - 1].unicode_at(j):
btns[i].text = strings[i].left(j + 1)
btns[i - 1].text = strings[i - 1].left(max(j + 1, btns[i - 1].text.length()))
break
for i in page_count - 1:
btns[i].text = btns[i].text + "-" + btns[i + 1].text
btns[page_count - 1].text += "-[End]"
func _on_button_pressed(button):
button.button_pressed = true
current_page = button.get_index()
_update_view()
func _on_LineEdit_value_changed(value):
rows_per_page = value
current_page = 0
_update_view()
func _update_view():
node_editor_view_root.refresh(false)

View File

@ -0,0 +1,9 @@
[plugin]
name="Edit Resources as Table"
description="Edit Many Resources from one Folder as a table.
Heavily inspired by Multi-Cursor-Editing in text editors, so after selecting multiple cells (in the same column!) using Ctrl+Click or Shift+Click, most Basic-to-Intermediate movements should be available."
author="Don Tnowe"
version="2.7"
script="plugin.gd"

View File

@ -0,0 +1,43 @@
@tool
extends EditorPlugin
var editor_view : Control
var undo_redo : EditorUndoRedoManager
func _enter_tree() -> void:
editor_view = load(get_script().resource_path.get_base_dir() + "/editor_view.tscn").instantiate()
editor_view.editor_interface = get_editor_interface()
if editor_view.editor_interface == null:
# 4.2: now a singleton
editor_view.editor_interface = Engine.get_singleton("EditorInterface")
editor_view.editor_plugin = self
undo_redo = get_undo_redo()
get_editor_interface().get_editor_main_screen().add_child(editor_view)
_make_visible(false)
func _exit_tree() -> void:
if is_instance_valid(editor_view):
editor_view.queue_free()
func _get_plugin_name():
return "ResourceTables"
func _make_visible(visible):
if is_instance_valid(editor_view):
editor_view.visible = visible
if visible:
editor_view.display_folder(editor_view.current_path)
func _has_main_screen():
return true
func _get_plugin_icon():
# Until I add an actual icon, this'll do.
return get_editor_interface().get_base_control().get_theme_icon("VisualShaderNodeComment", "EditorIcons")

View File

@ -0,0 +1,39 @@
@tool
extends GridContainer
const PREFIX := "addons/resources_spreadsheet_view/"
func _ready():
ProjectSettings.set_setting(PREFIX + "array_color_tint", 100.0 if ProjectSettings.get_setting(PREFIX + "color_arrays", true) else 0.0)
ProjectSettings.set_setting(PREFIX + "color_arrays", null)
for x in get_children():
var setting : String = PREFIX + x.name.to_snake_case()
if x is CheckBox:
x.toggled.connect(_set_setting.bind(setting))
if !ProjectSettings.has_setting(setting):
_set_setting(x.button_pressed, setting)
else:
x.button_pressed = ProjectSettings.get_setting(setting)
elif x is OptionButton:
x.item_selected.connect(_set_setting.bind(setting))
if !ProjectSettings.has_setting(setting):
_set_setting(x.selected, setting)
else:
x.selected = ProjectSettings.get_setting(setting)
elif x is Range:
x.value_changed.connect(_set_setting.bind(setting))
if !ProjectSettings.has_setting(setting):
_set_setting(x.value, setting)
else:
x.value = ProjectSettings.get_setting(setting)
func _set_setting(new_value, setting : String):
ProjectSettings.set_setting(setting, new_value)

View File

@ -0,0 +1,130 @@
[gd_scene load_steps=2 format=3 uid="uid://dhunxgcae6h1"]
[ext_resource type="Script" path="res://addons/resources_spreadsheet_view/settings_grid.gd" id="1_s8s2f"]
[node name="Settings" type="ScrollContainer"]
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = 8.0
offset_top = 8.0
offset_right = -622.0
offset_bottom = -322.0
grow_horizontal = 2
grow_vertical = 2
[node name="RichTextLabel" type="VBoxContainer" parent="."]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
[node name="GridContainer" type="GridContainer" parent="RichTextLabel"]
layout_mode = 2
columns = 2
script = ExtResource("1_s8s2f")
[node name="Label" type="Label" parent="RichTextLabel/GridContainer"]
layout_mode = 2
size_flags_horizontal = 3
text = "Color-type cells style rows"
autowrap_mode = 2
[node name="ColorRows" type="CheckBox" parent="RichTextLabel/GridContainer"]
layout_mode = 2
button_pressed = true
text = "Enable"
[node name="Label2" type="Label" parent="RichTextLabel/GridContainer"]
layout_mode = 2
size_flags_horizontal = 3
text = "Collection item color tint"
autowrap_mode = 2
[node name="ArrayColorTint" type="SpinBox" parent="RichTextLabel/GridContainer"]
layout_mode = 2
value = 100.0
[node name="Label3" type="Label" parent="RichTextLabel/GridContainer"]
layout_mode = 2
size_flags_horizontal = 3
text = "Array cell min width"
autowrap_mode = 2
[node name="ArrayMinWidth" type="SpinBox" parent="RichTextLabel/GridContainer"]
layout_mode = 2
min_value = 32.0
max_value = 512.0
value = 128.0
[node name="Label8" type="Label" parent="RichTextLabel/GridContainer"]
layout_mode = 2
size_flags_horizontal = 3
text = "Resource preview size (Including Textures)"
autowrap_mode = 2
[node name="ResourcePreviewSize" type="SpinBox" parent="RichTextLabel/GridContainer"]
layout_mode = 2
min_value = 8.0
max_value = 1024.0
value = 32.0
[node name="Label7" type="Label" parent="RichTextLabel/GridContainer"]
layout_mode = 2
size_flags_horizontal = 3
text = "Clip header text (makes columns smaller)"
autowrap_mode = 2
[node name="ClipHeaders" type="CheckBox" parent="RichTextLabel/GridContainer"]
layout_mode = 2
text = "Enable"
[node name="Label5" type="Label" parent="RichTextLabel/GridContainer"]
layout_mode = 2
size_flags_horizontal = 3
text = "Duplicate arrays on edit (slower, but can be undone)"
autowrap_mode = 2
[node name="DupeArrays" type="CheckBox" parent="RichTextLabel/GridContainer"]
layout_mode = 2
button_pressed = true
text = "Enable"
[node name="Label6" type="Label" parent="RichTextLabel/GridContainer"]
layout_mode = 2
size_flags_horizontal = 3
text = "Show context menu on left-click (and not just rightclick)"
autowrap_mode = 2
[node name="ContextMenuOnLeftclick" type="CheckBox" parent="RichTextLabel/GridContainer"]
layout_mode = 2
text = "Enable"
[node name="Label9" type="Label" parent="RichTextLabel/GridContainer"]
layout_mode = 2
size_flags_horizontal = 3
text = "Fold bottom docks on startup (drag their top bar to resize)"
autowrap_mode = 2
[node name="FoldDocks" type="CheckBox" parent="RichTextLabel/GridContainer"]
layout_mode = 2
button_pressed = true
text = "Enable"
[node name="Label10" type="Label" parent="RichTextLabel/GridContainer"]
layout_mode = 2
size_flags_horizontal = 3
text = "Resources in cells (allow viewing _to_string() override's result)"
autowrap_mode = 2
[node name="ResourceCellLabelMode" type="OptionButton" parent="RichTextLabel/GridContainer"]
layout_mode = 2
toggle_mode = false
selected = 1
allow_reselect = true
item_count = 3
popup/item_0/text = "Name + ToString()"
popup/item_0/id = 3
popup/item_1/text = "ToString()"
popup/item_1/id = 1
popup/item_2/text = "Name Only"
popup/item_2/id = 2

View File

@ -0,0 +1,187 @@
extends RefCounted
const non_typing_paragraph := ""
const non_typing_space := ""
const whitespace_chars := [
32, # " "
44, # ","
58, # ":"
45, # "-"
59, # ";"
40, # "("
41, # ")"
46, # "."
182, # "¶" Linefeed
10, # "\n" Actual Linefeed
967, # "●" Whitespace
]
static func is_character_whitespace(text : String, idx : int) -> bool:
if idx <= 0: return true # Stop at the edges.
if idx >= text.length(): return true
return text.unicode_at(idx) in whitespace_chars
static func show_non_typing(text : String) -> String:
text = text\
.replace(non_typing_paragraph, "\n")\
.replace(non_typing_space, " ")
if text.ends_with("\n"):
text = text.left(text.length() - 1) + non_typing_paragraph
elif text.ends_with(" "):
text = text.left(text.length() - 1) + non_typing_space
return text
static func revert_non_typing(text : String) -> String:
if text.ends_with(non_typing_paragraph):
text = text.left(text.length() - 1) + "\n"
elif text.ends_with(non_typing_space):
text = text.left(text.length() - 1) + " "
return text
static func get_caret_movement_from_key(keycode : int) -> int:
match keycode:
KEY_LEFT:
return -1
KEY_RIGHT:
return +1
KEY_HOME:
return -2
KEY_END:
return +2
return 0
static func multi_move_caret(offset : int, edited_cells_text : Array, edit_caret_positions : Array, whole_word : bool) -> bool:
if offset == -1:
for i in edit_caret_positions.size():
edit_caret_positions[i] = _step_cursor(edited_cells_text[i], edit_caret_positions[i], -1, whole_word)
elif offset == +1:
for i in edit_caret_positions.size():
edit_caret_positions[i] = _step_cursor(edited_cells_text[i], edit_caret_positions[i], +1, whole_word)
elif offset < -1:
for i in edit_caret_positions.size():
edit_caret_positions[i] = 0
elif offset > +1:
for i in edit_caret_positions.size():
edit_caret_positions[i] = edited_cells_text[i].length()
return offset != 0
static func multi_erase_right(values : Array, cursor_positions : Array, whole_word : bool):
for i in values.size():
var start_pos : int = cursor_positions[i]
cursor_positions[i] = _step_cursor(values[i], cursor_positions[i], 1, whole_word)
cursor_positions[i] = min(
cursor_positions[i],
values[i].length()
)
values[i] = (
values[i].left(start_pos)
+ values[i].substr(cursor_positions[i])
)
cursor_positions[i] = start_pos
return values
static func multi_erase_left(values : Array, cursor_positions : Array, whole_word : bool):
for i in values.size():
var start_pos : int = cursor_positions[i]
cursor_positions[i] = _step_cursor(values[i], cursor_positions[i], -1, whole_word)
values[i] = (
values[i].substr(0, cursor_positions[i])
+ values[i].substr(start_pos)
)
return values
static func multi_paste(values : Array, cursor_positions : Array):
var pasted_lines := DisplayServer.clipboard_get().replace("\r", "").split("\n")
var paste_each_line := pasted_lines.size() == values.size()
for i in values.size():
if paste_each_line:
cursor_positions[i] += pasted_lines[i].length()
else:
cursor_positions[i] += DisplayServer.clipboard_get().length()
values[i] = (
values[i].left(cursor_positions[i])
+ (pasted_lines[i] if paste_each_line else DisplayServer.clipboard_get())
+ values[i].substr(cursor_positions[i])
)
return values
static func multi_copy(values : Array):
DisplayServer.clipboard_set("\n".join(values))
static func multi_input(input_char : String, values : Array, cursor_positions : Array):
for i in values.size():
values[i] = (
values[i].left(cursor_positions[i])
+ input_char
+ values[i].substr(cursor_positions[i])
)
cursor_positions[i] = min(cursor_positions[i] + 1, values[i].length())
return values
static func get_caret_rect(cell_text : String, caret_position : int, font : Font, font_size : int, label_padding_left : float, caret_width : float = 2.0) -> Rect2:
var char_size := Vector2(0, font.get_ascent(font_size))
var result_pos := Vector2(label_padding_left, 0)
for j in max(caret_position, 0) + 1:
if j == 0: continue
if cell_text.unicode_at(j - 1) == 10:
# If "\n" found, next line.
result_pos.x = label_padding_left
result_pos.y += font.get_ascent(font_size)
continue
char_size = font.get_char_size(cell_text.unicode_at(j - 1), font_size)
result_pos.x += char_size.x
return Rect2(result_pos, Vector2(2, char_size.y))
static func _step_cursor(text : String, start : int, step : int = 1, whole_word : bool = false) -> int:
var cur := start
if whole_word and is_character_whitespace(text, cur + step):
cur += step
while true:
cur += step
if !whole_word or is_character_whitespace(text, cur):
if cur > text.length():
return text.length()
if cur <= 0:
return 0
if whole_word and step < 0:
return cur + 1
return cur
return 0

View File

@ -0,0 +1,36 @@
[gd_scene format=3 uid="uid://ydrs54md3knl"]
[node name="Label" type="MarginContainer"]
offset_right = 16.0
offset_bottom = 16.0
size_flags_vertical = 9
mouse_filter = 0
[node name="Box" type="HFlowContainer" parent="."]
layout_mode = 2
mouse_filter = 2
[node name="Back" type="Control" parent="."]
show_behind_parent = true
layout_mode = 2
mouse_filter = 2
[node name="ColorRect" type="ColorRect" parent="Back"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = 1.0
offset_top = 1.0
offset_right = -1.0
offset_bottom = -1.0
grow_horizontal = 2
grow_vertical = 2
mouse_filter = 2
color = Color(0.247059, 0.247059, 0.247059, 0.498039)
[node name="Selected" type="ColorRect" parent="."]
visible = false
layout_mode = 2
mouse_filter = 2
color = Color(1, 1, 1, 0.247059)

View File

@ -0,0 +1,33 @@
[gd_scene format=3 uid="uid://cghfjg6qt3rb1"]
[node name="Label" type="Label"]
offset_right = 20.0
offset_bottom = 23.0
size_flags_vertical = 9
mouse_filter = 0
[node name="Back" type="ColorRect" parent="."]
show_behind_parent = true
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = 1.0
offset_top = 1.0
offset_right = -1.0
offset_bottom = -1.0
grow_horizontal = 2
grow_vertical = 2
mouse_filter = 2
color = Color(0.247059, 0.247059, 0.247059, 0.498039)
[node name="Selected" type="ColorRect" parent="."]
visible = false
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
mouse_filter = 2
color = Color(1, 1, 1, 0.247059)

View File

@ -0,0 +1,42 @@
class_name ResourceTablesCellEditor
extends RefCounted
const TextEditingUtilsClass := preload("res://addons/resources_spreadsheet_view/text_editing_utils.gd")
const CELL_SCENE_DIR := "res://addons/resources_spreadsheet_view/typed_cells/"
var hint_strings_array := []
## Override to define where the cell should be shown.
func can_edit_value(value, type, property_hint, column_index) -> bool:
return value != null
## Override to change how the cell is created; preload a scene or create nodes from code.
## Caller is an instance of [code]editor_view.tscn[/code].
func create_cell(caller : Control) -> Control:
return load(CELL_SCENE_DIR + "basic.tscn").instantiate()
## Override to change behaviour when the cell is clicked to be selected.
func set_selected(node : Control, selected : bool):
node.get_node("Selected").visible = selected
## Override to change how the value is displayed.
func set_value(node : Control, value):
node.text = TextEditingUtilsClass.show_non_typing(str(value))
## Override to prevent the cell from being edited as text.
func is_text():
return true
## Override to define custom behaviour for converting the value into text for editing and copy/paste.
func to_text(value) -> String:
return var_to_str(value)
## Override to define custom behaviour for converting the value from text for editing and copy/paste.
func from_text(text : String):
return str_to_var(text)
## Override to change behaviour when there are color cells to the left of this cell.
func set_color(node : Control, color : Color):
node.get_node("Back").modulate = color * 1.0

View File

@ -0,0 +1,61 @@
extends ResourceTablesCellEditor
const TablesPluginSettingsClass := preload("res://addons/resources_spreadsheet_view/settings_grid.gd")
func can_edit_value(value, type, property_hint, column_index) -> bool:
return type == TYPE_PACKED_STRING_ARRAY or type == TYPE_ARRAY
func create_cell(caller : Control) -> Control:
return load(CELL_SCENE_DIR + "array.tscn").instantiate()
func set_value(node : Control, value):
var children := node.get_node("Box").get_children()
node.custom_minimum_size.x = ProjectSettings.get_setting(TablesPluginSettingsClass.PREFIX + "array_min_width")
var color_tint : float = 0.01 * ProjectSettings.get_setting(TablesPluginSettingsClass.PREFIX + "array_color_tint", 100.0)
var cell_label_mode : int = ProjectSettings.get_setting(TablesPluginSettingsClass.PREFIX + "resource_cell_label_mode", 0)
while children.size() < value.size():
children.append(Label.new())
node.get_node("Box").add_child(children[children.size() - 1])
var column_hints = hint_strings_array[node.get_index() % hint_strings_array.size()]
for i in children.size():
if i >= value.size():
children[i].visible = false
else:
children[i].visible = true
_write_value_to_child(value[i], value[i], column_hints, children[i], color_tint, cell_label_mode)
func _write_value_to_child(value, key, hint_arr : PackedStringArray, child : Label, color_tint : float, cell_label_mode : int):
if value is Resource:
value = _resource_to_string(value, cell_label_mode)
child.text = str(value)
child.self_modulate = (
Color.WHITE * (1.0 - color_tint)
+
(Color(str(key).hash()) + Color(0.2, 0.2, 0.2, 1.0)) * color_tint
)
static func _resource_to_string(res : Resource, cell_label_mode : int):
var prefix := ""
if cell_label_mode != 2:
if res.has_method(&"_to_string"):
prefix = res._to_string() + "\n"
elif res.has_method(&"ToString"):
prefix = res.ToString() + "\n"
if cell_label_mode == 1 && !prefix.is_empty():
return prefix.trim_suffix("\n")
return prefix + (res.resource_name if res.resource_name != "" else "[%s]" % res.resource_path.get_file())
func is_text():
return false

View File

@ -0,0 +1,30 @@
extends ResourceTablesCellEditor
func can_edit_value(value, type, property_hint, column_index) -> bool:
return type == TYPE_BOOL
func set_value(node : Control, value):
if value is bool:
_set_value_internal(node, value)
else:
_set_value_internal(node, node.text.begins_with("O"))
func _set_value_internal(node, value):
node.text = "ON" if value else "off"
node.self_modulate.a = 1.0 if value else 0.2
func to_text(value) -> String:
return "ON" if value else "off"
func from_text(text : String):
if text.begins_with("O"):
return text == "ON"
else:
return text != "off"

View File

@ -0,0 +1,43 @@
extends ResourceTablesCellEditor
var _cached_color := Color.WHITE
func create_cell(caller : Control) -> Control:
var node : Label = load(CELL_SCENE_DIR + "basic.tscn").instantiate()
var color := ColorRect.new()
node.horizontal_alignment = HORIZONTAL_ALIGNMENT_RIGHT
node.custom_minimum_size.x = 56
node.add_child(color)
color.name = "Color"
_resize_color_rect.call_deferred(color)
return node
func _resize_color_rect(rect):
if !is_instance_valid(rect): return # Table refreshed twice, probably? Either way, this fix is easier
rect.size = Vector2(8, 0)
rect.set_anchors_and_offsets_preset(Control.PRESET_LEFT_WIDE, Control.PRESET_MODE_KEEP_WIDTH)
func can_edit_value(value, type, property_hint, property_hint_string) -> bool:
return type == TYPE_COLOR
func set_value(node : Control, value):
if value is String:
node.text = TextEditingUtilsClass.show_non_typing(str(value))
else:
node.text = value.to_html(true)
_cached_color = value
node.get_node("Color").color = value
func to_text(value) -> String:
return value.to_html()
func from_text(text : String):
return Color.from_string(text, Color.BLACK)

View File

@ -0,0 +1,46 @@
extends "res://addons/resources_spreadsheet_view/typed_cells/cell_editor_array.gd"
func can_edit_value(value, type, property_hint, column_index) -> bool:
return type == TYPE_DICTIONARY
func create_cell(caller : Control) -> Control:
return load(CELL_SCENE_DIR + "array.tscn").instantiate()
func set_value(node : Control, value):
var children := node.get_node("Box").get_children()
node.custom_minimum_size.x = ProjectSettings.get_setting(TablesPluginSettingsClass.PREFIX + "array_min_width")
var color_tint : float = 0.01 * ProjectSettings.get_setting(TablesPluginSettingsClass.PREFIX + "array_color_tint", 100.0)
var cell_label_mode : int = ProjectSettings.get_setting(TablesPluginSettingsClass.PREFIX + "resource_cell_label_mode", 0)
while children.size() < value.size():
children.append(Label.new())
node.get_node("Box").add_child(children[children.size() - 1])
var column_hints : PackedStringArray = hint_strings_array[node.get_index() % hint_strings_array.size()]
var values : Array = value.values()
var keys : Array = value.keys()
for i in children.size():
if i >= values.size():
children[i].visible = false
else:
children[i].visible = true
if values[i] is Resource: values[i] = _resource_to_string(values[i], cell_label_mode)
if keys[i] is Resource: keys[i] = _resource_to_string(keys[i], cell_label_mode)
_write_value_to_child("%s%s" % [keys[i], values[i]], keys[i], column_hints, children[i], color_tint, cell_label_mode)
func is_text():
return false
func to_text(value) -> String:
return var_to_str(value).replace("\n", " ")
func from_text(text : String):
return str_to_var(text)

View File

@ -0,0 +1,38 @@
extends ResourceTablesCellEditor
func can_edit_value(value, type, property_hint, column_index) -> bool:
return type == TYPE_INT and property_hint == PROPERTY_HINT_ENUM
func set_value(node : Control, value):
if value == null:
# Sometimes, when creating new property, becomes null
value = 0
var value_str : String
var key_found := -1
var hint_arr : Array = hint_strings_array[node.get_index() % hint_strings_array.size()]
for i in hint_arr.size():
var colon_found : int = hint_arr[i].rfind(":")
if colon_found == -1:
key_found = value
break
if hint_arr[i].substr(colon_found + 1).to_int() == value:
key_found = i
break
if key_found != -1:
value_str = hint_arr[key_found]
else:
value_str = "?:%s" % value
node.text = value_str
node.self_modulate = Color(node.text.hash()) + Color(0.25, 0.25, 0.25, 1.0)
node.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
func is_text():
return false

View File

@ -0,0 +1,39 @@
extends "res://addons/resources_spreadsheet_view/typed_cells/cell_editor_array.gd"
func can_edit_value(value, type, property_hint, column_index) -> bool:
if (
type != TYPE_PACKED_INT32_ARRAY
and type != TYPE_PACKED_INT64_ARRAY
and type != TYPE_ARRAY
) or property_hint != PROPERTY_HINT_TYPE_STRING:
return false
return hint_strings_array[column_index][0].begins_with("2/2:")
func _write_value_to_child(value, key, hint_arr : PackedStringArray, child : Label, color_tint : float, cell_label_mode : int):
var value_str : String
var key_found := -1
for i in hint_arr.size():
var colon_found := hint_arr[i].rfind(":")
if colon_found == -1:
key_found = value
break
if hint_arr[i].substr(colon_found + 1).to_int() == value:
key_found = i
break
if key_found == 0:
# Enum array hints have "2/3:" before list.
var found := hint_arr[0].find(":") + 1
value_str = hint_arr[0].substr(hint_arr[0].find(":") + 1)
elif key_found != -1:
value_str = hint_arr[key_found]
else:
value_str = "?:%s" % value
super(value_str, value_str, hint_arr, child, color_tint, cell_label_mode)

View File

@ -0,0 +1,13 @@
extends ResourceTablesCellEditor
func can_edit_value(value, type, property_hint, column_index) -> bool:
return type == TYPE_FLOAT or type == TYPE_INT
func to_text(value) -> String:
return str(value)
func from_text(text : String):
return text.to_float()

View File

@ -0,0 +1,73 @@
extends ResourceTablesCellEditor
const TablesPluginSettingsClass := preload("res://addons/resources_spreadsheet_view/settings_grid.gd")
var previewer : EditorResourcePreview
func can_edit_value(value, type, property_hint, column_index) -> bool:
return type == TYPE_OBJECT
func create_cell(caller : Control) -> Control:
if previewer == null:
previewer = caller.editor_plugin.get_editor_interface().get_resource_previewer()
var node = load(CELL_SCENE_DIR + "resource.tscn").instantiate()
return node
func set_value(node : Control, value):
var preview_node := node.get_node("Box/Tex")
var label_node := node.get_node("Box/Label")
if value == null:
preview_node.visible = false
label_node.text = "[empty]"
node.editor_description = ""
if !value is Resource: return
node.editor_description = value.resource_path
label_node.text = _resource_to_string(value, ProjectSettings.get_setting(TablesPluginSettingsClass.PREFIX + "resource_cell_label_mode", 0))
if value is Texture:
preview_node.visible = true
preview_node.texture = value
else:
preview_node.visible = false
previewer.queue_resource_preview(value.resource_path, self, &"_on_preview_loaded", node)
preview_node.custom_minimum_size = Vector2.ONE * ProjectSettings.get_setting(
TablesPluginSettingsClass.PREFIX + "resource_preview_size"
)
func set_color(node : Control, color : Color):
node.get_node("Back").modulate = color * 0.6 if node.editor_description == "" else color
func is_text():
return false
func _on_preview_loaded(path : String, preview : Texture, thumbnail_preview : Texture, node):
# Abort if the node has been deleted since.
if is_instance_valid(node):
node.get_node("Box/Tex").visible = true
node.get_node("Box/Tex").texture = preview
static func _resource_to_string(res : Resource, cell_label_mode : int):
var prefix := ""
if cell_label_mode != 2:
if res.has_method(&"_to_string"):
prefix = res._to_string() + "\n"
elif res.has_method(&"ToString"):
prefix = res.ToString() + "\n"
if cell_label_mode == 1 && !prefix.is_empty():
return prefix.trim_suffix("\n")
return prefix + (res.resource_name if res.resource_name != "" else "[%s]" % res.resource_path.get_file())

View File

@ -0,0 +1,13 @@
extends ResourceTablesCellEditor
func to_text(value) -> String:
return str(value)
func from_text(text : String):
return text
func set_color(node : Control, color : Color):
node.get_node("Back").modulate = color * 0.6

View File

@ -0,0 +1,45 @@
[gd_scene format=3 uid="uid://clcndgxaty503"]
[node name="Label" type="MarginContainer"]
size_flags_vertical = 9
mouse_filter = 0
[node name="Back" type="Control" parent="."]
show_behind_parent = true
layout_mode = 2
mouse_filter = 2
[node name="ColorRect" type="ColorRect" parent="Back"]
layout_mode = 1
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
offset_left = 1.0
offset_top = 1.0
offset_right = -2.0
offset_bottom = -1.0
grow_horizontal = 2
grow_vertical = 2
mouse_filter = 2
color = Color(0.247059, 0.247059, 0.247059, 0.498039)
[node name="Selected" type="ColorRect" parent="."]
visible = false
layout_mode = 2
mouse_filter = 2
color = Color(1, 1, 1, 0.247059)
[node name="Box" type="HBoxContainer" parent="."]
layout_mode = 2
mouse_filter = 2
[node name="Tex" type="TextureRect" parent="Box"]
layout_mode = 2
mouse_filter = 2
expand_mode = 1
stretch_mode = 5
[node name="Label" type="Label" parent="Box"]
layout_mode = 2
size_flags_horizontal = 3
text = "res.tres"

View File

@ -0,0 +1,252 @@
@tool
extends ResourceTablesDockEditor
@onready var recent_container := $"HBoxContainer/Control2/HBoxContainer/HFlowContainer"
@onready var contents_label := $"HBoxContainer/HBoxContainer/Panel/Label"
@onready var button_box := $"HBoxContainer/HBoxContainer/Control/VBoxContainer/HBoxContainer"
@onready var value_input := $"HBoxContainer/HBoxContainer/Control/VBoxContainer/LineEdit"
var _stored_value
var _stored_type := 0
func _ready():
super()
contents_label.text_changed.connect(_on_contents_edit_text_changed)
func try_edit_value(value, type, property_hint) -> bool:
if (
type != TYPE_ARRAY and type != TYPE_PACKED_STRING_ARRAY
and type != TYPE_PACKED_INT32_ARRAY and type != TYPE_PACKED_FLOAT32_ARRAY
and type != TYPE_PACKED_INT64_ARRAY and type != TYPE_PACKED_FLOAT64_ARRAY
):
return false
if sheet.column_hint_strings[sheet.get_selected_column()][0].begins_with("2/2:"):
# For enums, prefer the specialized dock.
return false
_stored_type = type
_stored_value = value.duplicate() # Generic arrays are passed by reference
contents_label.text = str(value)
var is_generic_array : bool = _stored_type == TYPE_ARRAY and !value.is_typed()
button_box.get_child(1).visible = (
is_generic_array or value.get_typed_builtin() == TYPE_STRING or value.get_typed_builtin() == TYPE_STRING_NAME
or _stored_type == TYPE_PACKED_STRING_ARRAY
)
button_box.get_child(2).visible = (
is_generic_array or value.get_typed_builtin() == TYPE_INT
or _stored_type == TYPE_PACKED_INT32_ARRAY or _stored_type == TYPE_PACKED_INT64_ARRAY
)
button_box.get_child(3).visible = (
is_generic_array or value.get_typed_builtin() == TYPE_FLOAT
or _stored_type == TYPE_PACKED_FLOAT32_ARRAY or _stored_type == TYPE_PACKED_FLOAT64_ARRAY
)
button_box.get_child(5).visible = (
is_generic_array or value.get_typed_builtin() == TYPE_OBJECT
)
if value.get_typed_builtin() == TYPE_OBJECT:
if !value_input is EditorResourcePicker:
var new_input : EditorResourcePicker = load("res://addons/resources_spreadsheet_view/editor_resource_array_picker.gd").new()
new_input.size_flags_horizontal = SIZE_EXPAND_FILL
new_input.base_type = value.get_typed_class_name()
new_input.on_resources_dropped.connect(func(p):
_add_values(p)
for x in p:
_add_recent(x)
)
new_input.resource_selected.connect(func(p1, p2): EditorInterface.edit_resource(p1))
value_input.replace_by(new_input)
value_input.free()
value_input = new_input
else:
if !value_input is LineEdit:
var new_input := LineEdit.new()
new_input.size_flags_horizontal = SIZE_EXPAND_FILL
value_input.replace_by(new_input)
value_input.free()
value_input = new_input
return true
func _add_value(value):
_stored_value.append(value)
var values : Array = sheet.get_edited_cells_values()
var cur_value
var dupe_array : bool = ProjectSettings.get_setting(TablesPluginSettingsClass.PREFIX + "dupe_arrays")
for i in values.size():
cur_value = values[i]
if dupe_array:
cur_value = cur_value.duplicate()
cur_value.append(value)
values[i] = cur_value
sheet.set_edited_cells_values(values)
func _add_values(added_values : Array):
_stored_value.append_array(added_values)
var values : Array = sheet.get_edited_cells_values()
var cur_value
var dupe_array : bool = ProjectSettings.get_setting(TablesPluginSettingsClass.PREFIX + "dupe_arrays")
for i in values.size():
cur_value = values[i]
if dupe_array:
cur_value = cur_value.duplicate()
cur_value.append_array(added_values)
values[i] = cur_value
sheet.set_edited_cells_values(values)
func _remove_value(value):
_stored_value.remove_at(_stored_value.find(value))
var values : Array = sheet.get_edited_cells_values()
var cur_value : Array
var dupe_array : bool = ProjectSettings.get_setting(TablesPluginSettingsClass.PREFIX + "dupe_arrays")
for i in values.size():
cur_value = values[i]
if dupe_array:
cur_value = cur_value.duplicate()
if cur_value.has(value): # erase() not defined in PoolArrays
cur_value.remove_at(cur_value.find(value))
values[i] = cur_value
sheet.set_edited_cells_values(values)
func _add_recent(value):
for x in recent_container.get_children():
if x.text == str(value):
return
if value is Resource and x.tooltip_text == value.resource_path:
return
var node := Button.new()
var value_str : String = str(value)
if value is Resource:
value_str = value.resource_path.get_file() if value.resource_name == "" else value.resource_name
node.tooltip_text = value.resource_path
node.text = value_str
node.self_modulate = Color(value_str.hash()) + Color(0.25, 0.25, 0.25, 1.0)
node.pressed.connect(_on_recent_clicked.bind(node, value))
recent_container.add_child(node)
func _on_recent_clicked(button : Button, value):
var recent_mode : int = recent_container.get_child(1).selected
if value_input is EditorResourcePicker:
value_input.edited_resource = value
else:
value_input.text = str(value)
if recent_mode == 0:
_add_value(value)
if recent_mode == 1:
_remove_value(value)
if recent_mode == 2:
button.queue_free()
func _on_Remove_pressed():
if value_input is EditorResourcePicker:
_remove_value(value_input.edited_resource)
elif str_to_var(value_input.text) != null:
_remove_value(str_to_var(value_input.text))
else:
_remove_value(value_input.text)
func _on_RemoveLast_pressed():
_stored_value.pop_back()
var values : Array = sheet.get_edited_cells_values()
var cur_value : Array
var dupe_array : bool = ProjectSettings.get_setting(TablesPluginSettingsClass.PREFIX + "dupe_arrays")
for i in values.size():
cur_value = values[i]
if dupe_array:
cur_value = cur_value.duplicate()
cur_value.pop_back()
values[i] = cur_value
sheet.set_edited_cells_values(values)
func _on_ClearRecent_pressed():
for i in recent_container.get_child_count():
if i == 0: continue
recent_container.get_child(i).free()
func _on_Float_pressed():
_add_value(value_input.text.to_float())
func _on_Int_pressed():
_add_value(value_input.text.to_int())
func _on_String_pressed():
_add_value(value_input.text)
_add_recent(value_input.text)
func _on_Variant_pressed():
if value_input is EditorResourcePicker:
_add_value(value_input.edited_resource)
else:
_add_value(str_to_var(value_input.text))
func _on_Resource_pressed():
var new_value
if value_input is LineEdit:
new_value = load(value_input.text)
elif value_input is EditorResourcePicker:
new_value = value_input.edited_resource
_add_value(new_value)
_add_recent(new_value)
func _on_AddRecentFromSel_pressed():
for x in sheet.get_edited_cells_values():
for y in x:
_add_recent(y)
func _on_contents_edit_text_changed():
var value = str_to_var(contents_label.text)
if !value is Array:
return
var values : Array = sheet.get_edited_cells_values()
for i in values.size():
values[i] = values[i].duplicate()
values[i].resize(value.size())
for j in value.size():
values[i][j] = value[j]
_stored_value = value
sheet.set_edited_cells_values(values)

View File

@ -0,0 +1,195 @@
[gd_scene load_steps=5 format=3 uid="uid://c3a2cip8ffccv"]
[ext_resource type="Script" path="res://addons/resources_spreadsheet_view/typed_editors/dock_array.gd" id="1"]
[ext_resource type="Script" path="res://addons/resources_spreadsheet_view/editor_icon_button.gd" id="2"]
[sub_resource type="Image" id="Image_3slrg"]
data = {
"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 93, 93, 55, 255, 97, 97, 58, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 97, 97, 42, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 98, 98, 47, 255, 97, 97, 42, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 93, 93, 233, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 94, 94, 46, 255, 93, 93, 236, 255, 93, 93, 233, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
"format": "RGBA8",
"height": 16,
"mipmaps": false,
"width": 16
}
[sub_resource type="ImageTexture" id="ImageTexture_3oshq"]
image = SubResource("Image_3slrg")
[node name="EditArray" type="VBoxContainer"]
anchors_preset = 10
anchor_right = 1.0
grow_horizontal = 2
mouse_filter = 0
script = ExtResource("1")
[node name="Header" type="HBoxContainer" parent="."]
layout_mode = 2
mouse_filter = 0
mouse_default_cursor_shape = 9
[node name="HBoxContainer" type="HBoxContainer" parent="Header"]
layout_mode = 2
size_flags_horizontal = 3
[node name="Label" type="Label" parent="Header/HBoxContainer"]
layout_mode = 2
text = "EDIT: Array"
[node name="HSeparator" type="HSeparator" parent="Header/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
mouse_filter = 2
[node name="Label" type="Label" parent="Header"]
layout_mode = 2
text = "PROPERTY NAME"
[node name="HSeparator2" type="HSeparator" parent="Header"]
layout_mode = 2
size_flags_horizontal = 3
mouse_filter = 2
[node name="HBoxContainer" type="HSplitContainer" parent="."]
layout_mode = 2
size_flags_vertical = 3
split_offset = 380
[node name="HBoxContainer" type="HBoxContainer" parent="HBoxContainer"]
layout_mode = 2
alignment = 2
[node name="Panel" type="MarginContainer" parent="HBoxContainer/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
[node name="Label" type="TextEdit" parent="HBoxContainer/HBoxContainer/Panel"]
layout_mode = 2
size_flags_vertical = 5
text = "[]"
[node name="VSeparator2" type="VSeparator" parent="HBoxContainer/HBoxContainer"]
layout_mode = 2
[node name="Control" type="MarginContainer" parent="HBoxContainer/HBoxContainer"]
layout_mode = 2
[node name="VBoxContainer" type="VBoxContainer" parent="HBoxContainer/HBoxContainer/Control"]
layout_mode = 2
[node name="LineEdit" type="LineEdit" parent="HBoxContainer/HBoxContainer/Control/VBoxContainer"]
layout_mode = 2
placeholder_text = "Input value to add/erase..."
clear_button_enabled = true
[node name="HBoxContainer" type="HBoxContainer" parent="HBoxContainer/HBoxContainer/Control/VBoxContainer"]
layout_mode = 2
[node name="Label" type="Label" parent="HBoxContainer/HBoxContainer/Control/VBoxContainer/HBoxContainer"]
layout_mode = 2
text = "Add:"
[node name="String" type="Button" parent="HBoxContainer/HBoxContainer/Control/VBoxContainer/HBoxContainer"]
layout_mode = 2
tooltip_text = "Add string"
icon = SubResource("ImageTexture_3oshq")
script = ExtResource("2")
icon_name = "String"
[node name="Int" type="Button" parent="HBoxContainer/HBoxContainer/Control/VBoxContainer/HBoxContainer"]
layout_mode = 2
tooltip_text = "Add integer"
icon = SubResource("ImageTexture_3oshq")
script = ExtResource("2")
icon_name = "int"
[node name="Float" type="Button" parent="HBoxContainer/HBoxContainer/Control/VBoxContainer/HBoxContainer"]
layout_mode = 2
tooltip_text = "Add float"
icon = SubResource("ImageTexture_3oshq")
script = ExtResource("2")
icon_name = "float"
[node name="Variant" type="Button" parent="HBoxContainer/HBoxContainer/Control/VBoxContainer/HBoxContainer"]
layout_mode = 2
tooltip_text = "Guess type and add"
icon = SubResource("ImageTexture_3oshq")
script = ExtResource("2")
icon_name = "Variant"
[node name="Object" type="Button" parent="HBoxContainer/HBoxContainer/Control/VBoxContainer/HBoxContainer"]
layout_mode = 2
tooltip_text = "Add resource (by path if string)"
icon = SubResource("ImageTexture_3oshq")
script = ExtResource("2")
icon_name = "Object"
[node name="Label2" type="Label" parent="HBoxContainer/HBoxContainer/Control/VBoxContainer/HBoxContainer"]
layout_mode = 2
text = "Erase:"
[node name="Remove" type="Button" parent="HBoxContainer/HBoxContainer/Control/VBoxContainer/HBoxContainer"]
layout_mode = 2
tooltip_text = "Find and erase value in textbox"
icon = SubResource("ImageTexture_3oshq")
script = ExtResource("2")
icon_name = "Remove"
[node name="Remove2" type="Button" parent="HBoxContainer/HBoxContainer/Control/VBoxContainer/HBoxContainer"]
layout_mode = 2
tooltip_text = "Remove last value"
icon = SubResource("ImageTexture_3oshq")
script = ExtResource("2")
icon_name = "MoveLeft"
[node name="VSeparator" type="VSeparator" parent="HBoxContainer/HBoxContainer"]
layout_mode = 2
[node name="Control2" type="ScrollContainer" parent="HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
follow_focus = true
horizontal_scroll_mode = 0
[node name="HBoxContainer" type="HBoxContainer" parent="HBoxContainer/Control2"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
alignment = 2
[node name="VSeparator2" type="VSeparator" parent="HBoxContainer/Control2/HBoxContainer"]
layout_mode = 2
[node name="HFlowContainer" type="HFlowContainer" parent="HBoxContainer/Control2/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
[node name="Label" type="Label" parent="HBoxContainer/Control2/HBoxContainer/HFlowContainer"]
layout_mode = 2
text = "Recent:"
[node name="OptionButton" type="OptionButton" parent="HBoxContainer/Control2/HBoxContainer/HFlowContainer"]
layout_mode = 2
selected = 0
fit_to_longest_item = false
item_count = 3
popup/item_0/text = "Add"
popup/item_1/text = "Erase"
popup/item_1/id = 1
popup/item_2/text = "Remove From Recent"
popup/item_2/id = 2
[node name="AddRecentFromSel" type="Button" parent="HBoxContainer/Control2/HBoxContainer/HFlowContainer"]
layout_mode = 2
tooltip_text = "Add from selected cells"
icon = SubResource("ImageTexture_3oshq")
script = ExtResource("2")
icon_name = "ListSelect"
[connection signal="pressed" from="HBoxContainer/HBoxContainer/Control/VBoxContainer/HBoxContainer/String" to="." method="_on_String_pressed"]
[connection signal="pressed" from="HBoxContainer/HBoxContainer/Control/VBoxContainer/HBoxContainer/Int" to="." method="_on_Int_pressed"]
[connection signal="pressed" from="HBoxContainer/HBoxContainer/Control/VBoxContainer/HBoxContainer/Float" to="." method="_on_Float_pressed"]
[connection signal="pressed" from="HBoxContainer/HBoxContainer/Control/VBoxContainer/HBoxContainer/Variant" to="." method="_on_Variant_pressed"]
[connection signal="pressed" from="HBoxContainer/HBoxContainer/Control/VBoxContainer/HBoxContainer/Object" to="." method="_on_Resource_pressed"]
[connection signal="pressed" from="HBoxContainer/HBoxContainer/Control/VBoxContainer/HBoxContainer/Remove" to="." method="_on_Remove_pressed"]
[connection signal="pressed" from="HBoxContainer/HBoxContainer/Control/VBoxContainer/HBoxContainer/Remove2" to="." method="_on_RemoveLast_pressed"]
[connection signal="pressed" from="HBoxContainer/Control2/HBoxContainer/HFlowContainer/AddRecentFromSel" to="." method="_on_AddRecentFromSel_pressed"]

View File

@ -0,0 +1,50 @@
@tool
class_name ResourceTablesDockEditor
extends Control
const TablesPluginSettingsClass := preload("res://addons/resources_spreadsheet_view/settings_grid.gd")
@export var path_property_name := NodePath("Header/Label")
var sheet : Control
var selection : Array
var _resize_target_height := 0.0
func _ready():
var parent := get_parent()
while parent != null and !parent.has_method(&"display_folder"):
parent = parent.get_parent()
sheet = parent
get_node(path_property_name).add_theme_font_override(&"normal", get_theme_font(&"bold", &"EditorFonts"))
$"Header".gui_input.connect(_on_header_gui_input)
$"Header".mouse_filter = MOUSE_FILTER_STOP
$"Header".mouse_default_cursor_shape = CURSOR_VSIZE
## Override to define when to show the dock and, if it can edit the value, how to handle it.
func try_edit_value(value, type : int, property_hint : String) -> bool:
return true
## Override to define behaviour when stretching the header to change size.
func resize_drag(to_height : float):
return
func resize_set_hidden(state : bool):
get_child(1).visible = !state
func _on_header_gui_input(event : InputEvent):
if event is InputEventMouseMotion:
var pressed := Input.is_mouse_button_pressed(MOUSE_BUTTON_LEFT)
if pressed:
_resize_target_height -= event.relative.y
custom_minimum_size.y = clamp(_resize_target_height, 0.0, get_viewport().size.y * 0.75)
resize_drag(_resize_target_height)
resize_set_hidden(_resize_target_height <= $"Header".size.y)
if event is InputEventMouseButton:
_resize_target_height = custom_minimum_size.y

View File

@ -0,0 +1,139 @@
@tool
extends ResourceTablesDockEditor
@onready var _value_rect := $"EditColor/ColorProper/ColorRect"
@onready var _color_picker_panel := $"EditColor/VSeparator6/Panel"
@onready var _color_picker := $"EditColor/VSeparator6/Panel/MarginContainer/ColorPicker"
@onready var _custom_value_edit := $"EditColor/CustomX/Box/LineEdit"
var _stored_value := Color.WHITE
var _resize_height_small := 0.0
var _resize_expanded := true
func _ready():
super._ready()
_connect_buttons($"EditColor/RGBGrid", 0, 0)
_connect_buttons($"EditColor/RGBGrid", 5, 1)
_connect_buttons($"EditColor/RGBGrid", 10, 2)
_connect_buttons($"EditColor/HSVGrid", 0, 3)
_connect_buttons($"EditColor/HSVGrid", 5, 4)
_connect_buttons($"EditColor/HSVGrid", 10, 5)
_resize_height_small = get_child(1).get_minimum_size().y
func _connect_buttons(grid, start_index, property_bind):
grid.get_child(start_index + 0).pressed.connect(_increment_values_custom.bind(-1.0, property_bind))
grid.get_child(start_index + 1).pressed.connect(_increment_values.bind(-10.0, property_bind))
grid.get_child(start_index + 3).pressed.connect(_increment_values.bind(10.0, property_bind))
grid.get_child(start_index + 4).pressed.connect(_increment_values_custom.bind(1.0, property_bind))
func try_edit_value(value, type, property_hint) -> bool:
_color_picker_panel.top_level = false
if type != TYPE_COLOR:
return false
_set_stored_value(value)
_color_picker_panel.visible = false
return true
func resize_drag(to_height : float):
var expanded := to_height > _resize_height_small
if _resize_expanded == expanded:
return
_resize_expanded = expanded
$"EditColor/RGBGrid".visible = expanded
$"EditColor/ColorProper".visible = expanded
$"EditColor/HSVGrid".columns = 5 if expanded else 15
$"EditColor/CustomX/Label".visible = expanded
func _set_stored_value(v):
_stored_value = v
_color_picker.color = v
_value_rect.color = v
func _increment_values(by : float, property : int):
var cell_values : Array = sheet.get_edited_cells_values()
match property:
0:
_stored_value.r += by / 255.0
for i in cell_values.size():
cell_values[i].r += by / 255.0
1:
_stored_value.g += by / 255.0
for i in cell_values.size():
cell_values[i].g += by / 255.0
2:
_stored_value.b += by / 255.0
for i in cell_values.size():
cell_values[i].b += by / 255.0
3:
# Hue has 360 degrees and loops
_stored_value.h += by / 360.0
for i in cell_values.size():
cell_values[i].h = fposmod(cell_values[i].h + by / 360.0, 1.0)
4:
_stored_value.s += by * 0.005
for i in cell_values.size():
cell_values[i].s += by * 0.005
5:
_stored_value.v += by * 0.005
for i in cell_values.size():
cell_values[i].v += by * 0.005
_set_stored_value(_stored_value)
sheet.set_edited_cells_values(cell_values)
func _increment_values_custom(multiplier : float, property : int):
if property == 4 or property == 5:
# Numbered buttons increment by 5 for Sat and Value, so hue is x0.5 effect. Negate it here
multiplier *= 2.0
_increment_values(_custom_value_edit.text.to_float() * multiplier, property)
func _on_Button_pressed():
_color_picker_panel.visible = !_color_picker_panel.visible
if _color_picker_panel.visible:
_color_picker_panel.top_level = true
_color_picker_panel.global_position = (
sheet.global_position
+ Vector2(0, sheet.size.y - _color_picker_panel.size.y)
+ Vector2(16, -16)
)
_color_picker_panel.global_position.y = clamp(
_color_picker_panel.global_position.y,
0,
sheet.editor_plugin.get_editor_interface().get_base_control().size.y
)
_color_picker.color = _stored_value
elif _color_picker.color != _stored_value:
_set_stored_value(_color_picker.color)
update_cell_values()
func _on_ColorPicker_gui_input(event : InputEvent):
if event is InputEventMouseButton and !event.pressed:
_set_stored_value(_color_picker.color)
update_cell_values()
func update_cell_values():
var values = sheet.get_edited_cells_values()
for i in values.size():
values[i] = _stored_value
sheet.set_edited_cells_values(values)

View File

@ -0,0 +1,295 @@
[gd_scene load_steps=2 format=3 uid="uid://b3a3bo6cfyh5t"]
[ext_resource type="Script" path="res://addons/resources_spreadsheet_view/typed_editors/dock_color.gd" id="1"]
[node name="EditColor" type="VBoxContainer"]
offset_bottom = 131.0
grow_horizontal = 2
mouse_filter = 0
script = ExtResource("1")
[node name="Header" type="HBoxContainer" parent="."]
layout_mode = 2
mouse_filter = 0
mouse_default_cursor_shape = 9
[node name="HBoxContainer" type="HBoxContainer" parent="Header"]
layout_mode = 2
size_flags_horizontal = 3
[node name="Label" type="Label" parent="Header/HBoxContainer"]
layout_mode = 2
text = "EDIT: Color"
[node name="HSeparator" type="HSeparator" parent="Header/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
mouse_filter = 2
[node name="Label" type="Label" parent="Header"]
layout_mode = 2
text = "PROPERTY NAME"
[node name="HSeparator2" type="HSeparator" parent="Header"]
layout_mode = 2
size_flags_horizontal = 3
mouse_filter = 2
[node name="EditColor" type="HBoxContainer" parent="."]
layout_mode = 2
size_flags_vertical = 3
alignment = 1
[node name="VSeparator7" type="Control" parent="EditColor"]
layout_mode = 2
size_flags_horizontal = 3
[node name="VSeparator3" type="Control" parent="EditColor"]
visible = false
layout_mode = 2
[node name="ButtonRowTemplate" type="Control" parent="EditColor"]
visible = false
layout_mode = 2
[node name="Button3" type="Button" parent="EditColor/ButtonRowTemplate"]
layout_mode = 0
text = "-X"
[node name="Button2" type="Button" parent="EditColor/ButtonRowTemplate"]
layout_mode = 0
text = "-5"
[node name="Label" type="Label" parent="EditColor/ButtonRowTemplate"]
layout_mode = 0
text = "Red"
[node name="Button5" type="Button" parent="EditColor/ButtonRowTemplate"]
layout_mode = 0
text = "+5"
[node name="Button6" type="Button" parent="EditColor/ButtonRowTemplate"]
layout_mode = 0
text = "+X"
[node name="RGBGrid" type="GridContainer" parent="EditColor"]
layout_mode = 2
columns = 5
[node name="Button3" type="Button" parent="EditColor/RGBGrid"]
modulate = Color(1, 0.780392, 0.780392, 1)
layout_mode = 2
text = "-X"
[node name="Button2" type="Button" parent="EditColor/RGBGrid"]
modulate = Color(1, 0.780392, 0.780392, 1)
layout_mode = 2
text = "-10"
[node name="Label" type="Label" parent="EditColor/RGBGrid"]
layout_mode = 2
text = "Red"
horizontal_alignment = 1
[node name="Button5" type="Button" parent="EditColor/RGBGrid"]
modulate = Color(1, 0.780392, 0.780392, 1)
layout_mode = 2
text = "+10"
[node name="Button6" type="Button" parent="EditColor/RGBGrid"]
modulate = Color(1, 0.780392, 0.780392, 1)
layout_mode = 2
text = "+X"
[node name="Button7" type="Button" parent="EditColor/RGBGrid"]
modulate = Color(0.666667, 1, 0.745098, 1)
layout_mode = 2
text = "-X"
[node name="Button8" type="Button" parent="EditColor/RGBGrid"]
modulate = Color(0.666667, 1, 0.745098, 1)
layout_mode = 2
text = "-10"
[node name="Label2" type="Label" parent="EditColor/RGBGrid"]
layout_mode = 2
text = "Green"
horizontal_alignment = 1
[node name="Button11" type="Button" parent="EditColor/RGBGrid"]
modulate = Color(0.666667, 1, 0.745098, 1)
layout_mode = 2
text = "+10"
[node name="Button12" type="Button" parent="EditColor/RGBGrid"]
modulate = Color(0.666667, 1, 0.745098, 1)
layout_mode = 2
text = "+X"
[node name="Button13" type="Button" parent="EditColor/RGBGrid"]
modulate = Color(0.772549, 0.792157, 1, 1)
layout_mode = 2
text = "-X"
[node name="Button14" type="Button" parent="EditColor/RGBGrid"]
modulate = Color(0.772549, 0.792157, 1, 1)
layout_mode = 2
text = "-10"
[node name="Label3" type="Label" parent="EditColor/RGBGrid"]
layout_mode = 2
text = "Blue"
horizontal_alignment = 1
[node name="Button17" type="Button" parent="EditColor/RGBGrid"]
modulate = Color(0.772549, 0.792157, 1, 1)
layout_mode = 2
text = "+10"
[node name="Button18" type="Button" parent="EditColor/RGBGrid"]
modulate = Color(0.772549, 0.792157, 1, 1)
layout_mode = 2
text = "+X"
[node name="VSeparator" type="VSeparator" parent="EditColor"]
layout_mode = 2
[node name="ColorProper" type="VBoxContainer" parent="EditColor"]
layout_mode = 2
[node name="ColorRect" type="ColorRect" parent="EditColor/ColorProper"]
layout_mode = 2
size_flags_vertical = 3
[node name="Button" type="Button" parent="EditColor/ColorProper"]
layout_mode = 2
text = "Choose Color"
[node name="VSeparator2" type="VSeparator" parent="EditColor"]
layout_mode = 2
[node name="HSVGrid" type="GridContainer" parent="EditColor"]
layout_mode = 2
columns = 5
[node name="Button3" type="Button" parent="EditColor/HSVGrid"]
modulate = Color(1, 0.913725, 0.776471, 1)
layout_mode = 2
text = "-X"
[node name="Button2" type="Button" parent="EditColor/HSVGrid"]
modulate = Color(0.898039, 1, 0.698039, 1)
layout_mode = 2
text = "-10"
[node name="Label" type="Label" parent="EditColor/HSVGrid"]
layout_mode = 2
text = "Hue"
horizontal_alignment = 1
[node name="Button5" type="Button" parent="EditColor/HSVGrid"]
modulate = Color(0.717647, 1, 0.980392, 1)
layout_mode = 2
text = "+10"
[node name="Button6" type="Button" parent="EditColor/HSVGrid"]
modulate = Color(0.74902, 0.729412, 1, 1)
layout_mode = 2
text = "+X"
[node name="Button7" type="Button" parent="EditColor/HSVGrid"]
layout_mode = 2
text = "-X"
[node name="Button8" type="Button" parent="EditColor/HSVGrid"]
layout_mode = 2
text = "-5"
[node name="Label2" type="Label" parent="EditColor/HSVGrid"]
layout_mode = 2
text = "Sat"
horizontal_alignment = 1
[node name="Button11" type="Button" parent="EditColor/HSVGrid"]
layout_mode = 2
text = "+5"
[node name="Button12" type="Button" parent="EditColor/HSVGrid"]
layout_mode = 2
text = "+X"
[node name="Button13" type="Button" parent="EditColor/HSVGrid"]
layout_mode = 2
text = "-X"
[node name="Button14" type="Button" parent="EditColor/HSVGrid"]
layout_mode = 2
text = "-5"
[node name="Label3" type="Label" parent="EditColor/HSVGrid"]
layout_mode = 2
text = "Value"
horizontal_alignment = 1
[node name="Button17" type="Button" parent="EditColor/HSVGrid"]
layout_mode = 2
text = "+5"
[node name="Button18" type="Button" parent="EditColor/HSVGrid"]
layout_mode = 2
text = "+X"
[node name="VSeparator4" type="VSeparator" parent="EditColor"]
layout_mode = 2
[node name="CustomX" type="VBoxContainer" parent="EditColor"]
layout_mode = 2
[node name="Label" type="Label" parent="EditColor/CustomX"]
layout_mode = 2
size_flags_vertical = 6
text = "Custom Value"
horizontal_alignment = 1
[node name="Box" type="HBoxContainer" parent="EditColor/CustomX"]
layout_mode = 2
[node name="Label2" type="Label" parent="EditColor/CustomX/Box"]
layout_mode = 2
size_flags_vertical = 6
text = "X ="
[node name="LineEdit" type="LineEdit" parent="EditColor/CustomX/Box"]
layout_mode = 2
size_flags_horizontal = 3
text = "20"
[node name="VSeparator6" type="Control" parent="EditColor"]
layout_mode = 2
size_flags_horizontal = 3
[node name="Panel" type="PanelContainer" parent="EditColor/VSeparator6"]
visible = false
layout_mode = 1
anchors_preset = 2
anchor_top = 1.0
anchor_bottom = 1.0
grow_vertical = 0
[node name="Panel2" type="Panel" parent="EditColor/VSeparator6/Panel"]
self_modulate = Color(1.5, 1.5, 1.5, 1)
layout_mode = 2
[node name="MarginContainer" type="MarginContainer" parent="EditColor/VSeparator6/Panel"]
layout_mode = 2
[node name="ColorPicker" type="ColorPicker" parent="EditColor/VSeparator6/Panel/MarginContainer"]
layout_mode = 2
[node name="Button" type="Button" parent="EditColor/VSeparator6/Panel/MarginContainer/ColorPicker"]
layout_mode = 2
text = "OK"
[connection signal="pressed" from="EditColor/ColorProper/Button" to="." method="_on_Button_pressed"]
[connection signal="gui_input" from="EditColor/VSeparator6/Panel/MarginContainer/ColorPicker" to="." method="_on_ColorPicker_gui_input"]
[connection signal="pressed" from="EditColor/VSeparator6/Panel/MarginContainer/ColorPicker/Button" to="." method="_on_Button_pressed"]

View File

@ -0,0 +1,201 @@
@tool
extends "res://addons/resources_spreadsheet_view/typed_editors/dock_array.gd"
enum {
KEY_TYPE_STRINGNAME = 0,
KEY_TYPE_INT,
KEY_TYPE_FLOAT,
KEY_TYPE_OBJECT,
KEY_TYPE_VARIANT,
}
@onready var key_input := $"HBoxContainer/HBoxContainer/Control/VBoxContainer/KeyEdit/KeyEdit"
@onready var key_type := $"HBoxContainer/HBoxContainer/Control/VBoxContainer/KeyEdit/KeyType"
var _key_type_selected := 0
func try_edit_value(value, type, property_hint) -> bool:
if type != TYPE_DICTIONARY and type != TYPE_OBJECT:
return false
if value is Texture2D:
# For textures, prefer the specialized dock.
return false
key_type.visible = type != TYPE_OBJECT
_stored_type = type
if type == TYPE_DICTIONARY:
_stored_value = value.duplicate()
contents_label.text = var_to_str_no_sort(value)
return true
func _add_value(value):
var key = _get_key_from_box()
_stored_value[key] = value
var values : Array = sheet.get_edited_cells_values()
var cur_value
var dupe_value : bool = ProjectSettings.get_setting(TablesPluginSettingsClass.PREFIX + "dupe_arrays")
for i in values.size():
cur_value = values[i]
if dupe_value and (_stored_type == TYPE_DICTIONARY or cur_value.resource_path.rfind("::") != -1):
cur_value = cur_value.duplicate()
cur_value[key] = value
values[i] = cur_value
sheet.set_edited_cells_values(values)
super._add_recent(key)
func _add_values(added_values : Array):
for x in added_values:
_add_value(x)
func _remove_value(_value):
var key = _get_key_from_box()
_stored_value.erase(key)
var values : Array = sheet.get_edited_cells_values()
var cur_value
var dupe_value : bool = ProjectSettings.get_setting(TablesPluginSettingsClass.PREFIX + "dupe_arrays")
for i in values.size():
cur_value = values[i]
if dupe_value and (_stored_type == TYPE_DICTIONARY or cur_value.resource_path.rfind("::") != -1):
cur_value = cur_value.duplicate()
cur_value.erase(key)
values[i] = cur_value
sheet.set_edited_cells_values(values)
func _get_key_from_box():
if _stored_type == TYPE_OBJECT:
return StringName(key_input.text)
return _to_key(key_input.text, _key_type_selected)
func _to_key(from : String, key_type : int):
match key_type:
KEY_TYPE_STRINGNAME:
return StringName(from)
KEY_TYPE_INT:
return from.to_int()
KEY_TYPE_FLOAT:
return from.to_float()
KEY_TYPE_OBJECT:
return load(from)
KEY_TYPE_VARIANT:
return str_to_var(from)
func _on_Replace_pressed():
var old_key = _to_key(key_input.text, _key_type_selected)
var new_key = _to_key(value_input.text, _key_type_selected)
_stored_value[new_key] = _stored_value[old_key]
var values : Array = sheet.get_edited_cells_values()
var cur_value
var dupe_value : bool = ProjectSettings.get_setting(TablesPluginSettingsClass.PREFIX + "dupe_arrays")
for i in values.size():
cur_value = values[i]
if dupe_value and (_stored_type == TYPE_DICTIONARY or cur_value.resource_path.rfind("::") != -1):
cur_value = cur_value.duplicate()
cur_value[new_key] = cur_value[old_key]
values[i] = cur_value
sheet.set_edited_cells_values(values)
func _add_recent(_value):
pass
func _on_recent_clicked(button, value):
var val : int = recent_container.get_child(1).selected
key_input.text = str(value)
if val == 0:
# Do nothing! What if the value for the key doesn't match?
pass
if val == 1:
_remove_value(value)
if val == 2:
button.queue_free()
func _on_key_type_selected(index : int):
_key_type_selected = index
func _on_AddRecentFromSel_pressed():
for x in sheet.get_edited_cells_values():
if _stored_type == TYPE_OBJECT:
for y in x.get_property_list():
if y[&"usage"] & PROPERTY_USAGE_EDITOR != 0:
super._add_recent(y[&"name"])
else:
for y in x:
super._add_recent(y)
func _on_contents_edit_text_changed():
var value = str_to_var(contents_label.text)
if !value is Dictionary:
return
var values : Array = sheet.get_edited_cells_values()
for i in values.size():
values[i] = value.duplicate()
_stored_value = value
sheet.set_edited_cells_values(values)
func var_to_str_no_sort(value, indent = " ", cur_indent = ""):
var lines : Array[String] = []
if value is Array:
cur_indent += indent
lines.resize(value.size())
for i in lines.size():
if value[i] is Array or value[i] is Dictionary:
lines[i] = "%s%s" % [cur_indent, var_to_str_no_sort(value[i])]
else:
lines[i] = "%s%s" % [cur_indent, var_to_str(value[i])]
cur_indent = cur_indent.substr(0, cur_indent.length() - indent.length())
return "[\n" + ",\n".join(lines) + "\n]"
if value is Dictionary:
var keys : Array = value.keys()
var values : Array = value.values()
cur_indent += indent
lines.resize(keys.size())
for i in lines.size():
if values[i] is Array or values[i] is Dictionary:
lines[i] = "%s%s : %s" % [cur_indent, var_to_str(keys[i]), var_to_str_no_sort(values[i])]
else:
lines[i] = "%s%s : %s" % [cur_indent, var_to_str(keys[i]), var_to_str(values[i])]
cur_indent = cur_indent.substr(0, cur_indent.length() - indent.length())
return "{\n" + ",\n".join(lines) + "\n}"
return ",\n".join(lines)

View File

@ -0,0 +1,226 @@
[gd_scene load_steps=5 format=3 uid="uid://p6x03dbvhnqw"]
[ext_resource type="Script" path="res://addons/resources_spreadsheet_view/typed_editors/dock_dict.gd" id="1_2yivi"]
[ext_resource type="Script" path="res://addons/resources_spreadsheet_view/editor_icon_button.gd" id="2_yck0k"]
[sub_resource type="Image" id="Image_bq2wd"]
data = {
"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 93, 93, 55, 255, 97, 97, 58, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 97, 97, 42, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 98, 98, 47, 255, 97, 97, 42, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 93, 93, 233, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 94, 94, 46, 255, 93, 93, 236, 255, 93, 93, 233, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
"format": "RGBA8",
"height": 16,
"mipmaps": false,
"width": 16
}
[sub_resource type="ImageTexture" id="ImageTexture_3oshq"]
image = SubResource("Image_bq2wd")
[node name="EditArray" type="VBoxContainer"]
anchors_preset = 10
anchor_right = 1.0
grow_horizontal = 2
mouse_filter = 0
script = ExtResource("1_2yivi")
[node name="Header" type="HBoxContainer" parent="."]
layout_mode = 2
mouse_filter = 0
mouse_default_cursor_shape = 9
[node name="HBoxContainer" type="HBoxContainer" parent="Header"]
layout_mode = 2
size_flags_horizontal = 3
[node name="Label" type="Label" parent="Header/HBoxContainer"]
layout_mode = 2
text = "EDIT: Dict/Object"
[node name="HSeparator" type="HSeparator" parent="Header/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
mouse_filter = 2
[node name="Label" type="Label" parent="Header"]
layout_mode = 2
text = "PROPERTY NAME"
[node name="HSeparator2" type="HSeparator" parent="Header"]
layout_mode = 2
size_flags_horizontal = 3
mouse_filter = 2
[node name="HBoxContainer" type="HSplitContainer" parent="."]
layout_mode = 2
size_flags_vertical = 3
split_offset = 520
[node name="HBoxContainer" type="HBoxContainer" parent="HBoxContainer"]
layout_mode = 2
alignment = 2
[node name="Panel" type="MarginContainer" parent="HBoxContainer/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
[node name="Label" type="TextEdit" parent="HBoxContainer/HBoxContainer/Panel"]
layout_mode = 2
size_flags_vertical = 5
text = "[]"
[node name="VSeparator2" type="VSeparator" parent="HBoxContainer/HBoxContainer"]
layout_mode = 2
[node name="Control" type="HBoxContainer" parent="HBoxContainer/HBoxContainer"]
layout_mode = 2
[node name="VBoxContainer" type="VBoxContainer" parent="HBoxContainer/HBoxContainer/Control"]
layout_mode = 2
[node name="KeyEdit" type="HBoxContainer" parent="HBoxContainer/HBoxContainer/Control/VBoxContainer"]
layout_mode = 2
[node name="KeyEdit" type="LineEdit" parent="HBoxContainer/HBoxContainer/Control/VBoxContainer/KeyEdit"]
layout_mode = 2
size_flags_horizontal = 3
placeholder_text = "Input key to add/change/erase..."
clear_button_enabled = true
[node name="KeyType" type="OptionButton" parent="HBoxContainer/HBoxContainer/Control/VBoxContainer/KeyEdit"]
layout_mode = 2
selected = 0
fit_to_longest_item = false
item_count = 5
popup/item_0/text = "Stringname"
popup/item_1/text = "Int"
popup/item_1/id = 1
popup/item_2/text = "Float"
popup/item_2/id = 2
popup/item_3/text = "Resource path"
popup/item_3/id = 4
popup/item_4/text = "Guess type"
popup/item_4/id = 3
[node name="LineEdit" type="LineEdit" parent="HBoxContainer/HBoxContainer/Control/VBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
placeholder_text = "Input value..."
clear_button_enabled = true
[node name="HBoxContainer" type="HBoxContainer" parent="HBoxContainer/HBoxContainer/Control/VBoxContainer"]
layout_mode = 2
[node name="Label" type="Label" parent="HBoxContainer/HBoxContainer/Control/VBoxContainer/HBoxContainer"]
layout_mode = 2
text = "Set value with type:"
[node name="String" type="Button" parent="HBoxContainer/HBoxContainer/Control/VBoxContainer/HBoxContainer"]
layout_mode = 2
tooltip_text = "Set to string"
icon = SubResource("ImageTexture_3oshq")
script = ExtResource("2_yck0k")
icon_name = "String"
[node name="Int" type="Button" parent="HBoxContainer/HBoxContainer/Control/VBoxContainer/HBoxContainer"]
layout_mode = 2
tooltip_text = "Set to integer"
icon = SubResource("ImageTexture_3oshq")
script = ExtResource("2_yck0k")
icon_name = "int"
[node name="Float" type="Button" parent="HBoxContainer/HBoxContainer/Control/VBoxContainer/HBoxContainer"]
layout_mode = 2
tooltip_text = "Set to float"
icon = SubResource("ImageTexture_3oshq")
script = ExtResource("2_yck0k")
icon_name = "float"
[node name="Variant" type="Button" parent="HBoxContainer/HBoxContainer/Control/VBoxContainer/HBoxContainer"]
layout_mode = 2
tooltip_text = "Guess type and set"
icon = SubResource("ImageTexture_3oshq")
script = ExtResource("2_yck0k")
icon_name = "Variant"
[node name="Object" type="Button" parent="HBoxContainer/HBoxContainer/Control/VBoxContainer/HBoxContainer"]
layout_mode = 2
tooltip_text = "Set to resource (by path if string)"
icon = SubResource("ImageTexture_3oshq")
script = ExtResource("2_yck0k")
icon_name = "Object"
[node name="Label2" type="Label" parent="HBoxContainer/HBoxContainer/Control/VBoxContainer/HBoxContainer"]
layout_mode = 2
text = "Key:"
[node name="Remove" type="Button" parent="HBoxContainer/HBoxContainer/Control/VBoxContainer/HBoxContainer"]
layout_mode = 2
tooltip_text = "Erase keys in Key textbox"
icon = SubResource("ImageTexture_3oshq")
script = ExtResource("2_yck0k")
icon_name = "Remove"
[node name="Replace" type="Button" parent="HBoxContainer/HBoxContainer/Control/VBoxContainer/HBoxContainer"]
layout_mode = 2
tooltip_text = "Copy Key's value to new key
Example:
- key box = \"thing\"
- value box = \"thing two\"
- old dict = {\"thing\" : 12}
- resulting dict = {\"thing\" : 12, \"thing two\" : 12}"
icon = SubResource("ImageTexture_3oshq")
script = ExtResource("2_yck0k")
icon_name = "Override"
[node name="VSeparator" type="VSeparator" parent="HBoxContainer/HBoxContainer"]
layout_mode = 2
[node name="Control2" type="ScrollContainer" parent="HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
follow_focus = true
horizontal_scroll_mode = 0
[node name="HBoxContainer" type="HBoxContainer" parent="HBoxContainer/Control2"]
layout_mode = 2
size_flags_horizontal = 3
size_flags_vertical = 3
alignment = 2
[node name="VSeparator2" type="VSeparator" parent="HBoxContainer/Control2/HBoxContainer"]
layout_mode = 2
[node name="HFlowContainer" type="HFlowContainer" parent="HBoxContainer/Control2/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
[node name="Label" type="Label" parent="HBoxContainer/Control2/HBoxContainer/HFlowContainer"]
layout_mode = 2
text = "Recent:"
[node name="OptionButton" type="OptionButton" parent="HBoxContainer/Control2/HBoxContainer/HFlowContainer"]
layout_mode = 2
selected = 0
fit_to_longest_item = false
item_count = 3
popup/item_0/text = "Add"
popup/item_1/text = "Erase"
popup/item_1/id = 1
popup/item_2/text = "Remove From Recent"
popup/item_2/id = 2
[node name="AddRecentFromSel" type="Button" parent="HBoxContainer/Control2/HBoxContainer/HFlowContainer"]
layout_mode = 2
tooltip_text = "Add from selected cells"
icon = SubResource("ImageTexture_3oshq")
script = ExtResource("2_yck0k")
icon_name = "ListSelect"
[connection signal="item_selected" from="HBoxContainer/HBoxContainer/Control/VBoxContainer/KeyEdit/KeyType" to="." method="_on_key_type_selected"]
[connection signal="pressed" from="HBoxContainer/HBoxContainer/Control/VBoxContainer/HBoxContainer/String" to="." method="_on_String_pressed"]
[connection signal="pressed" from="HBoxContainer/HBoxContainer/Control/VBoxContainer/HBoxContainer/Int" to="." method="_on_Int_pressed"]
[connection signal="pressed" from="HBoxContainer/HBoxContainer/Control/VBoxContainer/HBoxContainer/Float" to="." method="_on_Float_pressed"]
[connection signal="pressed" from="HBoxContainer/HBoxContainer/Control/VBoxContainer/HBoxContainer/Variant" to="." method="_on_Variant_pressed"]
[connection signal="pressed" from="HBoxContainer/HBoxContainer/Control/VBoxContainer/HBoxContainer/Object" to="." method="_on_Resource_pressed"]
[connection signal="pressed" from="HBoxContainer/HBoxContainer/Control/VBoxContainer/HBoxContainer/Remove" to="." method="_on_Remove_pressed"]
[connection signal="pressed" from="HBoxContainer/HBoxContainer/Control/VBoxContainer/HBoxContainer/Replace" to="." method="_on_Replace_pressed"]
[connection signal="pressed" from="HBoxContainer/Control2/HBoxContainer/HFlowContainer/AddRecentFromSel" to="." method="_on_AddRecentFromSel_pressed"]

View File

@ -0,0 +1,137 @@
@tool
extends ResourceTablesDockEditor
@onready var options_container := $"HBoxContainer/Control2/HBoxContainer/HFlowContainer"
@onready var contents_label := $"HBoxContainer/HBoxContainer/Panel/Label"
@onready var _init_nodes_in_options_container := options_container.get_child_count()
var _stored_value
var _last_column := -1
func _ready():
super()
contents_label.text_changed.connect(_on_contents_edit_text_changed)
func try_edit_value(value, type, property_hint) -> bool:
if !sheet.column_hint_strings[sheet.get_selected_column()][0].begins_with("2/2:"):
return false
_stored_value = value.duplicate() # Generic arrays are passed by reference
if _last_column != sheet.get_selected_column():
_last_column = sheet.get_selected_column()
for x in options_container.get_children():
x.visible = x.get_index() < _init_nodes_in_options_container
for i in sheet.column_hint_strings[sheet.get_selected_column()].size():
_create_option_button(i)
contents_label.text = str(value)
return true
func _create_option_button(index : int):
var value = sheet.column_hint_strings[sheet.get_selected_column()][index]
if index == 0:
# Enum array hints have "2/3:" before list.
value = value.substr(value.find(":") + 1)
var node
if index >= options_container.get_child_count() - _init_nodes_in_options_container:
node = Button.new()
options_container.add_child(node)
var colon_found : int = value.rfind(":")
if colon_found == -1:
node.pressed.connect(_on_option_clicked.bind(index))
else:
node.pressed.connect(_on_option_clicked.bind(value.substr(colon_found + 1).to_int()))
else:
node = options_container.get_child(index + _init_nodes_in_options_container)
node.visible = true
node.text = str(value)
node.self_modulate = Color(value.hash()) + Color(0.25, 0.25, 0.25, 1.0)
return node
func _add_value(option_value : int):
_stored_value.append(option_value)
var values : Array = sheet.get_edited_cells_values()
var cur_value
var dupe_array : bool = ProjectSettings.get_setting(TablesPluginSettingsClass.PREFIX + "dupe_arrays")
for i in values.size():
cur_value = values[i]
if dupe_array:
cur_value = cur_value.duplicate()
cur_value.append(option_value)
values[i] = cur_value
sheet.set_edited_cells_values(values)
func _remove_value(option_value : int):
_stored_value.append(option_value)
var values : Array = sheet.get_edited_cells_values()
var cur_value
var dupe_array : bool = ProjectSettings.get_setting(TablesPluginSettingsClass.PREFIX + "dupe_arrays")
for i in values.size():
cur_value = values[i]
if dupe_array:
cur_value = cur_value.duplicate()
if cur_value.has(option_value):
cur_value.remove_at(cur_value.find(option_value))
values[i] = cur_value
sheet.set_edited_cells_values(values)
func _on_option_clicked(value : int):
var val = options_container.get_child(1).selected
if val == 0:
_add_value(value)
if val == 1:
_remove_value(value)
func _on_Remove_pressed():
_stored_value.remove_at(_stored_value.size() - 1)
var values = sheet.get_edited_cells_values()
var cur_value
var dupe_array : bool = ProjectSettings.get_setting(TablesPluginSettingsClass.PREFIX + "dupe_arrays")
for i in values.size():
cur_value = values[i]
if dupe_array:
cur_value = cur_value.duplicate()
cur_value.remove_at(cur_value.size() - 1)
values[i] = cur_value
sheet.set_edited_cells_values(values)
func _on_contents_edit_text_changed():
var value = str_to_var(contents_label.text)
if !value is Array:
return
for x in value:
if !x is int:
return
var values = sheet.get_edited_cells_values()
for i in values.size():
values[i] = values[i].duplicate()
values[i].resize(value.size())
for j in value.size():
values[i][j] = value[j]
_stored_value = value
sheet.set_edited_cells_values(values)

View File

@ -0,0 +1,117 @@
[gd_scene load_steps=5 format=3 uid="uid://ddqak780cwwfj"]
[ext_resource type="Script" path="res://addons/resources_spreadsheet_view/typed_editors/dock_enum_array.gd" id="1_n3flg"]
[ext_resource type="Script" path="res://addons/resources_spreadsheet_view/editor_icon_button.gd" id="2_mda1e"]
[sub_resource type="Image" id="Image_5ktp6"]
data = {
"data": PackedByteArray(255, 255, 255, 0, 255, 255, 255, 0, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 131, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 94, 94, 127, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 128, 128, 4, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 231, 255, 93, 93, 55, 255, 97, 97, 58, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 231, 255, 94, 94, 54, 255, 94, 94, 57, 255, 93, 93, 233, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 93, 93, 41, 255, 255, 255, 0, 255, 255, 255, 0, 255, 97, 97, 42, 255, 93, 93, 233, 255, 93, 93, 232, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 97, 97, 42, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 98, 98, 47, 255, 97, 97, 42, 255, 255, 255, 0, 255, 97, 97, 42, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 96, 96, 45, 255, 93, 93, 235, 255, 93, 93, 233, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 94, 94, 46, 255, 93, 93, 236, 255, 93, 93, 233, 255, 97, 97, 42, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 93, 93, 235, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 233, 255, 95, 95, 59, 255, 96, 96, 61, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 93, 93, 255, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 93, 93, 252, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0, 255, 255, 255, 0),
"format": "RGBA8",
"height": 16,
"mipmaps": false,
"width": 16
}
[sub_resource type="ImageTexture" id="ImageTexture_3oshq"]
image = SubResource("Image_5ktp6")
[node name="EditEnumArray" type="VBoxContainer"]
anchors_preset = 10
anchor_right = 1.0
mouse_filter = 0
script = ExtResource("1_n3flg")
[node name="Header" type="HBoxContainer" parent="."]
layout_mode = 2
mouse_filter = 0
mouse_default_cursor_shape = 9
[node name="HBoxContainer" type="HBoxContainer" parent="Header"]
layout_mode = 2
size_flags_horizontal = 3
[node name="Label" type="Label" parent="Header/HBoxContainer"]
layout_mode = 2
text = "EDIT: Enum Array"
[node name="HSeparator" type="HSeparator" parent="Header/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
mouse_filter = 2
[node name="Label" type="Label" parent="Header"]
layout_mode = 2
text = "PROPERTY NAME"
[node name="HSeparator2" type="HSeparator" parent="Header"]
layout_mode = 2
size_flags_horizontal = 3
mouse_filter = 2
[node name="HBoxContainer" type="HSplitContainer" parent="."]
layout_mode = 2
size_flags_vertical = 3
split_offset = 250
[node name="HBoxContainer" type="HBoxContainer" parent="HBoxContainer"]
layout_mode = 2
alignment = 2
[node name="Panel" type="MarginContainer" parent="HBoxContainer/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
[node name="Label" type="TextEdit" parent="HBoxContainer/HBoxContainer/Panel"]
layout_mode = 2
size_flags_vertical = 5
text = "[]"
[node name="VSeparator2" type="VSeparator" parent="HBoxContainer/HBoxContainer"]
layout_mode = 2
[node name="Control" type="MarginContainer" parent="HBoxContainer/HBoxContainer"]
layout_mode = 2
[node name="VBoxContainer" type="VBoxContainer" parent="HBoxContainer/HBoxContainer/Control"]
layout_mode = 2
[node name="Remove" type="Button" parent="HBoxContainer/HBoxContainer/Control/VBoxContainer"]
layout_mode = 2
text = "Remove Last Value"
icon = SubResource("ImageTexture_3oshq")
script = ExtResource("2_mda1e")
icon_name = "Remove"
[node name="VSeparator" type="VSeparator" parent="HBoxContainer/HBoxContainer"]
layout_mode = 2
[node name="Control2" type="MarginContainer" parent="HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
[node name="HBoxContainer" type="HBoxContainer" parent="HBoxContainer/Control2"]
layout_mode = 2
alignment = 2
[node name="VSeparator2" type="VSeparator" parent="HBoxContainer/Control2/HBoxContainer"]
layout_mode = 2
[node name="HFlowContainer" type="HFlowContainer" parent="HBoxContainer/Control2/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
[node name="Label" type="Label" parent="HBoxContainer/Control2/HBoxContainer/HFlowContainer"]
layout_mode = 2
text = "Options:"
[node name="OptionButton" type="OptionButton" parent="HBoxContainer/Control2/HBoxContainer/HFlowContainer"]
layout_mode = 2
item_count = 2
selected = 0
fit_to_longest_item = false
popup/item_0/text = "Add"
popup/item_0/id = 0
popup/item_1/text = "Erase"
popup/item_1/id = 1
[connection signal="pressed" from="HBoxContainer/HBoxContainer/Control/VBoxContainer/Remove" to="." method="_on_Remove_pressed"]

View File

@ -0,0 +1,188 @@
@tool
extends ResourceTablesDockEditor
@onready var _value_label := $"HBoxContainer/HBoxContainer/NumberPanel/Label"
@onready var _button_grid := $"HBoxContainer/HBoxContainer/GridContainer"
@onready var _button_grid_small := $"HBoxContainer/HBoxContainer/GridContainerSmall"
@onready var _sequence_gen_inputs := $"HBoxContainer/CustomX2/HBoxContainer"
@onready var _custom_value_edit := $"HBoxContainer/CustomX/Box/LineEdit"
var _stored_value = 0
var _stored_value_is_int := false
var _mouse_drag_increment := 0.0
var _mouse_down := false
var _resize_height_small := 0.0
var _resize_expanded := true
func _ready():
super._ready()
_button_grid.get_child(0).pressed.connect(_increment_values.bind(+0.1))
_button_grid.get_child(1).pressed.connect(_increment_values.bind(+1))
_button_grid.get_child(2).pressed.connect(_increment_values.bind(+10))
_button_grid.get_child(3).pressed.connect(_increment_values.bind(+100))
_button_grid.get_child(4).pressed.connect(_increment_values_custom.bind(true, false))
_button_grid.get_child(5).pressed.connect(_increment_values_custom.bind(true, true))
_button_grid.get_child(6).pressed.connect(_increment_values.bind(-0.1))
_button_grid.get_child(7).pressed.connect(_increment_values.bind(-1))
_button_grid.get_child(8).pressed.connect(_increment_values.bind(-10))
_button_grid.get_child(9).pressed.connect(_increment_values.bind(-100))
_button_grid.get_child(10).pressed.connect(_increment_values_custom.bind(false, false))
_button_grid.get_child(11).pressed.connect(_increment_values_custom.bind(false, true))
_button_grid_small.get_child(1).pressed.connect(_increment_values_custom.bind(false, true))
_button_grid_small.get_child(2).pressed.connect(_increment_values_custom.bind(false, false))
_button_grid_small.get_child(3).pressed.connect(_increment_values.bind(-1))
_button_grid_small.get_child(4).pressed.connect(_increment_values.bind(+1))
_button_grid_small.get_child(5).pressed.connect(_increment_values_custom.bind(true, false))
_button_grid_small.get_child(6).pressed.connect(_increment_values_custom.bind(true, true))
_resize_height_small = get_child(1).get_minimum_size().y
func try_edit_value(value, type, property_hint) -> bool:
if type != TYPE_FLOAT and type != TYPE_INT:
return false
_stored_value = value
_value_label.text = str(value)
_stored_value_is_int = type != TYPE_FLOAT
_button_grid.columns = 5 if _stored_value_is_int else 6
_button_grid.get_child(0).visible = !_stored_value_is_int
_button_grid.get_child(6).visible = !_stored_value_is_int
return true
func resize_drag(to_height : float):
var expanded : bool = to_height >= _resize_height_small
if _resize_expanded == expanded:
return
_resize_expanded = expanded
_button_grid.visible = expanded
_button_grid_small.visible = !expanded
$"HBoxContainer/CustomX2/HBoxContainer/Label2".visible = !expanded
$"HBoxContainer/CustomX2/HBoxContainer3".visible = expanded
$"HBoxContainer/HBoxContainer/NumberPanel".visible = expanded
$"HBoxContainer/CustomX2/HBoxContainer2".visible = expanded
$"HBoxContainer/CustomX2/HBoxContainer/Box".visible = !expanded
$"HBoxContainer/CustomX/Label".visible = expanded
func _increment_values(by : float):
var cell_values : Array = sheet.get_edited_cells_values()
if _stored_value_is_int:
_stored_value += int(by)
for i in cell_values.size():
cell_values[i] += int(by)
else:
_stored_value += by
for i in cell_values.size():
cell_values[i] += by
sheet.set_edited_cells_values(cell_values)
_value_label.text = str(_stored_value)
func _increment_values_custom(positive : bool, multiplier : bool):
var value := float(_custom_value_edit.text)
if !multiplier:
_increment_values(value if positive else -value)
else:
if !positive: value = 1 / value
var cell_values : Array = sheet.get_edited_cells_values()
_stored_value *= value
for i in cell_values.size():
cell_values[i] *= value
if _stored_value_is_int:
cell_values[i] = int(cell_values[i])
sheet.set_edited_cells_values(cell_values)
_value_label.text = str(_stored_value)
func _on_NumberPanel_gui_input(event):
if event is InputEventMouseButton and event.button_index == MOUSE_BUTTON_LEFT:
if event.pressed:
Input.mouse_mode = Input.MOUSE_MODE_CAPTURED
_mouse_drag_increment = 0.0
_mouse_down = true
else:
Input.mouse_mode = Input.MOUSE_MODE_VISIBLE
if _mouse_down:
Input.warp_mouse(_value_label.global_position + _value_label.size * 0.5)
_increment_values(_mouse_drag_increment)
_mouse_down = false
if _mouse_down and event is InputEventMouseMotion:
if _stored_value_is_int:
_mouse_drag_increment += event.relative.x * 0.25
_value_label.text = str(_stored_value + int(_mouse_drag_increment))
else:
_mouse_drag_increment += event.relative.x * 0.01
_value_label.text = str(_stored_value + _mouse_drag_increment)
func _on_SequenceFill_pressed(add : bool = false):
sheet.set_edited_cells_values(_fill_sequence(sheet.get_edited_cells_values(), add))
func _fill_sequence(arr : Array, add : bool = false) -> Array:
if !_sequence_gen_inputs.get_node("Start").text.is_valid_float():
return arr
var start := float(_sequence_gen_inputs.get_child(0).text)
var end = null
var step = null
if _sequence_gen_inputs.get_node("Step").text.is_valid_float():
step = float(_sequence_gen_inputs.get_node("Step").text)
if _sequence_gen_inputs.get_node("End").text.is_valid_float():
end = float(_sequence_gen_inputs.get_node("End").text)
if end == null:
end = INF if step == null or step >= 0 else -INF
var end_is_higher = end > start
if step == null:
if end == null or end == INF or end == -INF:
step = 0.0
else:
step = (end - start) / arr.size()
if _stored_value_is_int:
if start != null:
start = int(start)
if step != null:
step = int(step)
if end != INF and end != -INF:
end = int(end)
var cur = start
if !add:
for i in arr.size():
arr[i] = 0
# The range() global function can also be used, but does not work with floats.
for i in arr.size():
arr[i] = arr[i] + cur
cur += step
if (end_is_higher and cur >= end) or (!end_is_higher and cur <= end):
cur += (start - end)
return arr

View File

@ -0,0 +1,265 @@
[gd_scene load_steps=2 format=3 uid="uid://gtbf7b0wptv"]
[ext_resource type="Script" path="res://addons/resources_spreadsheet_view/typed_editors/dock_number.gd" id="1"]
[node name="EditNumber" type="VBoxContainer"]
offset_right = 1141.0
offset_bottom = 126.0
mouse_filter = 0
script = ExtResource("1")
[node name="Header" type="HBoxContainer" parent="."]
layout_mode = 2
mouse_filter = 0
mouse_default_cursor_shape = 9
[node name="HBoxContainer" type="HBoxContainer" parent="Header"]
layout_mode = 2
size_flags_horizontal = 3
[node name="Label" type="Label" parent="Header/HBoxContainer"]
layout_mode = 2
text = "EDIT: Number"
[node name="HSeparator" type="HSeparator" parent="Header/HBoxContainer"]
layout_mode = 2
size_flags_horizontal = 3
mouse_filter = 2
[node name="Label" type="Label" parent="Header"]
layout_mode = 2
text = "PROPERTY NAME"
[node name="HSeparator2" type="HSeparator" parent="Header"]
layout_mode = 2
size_flags_horizontal = 3
mouse_filter = 2
[node name="HBoxContainer" type="HBoxContainer" parent="."]
layout_mode = 2
size_flags_vertical = 3
alignment = 1
[node name="CustomX2" type="VBoxContainer" parent="HBoxContainer"]
layout_mode = 2
[node name="HBoxContainer3" type="HBoxContainer" parent="HBoxContainer/CustomX2"]
layout_mode = 2
size_flags_vertical = 3
[node name="Label" type="Label" parent="HBoxContainer/CustomX2/HBoxContainer3"]
layout_mode = 2
size_flags_vertical = 6
text = "Fill with Sequence"
[node name="Label2" type="Label" parent="HBoxContainer/CustomX2/HBoxContainer3"]
layout_mode = 2
size_flags_vertical = 6
tooltip_text = "Fill selected cells with a number sequence. Order is the same as the cells were selected.
- You must specify Start.
- If all values specified, selected cells will have a repeating sequence
of numbers from Start to End, with increment of Step, not including End.
- If both End AND Step are empty, cells are filled with Start.
- If End is omitted, selected cells will have values from Start to (Step x CellCount).
- If Step is omitted, selected cells will have values from Start to End inclusive,
step based on cell count."
mouse_filter = 0
mouse_default_cursor_shape = 16
text = "(?)"
[node name="HBoxContainer" type="HBoxContainer" parent="HBoxContainer/CustomX2"]
layout_mode = 2
[node name="Label2" type="Label" parent="HBoxContainer/CustomX2/HBoxContainer"]
visible = false
layout_mode = 2
text = "Sequence"
[node name="Start" type="LineEdit" parent="HBoxContainer/CustomX2/HBoxContainer"]
layout_mode = 2
tooltip_text = "Start (must not be blank)"
placeholder_text = "Start *"
[node name="End" type="LineEdit" parent="HBoxContainer/CustomX2/HBoxContainer"]
layout_mode = 2
tooltip_text = "End"
placeholder_text = "End"
[node name="Step" type="LineEdit" parent="HBoxContainer/CustomX2/HBoxContainer"]
layout_mode = 2
tooltip_text = "End"
placeholder_text = "Step"
[node name="Box" type="HBoxContainer" parent="HBoxContainer/CustomX2/HBoxContainer"]
visible = false
layout_mode = 2
[node name="VSeparator" type="VSeparator" parent="HBoxContainer/CustomX2/HBoxContainer/Box"]
layout_mode = 2
[node name="SequenceFill3" type="Button" parent="HBoxContainer/CustomX2/HBoxContainer/Box"]
layout_mode = 2
size_flags_horizontal = 3
tooltip_text = "Set"
text = "="
[node name="SequenceFill4" type="Button" parent="HBoxContainer/CustomX2/HBoxContainer/Box"]
layout_mode = 2
size_flags_horizontal = 3
tooltip_text = "Add to each value"
text = "+"
[node name="HBoxContainer2" type="HBoxContainer" parent="HBoxContainer/CustomX2"]
layout_mode = 2
[node name="SequenceFill" type="Button" parent="HBoxContainer/CustomX2/HBoxContainer2"]
layout_mode = 2
size_flags_horizontal = 3
text = "Set"
[node name="SequenceFill2" type="Button" parent="HBoxContainer/CustomX2/HBoxContainer2"]
layout_mode = 2
size_flags_horizontal = 3
text = "Add"
[node name="VSeparator2" type="VSeparator" parent="HBoxContainer"]
layout_mode = 2
[node name="HBoxContainer" type="VBoxContainer" parent="HBoxContainer"]
layout_mode = 2
alignment = 1
[node name="NumberPanel" type="MarginContainer" parent="HBoxContainer/HBoxContainer"]
layout_mode = 2
size_flags_vertical = 3
mouse_default_cursor_shape = 10
[node name="Panel" type="Panel" parent="HBoxContainer/HBoxContainer/NumberPanel"]
layout_mode = 2
mouse_filter = 2
[node name="Label" type="Label" parent="HBoxContainer/HBoxContainer/NumberPanel"]
layout_mode = 2
text = "1"
clip_text = true
[node name="GridContainer" type="GridContainer" parent="HBoxContainer/HBoxContainer"]
layout_mode = 2
columns = 6
[node name="Button15" type="Button" parent="HBoxContainer/HBoxContainer/GridContainer"]
layout_mode = 2
text = "+0.1"
[node name="Button12" type="Button" parent="HBoxContainer/HBoxContainer/GridContainer"]
layout_mode = 2
text = "+1"
[node name="Button8" type="Button" parent="HBoxContainer/HBoxContainer/GridContainer"]
layout_mode = 2
text = "+10"
[node name="Button13" type="Button" parent="HBoxContainer/HBoxContainer/GridContainer"]
layout_mode = 2
text = "+100"
[node name="Button9" type="Button" parent="HBoxContainer/HBoxContainer/GridContainer"]
layout_mode = 2
text = "+X"
[node name="Button16" type="Button" parent="HBoxContainer/HBoxContainer/GridContainer"]
layout_mode = 2
text = "*X"
[node name="Button14" type="Button" parent="HBoxContainer/HBoxContainer/GridContainer"]
layout_mode = 2
text = "-0.1"
[node name="Button11" type="Button" parent="HBoxContainer/HBoxContainer/GridContainer"]
layout_mode = 2
text = "-1"
[node name="Button7" type="Button" parent="HBoxContainer/HBoxContainer/GridContainer"]
layout_mode = 2
text = "-10"
[node name="Button10" type="Button" parent="HBoxContainer/HBoxContainer/GridContainer"]
layout_mode = 2
text = "-100"
[node name="Button4" type="Button" parent="HBoxContainer/HBoxContainer/GridContainer"]
layout_mode = 2
text = "-X"
[node name="Button5" type="Button" parent="HBoxContainer/HBoxContainer/GridContainer"]
layout_mode = 2
text = "/X"
[node name="GridContainerSmall" type="HBoxContainer" parent="HBoxContainer/HBoxContainer"]
visible = false
layout_mode = 2
[node name="Label" type="Label" parent="HBoxContainer/HBoxContainer/GridContainerSmall"]
layout_mode = 2
text = "Value"
[node name="Button1" type="Button" parent="HBoxContainer/HBoxContainer/GridContainerSmall"]
layout_mode = 2
size_flags_horizontal = 3
text = "/X"
[node name="Button2" type="Button" parent="HBoxContainer/HBoxContainer/GridContainerSmall"]
layout_mode = 2
size_flags_horizontal = 3
text = "-X"
[node name="Button3" type="Button" parent="HBoxContainer/HBoxContainer/GridContainerSmall"]
layout_mode = 2
size_flags_horizontal = 3
text = "-1"
[node name="Button4" type="Button" parent="HBoxContainer/HBoxContainer/GridContainerSmall"]
layout_mode = 2
size_flags_horizontal = 3
text = "+1"
[node name="Button5" type="Button" parent="HBoxContainer/HBoxContainer/GridContainerSmall"]
layout_mode = 2
size_flags_horizontal = 3
text = "+X"
[node name="Button6" type="Button" parent="HBoxContainer/HBoxContainer/GridContainerSmall"]
layout_mode = 2
size_flags_horizontal = 3
text = "*X"
[node name="VSeparator" type="VSeparator" parent="HBoxContainer"]
layout_mode = 2
[node name="CustomX" type="VBoxContainer" parent="HBoxContainer"]
layout_mode = 2
[node name="Label" type="Label" parent="HBoxContainer/CustomX"]
layout_mode = 2
size_flags_vertical = 6
text = "Custom Value"
horizontal_alignment = 1
[node name="Box" type="HBoxContainer" parent="HBoxContainer/CustomX"]
layout_mode = 2
[node name="Label" type="Label" parent="HBoxContainer/CustomX/Box"]
layout_mode = 2
text = "X ="
[node name="LineEdit" type="LineEdit" parent="HBoxContainer/CustomX/Box"]
custom_minimum_size = Vector2(96, 0)
layout_mode = 2
size_flags_horizontal = 3
text = "1000"
[connection signal="pressed" from="HBoxContainer/CustomX2/HBoxContainer/Box/SequenceFill3" to="." method="_on_SequenceFill_pressed"]
[connection signal="pressed" from="HBoxContainer/CustomX2/HBoxContainer/Box/SequenceFill4" to="." method="_on_SequenceFill_pressed"]
[connection signal="pressed" from="HBoxContainer/CustomX2/HBoxContainer2/SequenceFill" to="." method="_on_SequenceFill_pressed"]
[connection signal="pressed" from="HBoxContainer/CustomX2/HBoxContainer2/SequenceFill2" to="." method="_on_SequenceFill_pressed" binds= [true]]
[connection signal="gui_input" from="HBoxContainer/HBoxContainer/NumberPanel" to="." method="_on_NumberPanel_gui_input"]

Some files were not shown because too many files have changed in this diff Show More