Search Unity

Editor skinning thread

Discussion in '2019.3 Beta' started by pointcache, Jul 16, 2019.

  1. pointcache

    pointcache

    Joined:
    Sep 22, 2012
    Posts:
    525
  2. pointcache

    pointcache

    Joined:
    Sep 22, 2012
    Posts:
    525
    First question - anyone managed to replace font globally?

    I tried both
    Code (CSharp):
    1. :root {
    2.     font-size: 11px;
    3.     -unity-font: resource("Fonts/dejavu.ttf");
    4.     color: rgba(217, 217, 217, 0.95);
    5. }
    and in
    Code (CSharp):
    1. .unity-theme-env-variables
    block.

    It seems unity overrides font somewhere else, was looking with ilspy into where it happens and how to inject custom font.

    It doesn't work even for individual elements override
     
    Last edited: Jul 16, 2019
  3. GameDevCouple_I

    GameDevCouple_I

    Joined:
    Oct 5, 2013
    Posts:
    1,891
    Trying to work this out myself, please let me know if you figure it out and I will do the same!
     
  4. pointcache

    pointcache

    Joined:
    Sep 22, 2012
    Posts:
    525
    I managed to inject my font but it doesnt work

    upload_2019-7-16_16-42-15.png


    Code (CSharp):
    1. using UnityEditor;
    2. using UnityEditor.Experimental;
    3. using UnityEngine;
    4. using System.Reflection;
    5. using System.Collections.Generic;
    6.  
    7. [InitializeOnLoad]
    8. public static class ReloadTool
    9. {
    10.     private const string FONT = "Dejavu Sans Mono";
    11.     private const string FONTPATH = "Fonts/dejavu.ttf";
    12.  
    13.     static ReloadTool()
    14.     {
    15.         // var font = Resources.Load(FONTPATH) as Font;
    16.         // var fontField = typeof(EditorResources).GetField("s_NormalFont", BindingFlags.Static | BindingFlags.NonPublic);
    17.         // fontField.SetValue(null, font);
    18.         //
    19.         // GUISkin defaultSkin = GUIUtility.GetDefaultSkin();
    20.         // //defaultSkin.font =
    21.         // EditorResources.UpdateGUIStyleProperties(defaultSkin);
    22.  
    23.         var supportedFontsField = typeof(EditorResources).GetField("s_SupportedFonts", BindingFlags.Static | BindingFlags.NonPublic);
    24.        // var builtInFontsField = typeof(EditorResources).GetProperty("builtInFonts", BindingFlags.Static | BindingFlags.NonPublic);
    25.  
    26.         List<string> supportedFonts = supportedFontsField.GetValue(null) as List<string>;
    27.         if (supportedFonts == null)
    28.             supportedFonts = EditorResources.GetSupportedFonts();
    29.  
    30.         //Dictionary<string, string> builtInFonts = EditorResources.builtInFonts;
    31.  
    32.         EditorResources.builtInFonts.Add(FONT, FONTPATH);
    33.  
    34.         supportedFonts.Add(FONT);
    35.  
    36.         supportedFontsField.SetValue(null, supportedFonts);
    37.  
    38.         foreach (var item in supportedFonts)
    39.             Debug.Log(item);
    40.  
    41.         EditorPrefs.SetString("user_editor_font", FONTPATH);
    42.     }
    43. }
    44.  
    upload_2019-7-16_16-43-6.png

    tried various approaches but it fails to read, or switch, im not sure, ill try again later.

    HAHA literally noticed a typo on the last line, now it loads
     
    GameDevCouple_I likes this.
  5. Peter77

    Peter77

    Joined:
    Jun 12, 2013
    Posts:
    4,036
    The example in the documentation sets the font like:
    Code (CSharp):
    1. :root {
    2.     -unity-font: resource("Font/consola.ttf");
    3. }
    Where the folder is "Font" rather than "Fonts". Perhaps this makes a difference?
     
  6. pointcache

    pointcache

    Joined:
    Sep 22, 2012
    Posts:
    525
    upload_2019-7-16_16-46-1.png

    Sadly whole ui is borked but at least font works

    It seems i broke it so hard it doesnt start anymore
     
    Last edited: Jul 16, 2019
    Lars-Steenhoff likes this.
  7. pointcache

    pointcache

    Joined:
    Sep 22, 2012
    Posts:
    525
    Nah it was
    EditorPrefs.SetString("user_editor_font", FONTPATH);
    instead of
    EditorPrefs.SetString("user_editor_font", FONT);
     
    GameDevCouple_I likes this.
  8. Lars-Steenhoff

    Lars-Steenhoff

    Joined:
    Aug 7, 2007
    Posts:
    2,100
    Nice work !
     
  9. pointcache

    pointcache

    Joined:
    Sep 22, 2012
    Posts:
    525
    upload_2019-7-16_17-23-12.png

    its working!

    Code (CSharp):
    1. using UnityEditor;
    2. using UnityEditor.Experimental;
    3. using UnityEngine;
    4. using System.Reflection;
    5. using System.Collections.Generic;
    6.  
    7. [InitializeOnLoad]
    8. public static class FontLoader
    9. {
    10.     private const string FONT = "Dejavu Sans Mono";
    11.     private const string FONTPATH = "Fonts/dejavu.ttf";
    12.  
    13.     static FontLoader()
    14.     {
    15.         var font = EditorResources.Load<Font>(FONTPATH) as Font;
    16.         var fontField = typeof(EditorResources).GetField("s_NormalFont", BindingFlags.Static | BindingFlags.NonPublic);
    17.         fontField.SetValue(null, font);
    18.      
    19.         GUISkin defaultSkin = GUIUtility.GetDefaultSkin();
    20.         defaultSkin.font = font;
    21.         EditorResources.UpdateGUIStyleProperties(defaultSkin);
    22.  
    23.     }
    24. }
    actually half of it was not necessary, you don't get the selector in menu, but it does its job /shrug
     
  10. Peter77

    Peter77

    Joined:
    Jun 12, 2013
    Posts:
    4,036
    Can you also just overwrite the font in the Console window, but keep the font of other UI untouched, for example? I always wanted a mono-space font for the console.
     
    Last edited: Jul 17, 2019
    ArthurT likes this.
  11. Grimreaper358

    Grimreaper358

    Joined:
    Apr 8, 2013
    Posts:
    551
    Managed to override almost every element. Everything that's still bright is using the light skin because they are using an icon/image as the background. Trying to override the icons with the Editor Default Resources folder isn't working. A fair amount of the UI uses these images, mainly the Profiler.

    upload_2019-7-16_18-17-2.png
     
  12. pointcache

    pointcache

    Joined:
    Sep 22, 2012
    Posts:
    525
    Can you share the files?
     

    Attached Files:

    Lars-Steenhoff likes this.
  13. pointcache

    pointcache

    Joined:
    Sep 22, 2012
    Posts:
    525
    I can't figure out a way to replace a single element font sadly. Actually i can't even change a lot of elements since none of the selectors i tried work, i wonder how @Grimreaper358 did it.

    It seems that a lot of uss properties get just flat out ignored. It seems there are inconsistencies between uss to imgui styles.
     
    Last edited: Jul 17, 2019
    Peter77 and Lars-Steenhoff like this.
  14. Grimreaper358

    Grimreaper358

    Joined:
    Apr 8, 2013
    Posts:
    551
    Yea, I was planning to do this after I got it to a decent state and would only have minor fixes to do.

    The last few major things I need to do is replace the icons and fix some broken windows that use a separate/their own theme. For the icons contributing to the light theme still, I will have to remake or wait and see if they will add the editor resources to GitHub and use/modify those. As for how I edit the UI, I just followed the steps from the other thread which you also linked "Basic how-to" (After a while I converted it a package, this way it's easily shared and updated)

    Also for some reason, Unity stopped auto-reloading the file so every change I make I have to close the editor and reopen which increases the iterating time a lot. Especially since some element names don't really do anything when you try to edit them (If anyone knows a fix let me know, also how to replace the icons. Followed the steps by the dev but it didn't work)

    Broken Windows - Not 100% sure how to fix so far

    Package Manager - Has it's own UI so I will have to see if I can override its properties in the same file
    upload_2019-7-17_7-50-57.png

    Light Explorer
    upload_2019-7-17_7-52-42.png

    Profiler - I changed the text color and It inherited its color label (Also light theme from icons)
    upload_2019-7-17_7-59-10.png
     
    Alverik, Mauri and Peter77 like this.
  15. Stormy102

    Stormy102

    Joined:
    Jan 17, 2014
    Posts:
    495
    That is beautiful :O
     
  16. Stardog

    Stardog

    Joined:
    Jun 28, 2010
    Posts:
    1,322
    What's the earliest version of 2019 that supports this? I can't open 2019.3.0a8 due to crashes.

    Edit: Working in a7.
     
    Last edited: Jul 19, 2019
  17. TheZombieKiller

    TheZombieKiller

    Joined:
    Feb 8, 2013
    Posts:
    43
    If you know the paths to the resources, you could rip them using EditorGUIUtility.Load, Instantiate and AssetDatabase.CreateAsset. Redistributing them wouldn't be necessary either since you could just distribute an editor script to rip them on the user's side. The resource paths can probably be found in the built-in .uss files, which can be obtained using my script linked in the OP.
     
  18. Grimreaper358

    Grimreaper358

    Joined:
    Apr 8, 2013
    Posts:
    551
    I was thinking of going back and looking at your script to see if I could get some information from it. Unfortunately, I don't really know much about coding so I'm kind of limited there. I'm not even 100% sure how to use the script you made so ripping the assets using the libraries? listed above is slightly out of my reach in terms of knowledge. I just know a bit of web development so it wasn't any problem restyling the editor so far.
     
  19. TheZombieKiller

    TheZombieKiller

    Joined:
    Feb 8, 2013
    Posts:
    43
    If you just need a simple step-by-step instruction on how to get it running:

    1. Make a new folder in your Assets directory, call it whatever you want
    2. In the editor, right click inside that folder and go Create -> Assembly Definition
    3. Name the assembly definiton Assembly-CSharp-Editor-testable
    4. Create a new script in the folder, call it StyleSheetExporter
    5. Open the script in a text editor, delete everything and paste my code in

    Upon a refresh, you should have a new "StyleSheet Exporter" dropdown in the editor.
    When you run it, it'll save all the files into your project directory, just outside the Assets folder.
     
    Grimreaper358 likes this.
  20. Grimreaper358

    Grimreaper358

    Joined:
    Apr 8, 2013
    Posts:
    551
    Ah, I tried it before but didn't notice the menu item for the dropdown so I deleted the assembly definition and the script. I'll try it again and see.
     
  21. Grimreaper358

    Grimreaper358

    Joined:
    Apr 8, 2013
    Posts:
    551
    @TheZombieKiller I got the stylesheets now. It seems like I'll be able to use a good amount of information from them. Thanks!
     
    TheZombieKiller likes this.
  22. Grimreaper358

    Grimreaper358

    Joined:
    Apr 8, 2013
    Posts:
    551
    Quick update. Still, a good amount of icons to update/replace but most of them are replaced now thanks to @TheZombieKiller script. They are still using the built-in icons so I didn't have to rip anything out just the script referencing the icons. Also, I still have the three windows previously posted to get to a decent state as well.

    upload_2019-7-19_1-52-33.png

    upload_2019-7-19_1-52-49.png
     
  23. Lars-Steenhoff

    Lars-Steenhoff

    Joined:
    Aug 7, 2007
    Posts:
    2,100
    What about this bright vertical line between the windows? is that also an image?
     
  24. GameDevCouple_I

    GameDevCouple_I

    Joined:
    Oct 5, 2013
    Posts:
    1,891
    Are you gonna stick this on git? If not, can you upload it to git so the community can help you maintain this + have access to it?
     
  25. TheZombieKiller

    TheZombieKiller

    Joined:
    Feb 8, 2013
    Posts:
    43
    Based on this, I assume that'll happen soon enough:
     
    GameDevCouple_I likes this.
  26. GameDevCouple_I

    GameDevCouple_I

    Joined:
    Oct 5, 2013
    Posts:
    1,891
    Brilliant must have missed that, thanks :)
     
  27. Stardog

    Stardog

    Joined:
    Jun 28, 2010
    Posts:
    1,322
    How do we style the weirdly named elements like 'IN Foldout' and 'TV Line'? How can we know what the most base button class is called? Are the play/pause buttons using this? From the generated css script they have a unique class like 'unity-toolbar-button'...

    Also, I'm assuming they haven't finished converting the editor to UIElements yet? A lot of things aren't working, such as when changing the height of .Toolbar nothing flows with it, also when changing the margin/padding on containers, everything inside just ignores it.

    It's like it's only 20% UIElements so far.
     
    Last edited: Jul 19, 2019
  28. Grimreaper358

    Grimreaper358

    Joined:
    Apr 8, 2013
    Posts:
    551
    Yes, that's what it seems like so far. Just an image stretched as a background/container.
    upload_2019-7-19_7-54-42.png
     
  29. Stardog

    Stardog

    Joined:
    Jun 28, 2010
    Posts:
    1,322
    Changing .dockarea background-color changes it anyway.
     
  30. Grimreaper358

    Grimreaper358

    Joined:
    Apr 8, 2013
    Posts:
    551
    Wasn't and still isn't changing for me. Wonder why that is.

    Whenever you see a name like this you just add a - instead of space so this would be

    .IN-Foldout
    .TV-Line
     
    Stardog likes this.
  31. Stardog

    Stardog

    Joined:
    Jun 28, 2010
    Posts:
    1,322
    Maybe it was the border instead. I'm sure it was just the background-color.
    Code (csharp):
    1. .dockarea {
    2.     border-top-width: 0;
    3.     border-right-width: 0;
    4.     border-bottom-width: 0;
    5.     border-left-width: 0;
    6. }
     
  32. Grimreaper358

    Grimreaper358

    Joined:
    Apr 8, 2013
    Posts:
    551

    Yea I tried it again yesterday and it worked. Actually, a few elements that weren't working before started working after, not sure what happened there but glad it's working now.

    Maybe it was because I had to switch from Asset pipeline V2 to V1 a few times. V2 stopped updating the script when you enter play mode/reload domain. After switching to V1 it worked again for a while then broke (after a while it stopped reading some of the changes I made to the file but others would work), so I was switching between both asset pipeline for a while until it stopped messing up on V1.
     
  33. Grimreaper358

    Grimreaper358

    Joined:
    Apr 8, 2013
    Posts:
    551
    Progress Update - Fixed a lot of small issues and replaced some icons. Currently, I can't replace a lot of icons as they are wrapped in generic containers and I don't have any way to directly access their direct class names to replace them.

    UI Elements aren't changing when I edit their values

    Package Manager - It's using a UI Elements.

    Lighting Explorer - There's still one element in it that I can't find the class dor to replace its color. This also affects other parts of the UI but not as much.

    Profiler - Made some progress here but there's still some weird behavior I need to sort out. It uses a lot of images/icons as background for its gradients
     
  34. Peter77

    Peter77

    Joined:
    Jun 12, 2013
    Posts:
    4,036
    Here is a different approach and perhaps it brings something useful to the table, I don't know. The following code finds all StyleSheets in memory and modifies their color. Then triggers a domain reload to make the changes visible.

    My idea was if I convert color to HSV space, it should be simple to make it dark or light. I simply invert the "Value", but you can probably add rules (aka if else) that describe to how much and what colors to actually modify.



    Code (CSharp):
    1. using System.Collections;
    2. using System.Collections.Generic;
    3. using UnityEngine;
    4. using UnityEditor;
    5. using UnityEditor.StyleSheets;
    6. using UnityEngine.UIElements;
    7.  
    8. public class TestCode
    9. {
    10.     [MenuItem("Theme/Patch")]
    11.     static void PatchTheme()
    12.     {
    13.         Undo.IncrementCurrentGroup();
    14.         Undo.SetCurrentGroupName("Patch Theme");
    15.  
    16.         // Find all StyleSheet's in memory
    17.         var styleSheets = Resources.FindObjectsOfTypeAll<StyleSheet>();
    18.         foreach(var styleSheet in styleSheets)
    19.         {
    20.             var serObj = new SerializedObject(styleSheet);
    21.             serObj.Update();
    22.  
    23.             // Iterate over all its colors
    24.             var colorsProperty = serObj.FindProperty("colors");
    25.             for (var n=0; n< colorsProperty.arraySize; ++n)
    26.             {
    27.                 var colorProp = colorsProperty.GetArrayElementAtIndex(n);
    28.                 var color = colorProp.colorValue;
    29.  
    30.                 float h, s, v;
    31.                 Color.RGBToHSV(color, out h, out s, out v);
    32.  
    33.                 // Here we modify the color
    34.                 // invert value
    35.                 v = 1 - v;
    36.  
    37.                 color = Color.HSVToRGB(h, s, v);
    38.  
    39.                 // Write back modified color
    40.                 colorProp.colorValue = color;
    41.             }
    42.  
    43.             // Apply the modified colors to the styleSheet
    44.             serObj.ApplyModifiedProperties();
    45.         }
    46.  
    47.         // Reload scripts to make the change visible
    48.         UnityEditorInternal.InternalEditorUtility.RequestScriptReload();
    49.         UnityEditorInternal.InternalEditorUtility.RepaintAllViews();
    50.     }
    51.  
    52.     [MenuItem("Theme/Undo")]
    53.     static void UndoTheme()
    54.     {
    55.         EditorApplication.ExecuteMenuItem("Edit/Undo");
    56.         UnityEditorInternal.InternalEditorUtility.RequestScriptReload();
    57.     }
    58. }
    59.  
     
  35. Peter77

    Peter77

    Joined:
    Jun 12, 2013
    Posts:
    4,036
    BTW, if Light and Dark themes are actually available and loaded to memory, it should be possible to just copy over the entire theme ;)

    Something like:
    Code (CSharp):
    1. StyleSheet dark = FindDarkTheme();
    2. StyleSheet light = FindLightTheme();
    3.  
    4. EditorUtility.CopySerialized(dark, light);
     
    Last edited: Jul 24, 2019
  36. Grimreaper358

    Grimreaper358

    Joined:
    Apr 8, 2013
    Posts:
    551
    This would be really useful to have light or dark theme
     
    Alverik likes this.
  37. TheZombieKiller

    TheZombieKiller

    Joined:
    Feb 8, 2013
    Posts:
    43
    This actually got me thinking, and I played around a bit more. I managed to find the actual dark theme stylesheet, it's simply called "dark.uss", and can be ripped by adding it to the list of stylesheets in my code.

    EDIT: "SettingsWindowDark.uss" is another useful one

    upload_2019-7-24_4-18-26.png
     
    Last edited: Jul 24, 2019
    Grimreaper358 and Peter77 like this.
  38. Grimreaper358

    Grimreaper358

    Joined:
    Apr 8, 2013
    Posts:
    551
    Sweet, gonna give it a go. Should have thought of this as the initial info we got the dev listed dark.uss and light.uss as the files we would edit in the editor folder.
     
  39. TheZombieKiller

    TheZombieKiller

    Joined:
    Feb 8, 2013
    Posts:
    43
    Yeah, it feels so obvious now
     
  40. Grimreaper358

    Grimreaper358

    Joined:
    Apr 8, 2013
    Posts:
    551
    @TheZombieKiller I'm now using 2019.3 a10 and trying to export with the Stylesheet Exporter is giving an error. Also does the dark.uss file contains links to the dark theme icons?

    upload_2019-7-24_2-28-6.png
     
  41. TheZombieKiller

    TheZombieKiller

    Joined:
    Feb 8, 2013
    Posts:
    43
    What changes did you make to add it to the list? It should just be "dark.uss" and not "StyleSheets/dark.uss" or anything like that
     
  42. Grimreaper358

    Grimreaper358

    Joined:
    Apr 8, 2013
    Posts:
    551
    It just won't work, even without adding the new CSS files to it.
    It just shows that error every time I go to try and export.
    Might have to install 2019.3 a8 again.
     
  43. TheZombieKiller

    TheZombieKiller

    Joined:
    Feb 8, 2013
    Posts:
    43
    I'm installing 2019.3a10 currently, I'll give it a try and report back
     
    Grimreaper358 likes this.
  44. TheZombieKiller

    TheZombieKiller

    Joined:
    Feb 8, 2013
    Posts:
    43
    Okay, bear with me because I'm trying not to redistribute any code (because of the UnityCsReference license, among other things).

    The first thing you need to do is save this script in your project:

    https://github.com/Unity-Technologi...yleSheetsEditor/Converters/StyleSheetToUss.cs

    Then find the following line:

    Code (CSharp):
    1. case StyleValueType.Keyword:
    And add this line above it:

    Code (CSharp):
    1. case StyleValueType.Variable:
    Then at the top of the file, change "namespace UnityEditor.StyleSheets" to something like "namespace ThemeExporter.StyleSheets". Then in my script, change "using UnityEditor.StyleSheets;" to "using ThemeExporter.StyleSheets;".

    The export should then succeed.
     
    Grimreaper358 likes this.
  45. Grimreaper358

    Grimreaper358

    Joined:
    Apr 8, 2013
    Posts:
    551
    There's nothing else after this?
    Code (CSharp):
    1. case StyleValueType.Variable:
    Did all the changes but it seems like this part needs an extra line underneath like the rest do. So for me, it won't compile
    upload_2019-7-24_11-24-7.png

    That might not be what's causing the error as it's getting a lot of protection level lock
    upload_2019-7-24_11-40-35.png
     
    Last edited: Jul 24, 2019
  46. Grimreaper358

    Grimreaper358

    Joined:
    Apr 8, 2013
    Posts:
    551
    @TheZombieKiller I Messed around and found that you have to have all the files at the same spot for it to work. Thanks for the info and help as always.

    The only problem now is that I get an Empty Path error for the new stylesheets

    I just added the new paths to it
    upload_2019-7-24_15-3-21.png
     
    Last edited: Jul 24, 2019
  47. Stardog

    Stardog

    Joined:
    Jun 28, 2010
    Posts:
    1,322
    This is about as far as I can be bothered to go with the current info about class names.

    Capture.PNG

    I was thinking I might try to make a skin editor using UIElements. It would just list all of the known classes for various parts of the interface and let you add some CSS to them by clicking, then it would generate the USS file for you. It could also let you store colours as reusable variables. A bit like Webflow but much simpler (image is Webflow):
    Capture.PNG
     
    TheZombieKiller likes this.
  48. TheZombieKiller

    TheZombieKiller

    Joined:
    Feb 8, 2013
    Posts:
    43
    Try changing
    Code (CSharp):
    1. Directory.CreateDirectory(folder);
    To
    Code (CSharp):
    1.  
    2. if (!string.IsNullOrWhiteSpace(folder))
    3.     Directory.CreateDirectory(folder);
     
    Grimreaper358 likes this.
  49. Grimreaper358

    Grimreaper358

    Joined:
    Apr 8, 2013
    Posts:
    551
    Sweet! That worked thanks!
     
    TheZombieKiller likes this.
  50. TheZombieKiller

    TheZombieKiller

    Joined:
    Feb 8, 2013
    Posts:
    43
    Took @Peter77's concept a little further and wrote a script that tries to locate dark versions of light-theme stylesheets and copy the information over. It'll invert the colours if it can't find a dark version.

    Code (CSharp):
    1.  
    2. using System;
    3. using System.Text.RegularExpressions;
    4. using UnityEngine;
    5. using UnityEngine.UIElements;
    6. using UnityEditor;
    7. using UnityEditorInternal;
    8.  
    9. public static class DarkTheme
    10. {
    11.     [MenuItem("Theme/Init Dark")]
    12.     static void Init()
    13.     {
    14.         foreach (var sheet in Resources.FindObjectsOfTypeAll<StyleSheet>())
    15.         {
    16.             if (ContainsInsensitive(sheet.name, "Dark"))
    17.                 continue;
    18.  
    19.             if (!ContainsInsensitive(sheet.name, "Light"))
    20.             {
    21.                 InvertColors(sheet);
    22.                 continue;
    23.             }
    24.  
    25.             var dark = null as StyleSheet;
    26.             var path = ReplaceInsensitive(AssetDatabase.GetAssetPath(sheet), "Light", "Dark");
    27.             var name = ReplaceInsensitive(sheet.name, "Light", "Dark");
    28.  
    29.             if (path == "Library/unity editor resources")
    30.                 dark = EditorGUIUtility.Load(name) as StyleSheet;
    31.             else
    32.                 dark = AssetDatabase.LoadAssetAtPath<StyleSheet>(path);
    33.  
    34.             if (!dark)
    35.                 InvertColors(sheet);
    36.             else
    37.             {
    38.                 string oldName = sheet.name;
    39.                 EditorUtility.CopySerialized(dark, sheet);
    40.                 sheet.name = oldName;
    41.             }
    42.         }
    43.  
    44.         InternalEditorUtility.RequestScriptReload();
    45.         InternalEditorUtility.RepaintAllViews();
    46.     }
    47.  
    48.     static void InvertColors(StyleSheet sheet)
    49.     {
    50.         var serialized = new SerializedObject(sheet); serialized.Update();
    51.         var colors     = serialized.FindProperty("colors");
    52.      
    53.         for (int i = 0; i < colors.arraySize; i++)
    54.         {
    55.             var property = colors.GetArrayElementAtIndex(i);
    56.             Color.RGBToHSV(property.colorValue, out var h, out var s, out var v);
    57.             property.colorValue = Color.HSVToRGB(h, s, 1 - v);
    58.         }
    59.  
    60.         serialized.ApplyModifiedProperties();
    61.     }
    62.  
    63.     static string ReplaceInsensitive(string str, string oldValue, string newValue)
    64.     {
    65.         return Regex.Replace(str, Regex.Escape(oldValue), newValue.Replace("$", "$$"), RegexOptions.IgnoreCase);
    66.     }
    67.  
    68.     static bool ContainsInsensitive(string str, string find)
    69.     {
    70.         return str.IndexOf(find, StringComparison.OrdinalIgnoreCase) != -1;
    71.     }
    72. }