Search Unity

  1. Megacity Metro Demo now available. Download now.
    Dismiss Notice
  2. Unity support for visionOS is now available. Learn more in our blog post.
    Dismiss Notice

[Open Source] VFW (135): Drawers. Save System and full exposure

Discussion in 'Assets and Asset Store' started by vexe, Sep 2, 2014.

  1. vexe

    vexe

    Joined:
    May 18, 2013
    Posts:
    644
    Latest version: 1.3.5

    Github repository: https://github.com/vexe/VFW

    Notes:
    - When updating to a new major version, it's better if you remove the current version you have and download the newer one
    - Please raise any issues/bugs you have via the issues section in github that way other users can see if an issue they're having is already raised or not.

    _______________
    -- 1.3.5 --


    - Fixed errors on web player
    - Fixed Unity 5.3 errors
    - Deprecated BetterBehaviour/BetterScriptableObject and custom serialization. Use Unity's standard serialization.
    - Deprecated uDelegate, uAction, uFunc, etc. Use UnityEvents instead.
    - Deprecated BetterPrefs. There are no more editor .asset files. Visit the actual VFWSettings.cs file to modify the stuff that's in it. Foldout values are now saved as part of BaseBehaviour (in an EditorRecord object) (Editor only, no runtime overhead)
    - "Drag drop" area shows when an array/list is empty as well
     
    Last edited: Nov 26, 2018
    hawkyjl, bitinn, Black_Demon and 30 others like this.
  2. vexe

    vexe

    Joined:
    May 18, 2013
    Posts:
    644
    [Edit the implementation in this answer is kind of outdated] I wrote a sample implementation (very basic) explaining how I went about BinaryFormatter serialization here.
     
    Last edited: Oct 7, 2014
  3. AlwaysSunny

    AlwaysSunny

    Joined:
    Sep 15, 2011
    Posts:
    260
    Incredible. You're a kitbash rockstar, vexe. I've tiptoed down this road before, but you've tarred, graded, painted, and put in sidewalks. Truly a wonderful contribution to the community. Haven't dug in to do any testing, but I'll be back with bells on. I can't wait to get my greedy hands on exposed (drawn) interfaces - the lack thereof has completely changed the way I approach programming. Please, for the love of C#, don't quit now! If you're ever in town I'm skipping the handshake and giving you a great big overzealous probably-awkward hug.
     
    ModLunar, vexe, Ghopper21 and 5 others like this.
  4. vexe

    vexe

    Joined:
    May 18, 2013
    Posts:
    644
    @AlwaysSunny thanks for the 'extremely' kind words! made me smile :) - I like the hug idea, but I'm pretty far :(
    I'm working on an interface drawer as we speak. It's a bit tricky, cause all the drawers I wrote so far work well with SerializedProperties and UnityObjects, not with raw System.Objects, so there's many adjustments to be made to the drawers. Also to the serialization logic, if you look at the code, you'll see I dis-include generics, abstracts etc (when I modeled the code for BetterBehaviour, I did not have interfaces/generics, etc in mind) - If I change the serialization logic, I'd have to have drawers for each new thing I support.
     
    Last edited: Oct 7, 2014
  5. sicga123

    sicga123

    Joined:
    Jan 26, 2011
    Posts:
    782
    Thanks, this is really generous.
     
  6. gear

    gear

    Joined:
    May 11, 2009
    Posts:
    35
    Hi Vexe,
    I am having issue with 4.5.3f4. When i run the Serialization scene, i get the following exception

    Code (CSharp):
    1. NullReferenceException: (null)
    2. UnityEditor.SerializedObject..ctor (UnityEngine.Object[] objs) (at C:/BuildAgent/work/d63dfc6385190b60/artifacts/EditorGenerated/SerializedPropertyBindings.cs:72)
    3. UnityEditor.Editor.GetSerializedObjectInternal () (at C:/BuildAgent/work/d63dfc6385190b60/artifacts/EditorGenerated/EditorBindings.cs:124)
    4. UnityEditor.Editor.get_serializedObject () (at C:/BuildAgent/work/d63dfc6385190b60/artifacts/EditorGenerated/EditorBindings.cs:117)
    5. UnityEditor.AnimationEditor.OnEnable () (at C:/BuildAgent/work/d63dfc6385190b60/Editor/Mono/Inspector/AnimationEditor.cs:20)
    6.  
    Can you please point to direction, so if possible i can fix it? Thanks for such wonderfull package.

    Thanks
     
  7. vexe

    vexe

    Joined:
    May 18, 2013
    Posts:
    644
    Hey @gear thanks for dropping by. So I get this sort of errors too very randomly (not just in SEA but just randomly in general...). Like you see from the stacktrace, it's not coming from any of my scripts. It's coming from Unity's code. These types of errors always (at least in my experience) resolve themselves when you restart Unity, try that. If that didn't fix it, let me know
     
  8. gear

    gear

    Joined:
    May 11, 2009
    Posts:
    35
    Hello @vexe,
    Well i restarted Unity but if didnt work. Thing is that its just give random exception like you mention. I am trying to import in new project.

    Thanks
     
  9. vexe

    vexe

    Joined:
    May 18, 2013
    Posts:
    644
    @gear I just downloaded the package and imported, didn't get any errors. Using the latest Unity (4.5.4f1) - pretty sure it shouldn't make a difference if you were on 4.5.3 since that's the version I used to release the current build. Maybe consider upgrading anyways?
     
  10. gear

    gear

    Joined:
    May 11, 2009
    Posts:
    35
    @vexe, it work fine importing in fresh project. Sorry for wrong report!

    Thanks
     
  11. vexe

    vexe

    Joined:
    May 18, 2013
    Posts:
    644
    Guys, I've released my new API. ShowEmAll is no more, Vexe Framework 1.0 is here! I rewrote the thread and removed some of my old redundant replies. Check out the new stuff!
     
    clunk47 likes this.
  12. sysameca

    sysameca

    Joined:
    Mar 2, 2013
    Posts:
    99
    This looks very promising. Maybe Unity should hire you to fix the overall state of the inspector. I will give it a try soon as i finish the 1000 tasks i have to do and will leave you a comment :)
     
    rakkarage likes this.
  13. AlwaysSunny

    AlwaysSunny

    Joined:
    Sep 15, 2011
    Posts:
    260
    Well well, you've been busy! I really can't wait to dive in and play with all these features. It's incredibly generous of you to offer this package free of charge; it's obvious you've put in a ton of work. It deserves more recognition - I hope lots of folks will discover and make use of it as time goes by.
     
  14. Radivarig

    Radivarig

    Joined:
    May 15, 2013
    Posts:
    120
    WOW!! humongous thanks!
     
  15. vexe

    vexe

    Joined:
    May 18, 2013
    Posts:
    644
    Thank you all for the kind words! - Luckily I got some contract jobs so I could test my framework in the real world. I've come along a few bugs so make sure you update. The only thing that I found a bit annoying was the editor performance, it's laggy if there's lots of things going on (lists with many elements, too many components with many categories, etc), I got my hands on a Unity pro license so I could finally profile now. I could probably assume that a lot of the performance hits are due to GUILayout and maybe because I use too much closures and LINQ (maybe Slinq could help here...) - memoizaiton would definitely help. But can't make any real assumption till I profile. Will do as soon as I can. Let me guys know of your experiences too, I'd love to hear from you!
     
    Last edited: Oct 10, 2014
  16. vexe

    vexe

    Joined:
    May 18, 2013
    Posts:
    644
    So if any of you have downloaded and used the package you might have noticed the editor is slow... This is mainly due to hits on the GC, now that I have access to Unity's profiler, I managed to improve performance. Garbage is halved, and performance is about 30% to 40% better. Did many things to achieve that, like memoization/smart caching, minimizing allocations, and replacing my GUI API to just use normal methods instead of delegates, removing closures helped reduce garbage.

    I know that Unity's GC is from the Flintstones, but I didn't expect it to hit performance that hard... I'm glad I profiled now and not latter. I'm still not happy though, there's still some ridiculous amount of garbage generated for fairly simple stuff... I kept thinking that it's coming from me, but I just didn't see how and where... I wish I had a better machine to enable Deep profiling, it literally dies when I do that... but I had to do it.. I then see that most of this garbage and performance hits are from Unity's GUILayout business... So yeah... I have to go back to the GUIWrapper I used in uFAction which is *much* faster than GUILayout... GUILayout is a no go, and neither is dealing with rects and positions manually via just GUI. We NEED a faster/better auto-layedout GUI system. Will see what I can hack, gotta get back to work though, damn money.
     
    twobob likes this.
  17. vexe

    vexe

    Joined:
    May 18, 2013
    Posts:
    644
    So I didn't get a chance yet to try out BetterPrefs in an actual game, but I think it's interesting. First create a BetterPrefs asset by going Tools | Vexe | BetterPrefs | CreateAsset. The asset will be created under Vexe/Runtime/Examples/ScriptableAssets. Here's a sample code of what you can do with BP:

    Code (csharp):
    1.  
    2.   public class BetterPrefsExample : BetterBehaviour
    3.    {
    4.      public BetterPrefs prefs; // assign the asset you created to this field
    5.  
    6.      [Show] void SaveVectorsToPrefs()
    7.      {
    8.        // save position, euler angles and local scale to prefs
    9.        prefs.Vector3s["MyPosition"] = position;
    10.        prefs.Vector3s["MyRotation"] = localEulerAngles;
    11.        prefs.Vector3s["MyScale"]  = localScale;
    12.      }
    13.  
    14.      [Show] void ReadVectorsFromPrefsAndWriteToFile()
    15.      {
    16.        // read values...
    17.        var pos = prefs.Vector3s["MyPosition"];
    18.        var rot = prefs.Vector3s["MyRotation"];
    19.        var scl = prefs.Vector3s["MyScale"];
    20.  
    21.        // log them
    22.        Log("Position: {0} - Rotation {1} - Scale {2}", pos, rot, scl);
    23.  
    24.        // maybe serialize and write them to a file?
    25.        string serializedPos = Serializer.Serialize<Vector3>(pos);
    26.        string serializedRot = Serializer.Serialize<Vector3>(rot);
    27.        string serializedScl = Serializer.Serialize<Vector3>(scl);
    28.        using (var file = File.Open("Assets/Vexe/Runtime/Examples/Assets/Sample.data", FileMode.OpenOrCreate))
    29.        {
    30.          using (var writer = new StreamWriter(file))
    31.          {
    32.            writer.WriteLine(serializedPos);
    33.            writer.WriteLine(serializedRot);
    34.            writer.WriteLine(serializedScl);
    35.          }
    36.        }
    37.      }
    38.    }
    39.  


    Using FullSerializer here we get that nice JSON output. So yeah there you go, BetterPrefs, quick way to save data. it's simple now I may add some helper methods to it to quickly write/read to/from files using the current BetterBehaviour serializer.

    Of course you could just serialize the whole dictionary as well, and then write that to a file:
    Code (csharp):
    1.  
    2. string serializedDictionary = Serializer.Serialize(prefs.Vector3s);
    3. ...
    4. writer.WriteLine(serializedDictionary);
    5.  
    With an object marked with DontDestroyOnLoad and has a BetterPrefs instance, BetterPrefs could also be used to pass data between scenes. Which is part of why I created it. PlayerPrefs is nice, but it's limited and uses the registry. I wanted something local, and simple
     
    Last edited: Oct 15, 2014
  18. vexe

    vexe

    Joined:
    May 18, 2013
    Posts:
    644
    1.0.4 is here. Added some missing attributes and a way to define default values. Also changed the project's folder hierarchy to make use of the Plugins and Plugins/Editor folder which improved compilation times when an assembly reload is triggered from a compilation pass/group other than the first one (see)

    For anyone that's downloaded my package, I'd love to hear your experience so far!
     
    Last edited: Oct 20, 2014
  19. vangojames

    vangojames

    Joined:
    Oct 31, 2014
    Posts:
    8
    Hey Vexe,

    This Framework looks awesome, but I am unclear of how to extend it. I have a generic type MyType<T> and I would like to implement a custom drawer for that (and get it serializing correctly). It is a member of a monobehaviour that I have created, however I would like to use this custom drawer in the editor whenever there is a member of that type on any object. What are the basic steps to go through to get that working?

    Thanks,

    James
     
    baguwka likes this.
  20. vexe

    vexe

    Joined:
    May 18, 2013
    Posts:
    644
    Hey @vangojames,

    Thanks for dropping by, apologies for the confusion, but I didn't have the time to explain/make a tutorial on the exact part you're mentioning. It is very straight forward though:

    Code (csharp):
    1.  
    2. public MyObjectDrawer<T> : ObjectDrawer<MyType<T>>
    3. {
    4.    public override void OnGUI()
    5.    {
    6.         // ...
    7.    }
    8. }
    9.  
    Basically, you just need to keep in mind, there are 2 types of drawers you could use: 1- Object Drawers, 2- Composite Drawers. Object drawers split to two types: ObjectDrawer<T> and AttributeDrawer<TObject, TAttribute>

    Object drawers define how an object is drawn, composite drawers just add decorations and they shouldn't define/mess with how an object is represented. With Object drawers you get an OnGUI call to override, there, you write your GUI code. In Composite drawers however; you get OnLeftGUI, OnRightGUI, OnLowerGUI and OnUpperGUI that way you can selectively target which part of your object you want to decorate. See WhitespaceAttributeDrawer as an example.

    1- If you have a certain Type T and you want to write a drawer for it such that wherever an object of that type is mentioned, your drawer will be used; you should use ObjectDrawer<T> (ex DictionaryDrawer, ArrayDrawer, ListDrawer, etc)

    2- If you have a certain Type TObject and you want to write a drawer for it such that wherever an object of that type is mentioned AND it is decorated with an attribute of type TAttribute, your drawer will be used; you should use AttributeDrawer (ex ShowTypeAttributeDrawer) (NOTE: TAttribute must inherit DrawnAttribute)

    3- Now for composite drawers, you need to specify the type of object you're targeting, and the type of attribute that way if you annotate where you shouldn't, it just gets ignored and things won't blow up. Say you wanted to write a composite drawer for strings, and for an attribute of type MyAttribute. (NOTE: MyAttribute should inherit CompositeAttribute)

    Code (csharp):
    1.  
    2. public class MyAttributeDrawer : CompositeDrawer<string, MyAttribute>
    3. {
    4.     // override whatever OnXXXGUI you want...
    5. }
    6.  

    4- Last thing you need to keep in mind, when you use any type of drawer, you will get a strongly typed reference to what you're targeting; that reference is dataMember; it's basically a wrapper around fields/properties, it's what lets you treat both equally so you don't have to worry about whether you're targeting a field/property (think of it as a clean SerializedProperty on steroids). dataMember.Value is the value of the object you're targetting (strongly typed), you could just use dmValue for short. If you use CompoiteDrawer or AttributeDrawer, you also get a strongly typed reference to the attribute you're targeting; that is "attribute"

    Example usages of the drawing API is scattered all around the code base. Pretty much every drawer for every single attribute/object in the framework, uses this drawing API so if you just peek a little bit around you'll get a decent understanding.

    For serialization, it depends on what serializer you're using, if you're using BinaryFormatter you have to mark your type with [Serializable], if you're using Protobuf you have to mark it with [ProtoContract] and each member you want to serialize with [ProtoMember(id)] etc. If you're using FullSerializer you don't need to do anything! I recommend you stick to FullSerializer, it's the default serializer anyways. Watch the serialization video if you haven't already, should cover most questions.

    Let me know if you have more questions or if you get stuck somewhere
     
    Last edited: Oct 31, 2014
    sGlorz likes this.
  21. vangojames

    vangojames

    Joined:
    Oct 31, 2014
    Posts:
    8
    Great response thanks very much! Does my custom type have to extend BetterScriptableObject or be a member of BetterBehaviour in order to serialize and use the drawer, or can it just simply extend a standard object and be a member of a MonoBehaviour?

    Thanks,

    James
     
  22. vexe

    vexe

    Joined:
    May 18, 2013
    Posts:
    644
    @vangojames if you want a MonoBehaviour, extend BetterBehaviour, if you want a ScriptableObject, extend BetterSciptableObject. In previous versions of this framework (used to be ShowEmAll) most the attributes worked fine with MonoBehaviours, only some where BetterBehaviour-exclusive. Now everything is BetterBehaviour-exclusive, meaning the attributes don't have any affect if you apply them on members 'directly' in a MonoBehaviour, you have to use BetterBehaviour.

    [Edit] Your custom type could be anything (any System.Type) - I was talking about Unity behaviours.

    So:

    Code (csharp):
    1.  
    2. public class MyType<T>
    3. {
    4.     public T MyValue { get; set; }
    5. }
    6.  
    7. public class MyBehaviour : BetterBehaviour
    8. {
    9.     public MyType<int> myInt;
    10. }
    11.  
    12. public class MyTypeDrawer<T> : ObjectDrawer<MyType<T>>
    13. {
    14.    public override void OnGUI() { ... }
    15. }
    16.  
    FYI you don't need to write a custom drawer for new types, even if they were generic, there's always at least one drawer that's used for a certain type; a fallback drawer I call RecursiveDrawer<T> (used to draw interfaces, abstract system.objects and structs) - so if there's nothing special about the way your object is drawn, you could just let that drawer draw your object.
     
    Last edited: Nov 2, 2014
  23. vangojames

    vangojames

    Joined:
    Oct 31, 2014
    Posts:
    8
    Thanks for that it's very useful! Weirdly, after doing this I don't appear to get any callbacks in OnGUI(). The only difference I can see to the example you gave is that I have constraints on T (where T : struct, IConvertible). The drawer I have created doesn't appear to be getting used at all.
     
  24. vexe

    vexe

    Joined:
    May 18, 2013
    Posts:
    644
    What you mean "don't get any callbacks in OnGUI()"? - could you post what you have so far?
     
  25. vangojames

    vangojames

    Joined:
    Oct 31, 2014
    Posts:
    8
    Looks like the reason I am not getting any callbacks is because MyTypeDrawer<T> is not showing in the assembly. In the constructor of MemberDrawersHandler there seems to be a call to cache out all the object drawer types in the executing assembly. When this gets called my type is not in the list. Maybe it isn't being compiled into the assembly.
    Code (CSharp):
    1. Type[] drawerTypes = Assembly.GetExecutingAssembly().GetTypes()
    2.                                                   .Where(t => t.IsA<BaseDrawer>())
    3.                                                   .Where(t => !t.IsAbstract)
    4.                                                   .ToArray();
    I did notice that MemberDrawersHandler is a singleton and so the constructor will only be called once, so I think this list won't get refreshed without a restart of unity, unless the static instance get cleared out somehow when new scripts are added and the dll gets reloaded. I tried that, but my type is still not in the list.
     
  26. vangojames

    vangojames

    Joined:
    Oct 31, 2014
    Posts:
    8
    BTW this codebase is really elegantly written! Good work!
     
  27. vexe

    vexe

    Joined:
    May 18, 2013
    Posts:
    644
    That is what happens, everything should get cleared out after an assembly reload. Even static stuff. Btw you could explicitly clear the drawers cache, if I still have that menu item, somewhere in Tools | Vexe | BetterBehaviour | ClearDrawerCache. The code for the MenuItem should be in MemberDrawersHandler

    I think I know why you drawer isn't being spotted, try and put your drawer under Plugins/Editor. I only recently reorganized my folder structures to go under Plugins. I seem to have totally forgotten about the fact that files under it will be compiled in a different assembly. Will fix. In the meantime, you could either try what I just mentioned or just concat your assembly types.

    Code (csharp):
    1.  
    2. Type[] myTypes = typeof(SomeTypeThatsInYourAsm).Assembly.GetTypes();
    3.  
    4. Type[] drawerTypes = Assembly.GetExecutingAssembly().GetTypes().Concat(myTypes)....
    5.  
    Thanks for bringing that up and taking the time to investigate, appreciate it!

    [Edit] - I don't think you can concatenate your assembly types cause your asm gets compiled 'after' the one that contains the Plugins/Editor files so it won't be visible. For now, write your drawers in Plugins/Editor if that works and I'll see how this can be better handled.
     
    Last edited: Nov 3, 2014
  28. vangojames

    vangojames

    Joined:
    Oct 31, 2014
    Posts:
    8
    Hey I got it working by just iterating over the full list of assemblies in use in the current app domain :

    Code (CSharp):
    1. Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();
    2.             Type[] drawerTypes = new Type[0];
    3.  
    4. foreach(Assembly assembly in assemblies)
    5. {
    6.                drawerTypes = drawerTypes.Concat(assembly.GetTypes()
    7.                          .Where(t => t.IsA<BaseDrawer>())
    8.                          .Where(t => !t.IsAbstract)
    9.                          .ToArray()).ToArray();
    10.  
    11. }
     
  29. vangojames

    vangojames

    Joined:
    Oct 31, 2014
    Posts:
    8
    Actually I now get callbacks but the data member's value is null. Trying to figure out why but for some reason I can't get to the implementation for data member and so I can't see where it is going wrong while stepping through. Is it referenced in an external dll? I also tried putting the code in the plugins folder but I still get the same problem.
     
  30. vexe

    vexe

    Joined:
    May 18, 2013
    Posts:
    644
    It will be null if you haven't done anything with it yet... I check for the object's value to make sure it's not null in almost all my drawers. So for ex if you write a composite drawer that targets strings, you might wanna do something like:

    Code (csharp):
    1.  
    2. public override void OnInitialized()
    3. {
    4.     if (dmValue == null)
    5.        dmValue = string.Empty;
    6. }
    7.  
    Similar thing you'll see in Dictionary/Sequence drawers when reading the member's value:

    Code (csharp):
    1.  
    2. // DictionaryDrawer.cs
    3. dictionary = dmValue == null ? new KVPList<TKey, TValue>() : dmValue.ToKVPList();
    4.  
    5. // SequenceDrawer.cs
    6. if (dmValue == null)
    7.       dmValue = GetNew(); // returns new List<T>() in case of ListDrawer, and new T[0] in ArrayDrawer
    8.  
    You should be able to step into DataMember stuff, there's debug symbols for all my dlls. DataMember is now in the Fasterflect assembly. I will move it out though in the next update. Apologies for the frustration.
     
  31. vangojames

    vangojames

    Joined:
    Oct 31, 2014
    Posts:
    8
    Nice works perfectly now, thanks!
     
  32. vexe

    vexe

    Joined:
    May 18, 2013
    Posts:
    644
    Glad to hear.

    1.0.5 is out. Managed to fix field intializers where previously they were ignored so you couldn't assign default values to fields, so you had to use the DefaultAttribute, not anymore. I'm keeping that attribute around though cause you could use it to give default values to properties.

    Another thing I found useful was to change GameEvent from an abstract class to an interface. Been doing some mobile dev lately and found it quite convenient to have my GameEvents (the simple ones) as memory-friendly allocation-free structs.

    I'm currently working on a faster and better GUI layouting system that will hopefully improve the editor experience, an implementation that's faster than my GUIWrapper used in uFaction (which as at least 2x faster than GUILayout). I'm not so sure why Unity's GUILayout is so horribly slow... there's really not that much to automatic layouting... it's really simple.

    I'm also working on a Save/Load solution cause tbh the only solution I found worthy was Mike Talbot's Unity serializer. It's very good, but the code base is very hard to follow and understand... I'm trying to implement something useful, practical, simpler and easy to understand and use
     
    vangojames likes this.
  33. buggle52

    buggle52

    Joined:
    Nov 17, 2013
    Posts:
    5
    I know I'm being really dumb, but here goes. I'm trying to expose a property that, in the setter, creates a GameObject. The issue is, when you set the value in design mode, it doesn't just create one GameObject, it creates loads, which obviously means it's being called multiple times even in design mode (yes I could clean up, but I'd rather understand the underlying issue). Any ideas? Many thanks in advance.

    Code (CSharp):
    1.     [Serialize, HideInInspector]
    2.     Turret _turret;
    3.  
    4.     [Show]
    5.     public Turret MyTurret
    6.     {
    7.         get { return _turret; }
    8.         set {
    9.             _turret = value;
    10.             Instantiate(_turret,turretMount.transform.position, turretMount.transform.rotation);
    11.         }
    12.     }
     
  34. vexe

    vexe

    Joined:
    May 18, 2013
    Posts:
    644
    @buggle52 fixed in 1.0.6 - the issue was that the property was being set rapidly even if its value hasn't changed. I fixed it by making sure I only set the property if the value we're setting to is a new one. Check out Vexe/Runtime/Types/Others/DataMember.cs - This is the wrapper I use in my drawers to wrap fields/properties. This is where all the set/get operations are done when you modify something in the inspector.

    Note that I'd check to make sure the turret is not null before instantiating:

    Code (csharp):
    1.  
    2.   [Show]
    3.   public Turret MyTurret
    4.   {
    5.       get { return _turret; }
    6.       set
    7.       {
    8.              if (_turret == value) return;
    9.              _turret = value;
    10.              if (value != null)
    11.                   Instantiate(value, turretMount.transform.position, turretMount.transform.rotation);
    12.        }
    13.   }
    14.  
    Btw you could just use Hide instead of HideInInspector
     
    Last edited: Nov 6, 2014
  35. buggle52

    buggle52

    Joined:
    Nov 17, 2013
    Posts:
    5
    Thanks for the quick response! I'll try it out tomorrow.
     
  36. vexe

    vexe

    Joined:
    May 18, 2013
    Posts:
    644
    Finally got time to work on the new GUI. It's now released in VFW 1.1 by the name of RabbitGUI: a much faster and cleaner GUI Layouting system than Unity's GUILayout (which I still support, under the name of TurtleGUI :p)

    RabbitGUI is designed with performance and ease of use in mind. The API is very similar to Unity's GUI with a few changes/simplifications here and there.

    Here's some API differences. With Unity GUI, you have to:

    Code (csharp):
    1.  
    2. GUILayout.BeginHorizontal();
    3. {
    4.     // stuff...
    5.     GUILayout.BeginVertical();
    6.     {
    7.         // stuff...
    8.     }
    9.     GUILayout.EndVertical();
    10. }
    11. GUILayout.EndHorizontal();
    12.  
    I don't like this for many reasons:
    1- Not sexy, and too cumbersome to type.
    2- Very error prone. If you do a return inside of a block without ending it, you'll get errors.
    3- One might forget to end a block.

    With Rabbit, all those problems don't exist. I take advantage of IDisposable, so you:
    Code (csharp):
    1.  
    2. using (gui.Horizontal())
    3. {
    4.     using (gui.Vertical())
    5.     {
    6.          // stuff...
    7.     }
    8. }
    9.  
    The block will get disposed even if you return. It's a lot less keystrokes to type, and it looks much nicer.

    More tedious blocks are colors, state, enabled, etc. In Unity you have to:

    Code (csharp):
    1.  
    2. var prevColor = GUI.color;
    3. GUI.color = someColor;
    4. // code...
    5. GUI.color = prevColor;
    6.  
    7. var prevState = GUI.enabled;
    8. GUI.enabled = newState;
    9. // code....
    10. GUI.enabled = prevState;
    11.  
    12. var prevChanged = GUI.changed;
    13. GUI.changed = someValue;
    14. // code...
    15. GUI.changed = prevChanged;
    16.  
    In Rabbit you just simply:
    Code (csharp):
    1.  
    2. using (gui.ColorBlock(someColor))
    3. {
    4.     // code...
    5. }
    6.  
    7. using (gui.State(newState))
    8. {
    9.    // code...
    10. }
    11.  
    12. using (gui.Changed(someValue))
    13. {
    14.    // code...
    15. }
    16.  
    You get the idea... Now when it comes to fields. In Unity you do:
    Code (csharp):
    1.  
    2. myFloat = EditorGUILayout.FloatField("FloatField", myFloat, GUILayout.Width(50f), GUILayout.Height(15));
    3.  
    In Rabbit it's almost the same:
    Code (csharp):
    1.  
    2. myFloat = gui.Float("FloatField", myFloat, Layout.sWidth(50f).Height(15)); // or new Layout { width = 50f, height = 15 }
    3.  
    In general, I omitted the word "Field". So Text instead of TextField, Int instead of IntField, etc.

    Some more differences: gui.LastRect instead of GUILayoutUtility.GetLastRect() and gui.Cursor(rect, cursor) instead of EditorGUIUtility.AddCursor(rect, cursor);

    There's some extra fields like ToolbarSearch (the bar used hierarchy/project views), EnumMask, Splitter, etc.

    Implementation: There's a BaseGUI, from which RabbitGUI and TurtleGUI inherit. RabbitGUI uses GUI/EditorGUI as its backend and uses a custom layouting algorithm. TurtleGUI is a GUILayout/EditorGUILayout wrapper. You can switch between those implementations to see the difference via: Tools/Vexe/GUI/Use[Rabbit|Turtle]GUI. Your GUI code doesn't change when you switch between those cause they both implement BaseGUI

    Here's a sample showing the garbage generated and performance difference between Turtle and Rabbit (I 'shake' the inspector window a bit to trigger drawing/layouting to get those spikes):






    You can see Rabbit is a bit more 'compact' and so some of the coordinates/dimensions are different. It's not the silver bullet unfortnately, there's some things that doesn't look well now: like Vector fields in narrow mode (when the inspector width is less than 300pix or something). And Inline object drawing :( - Will fix in later updates.

    Why ~20MBs? Ask Unity :D - Now to be fair, I had slightly lower numbers before wrapping Unity's GUI with TurtleGUI. I don't know why it's much more slower now, but it was still bad, a single call to BeginHorizontal could generate up to 50KB garbage (at least in my profiling) You can look at the code yourself and see if I missed something. I'm not sure I did, the drawers code is exactly the same, in both these cases ^ except one Uses GUILayout/EditorGUILayout (TurtleGUI) and the other uses RabbitGUI

    NOTE: Although I shipped 1.1 with RabbitGUI's being the default activated current GUI, If you ever get messed up drawing, make sure you're using Rabbit and not Unity's GUI. Click on Tools/Vexe/GUI/UseRabbitGUI and see if that makes things better. Some of the drawers are a bit off when drawn using Unity's GUI
     
    Last edited: Nov 13, 2014
  37. vexe

    vexe

    Joined:
    May 18, 2013
    Posts:
    644
    VFW 1.1.2

    Finally fixed inline drawing. Works now both with Rabbit and Unity GUILayout. Due to the fact that all Unity's components uses Unity's GUILayout system, it's hard to just create an Editor for a certain target and call its OnInspectorGUI because Unity's layout is incompatible with Rabbit's layout. I came up with a solution, not the best, but reasonably easy/feasible. I wrote editor code for the most-used Unity components and the ones that it usually makes sense to Inline such BoxCollider, Rigidbody, Transform, etc. If you want to support more component types, just go in Plugins/Editor/Vexe/Drawers/Others/InlineAttributeDrawer and add your drawer type and code, it's very simple. Now for Behaviours (Mono/Better) I used recursive drawing, so you don't need to add them you're gonna get all your properties/fields but no categories or custom editors.

    Also fixed vector drawing. Unity does this thing in Vector2/3Field where if the inspector is narrow it compresses the fields so that they still have usable width. I thought it was fancy, didn't like it. So I just used float fields instead, simplifying things.
     
  38. buggle52

    buggle52

    Joined:
    Nov 17, 2013
    Posts:
    5
    Just wanted to drop a note to say your efforts are really appreciated. The lack of action on this topic is probably a testament to how well your framework is working... at least for me anyway!
     
    baguwka likes this.
  39. Enoch

    Enoch

    Joined:
    Mar 19, 2013
    Posts:
    198
    This. This right here is why Unity is a thing of Beauty. This is awesome, I've spent hours banging my head against some of the problems you are claiming to solve here. If this works as advertised (testing it out tonight), then it is on my list of essential assets. This really is an editor extension no one should be without.

    Is it possible to include this with any assets put up on the asset store or would there be licensing issues?
     
  40. vexe

    vexe

    Joined:
    May 18, 2013
    Posts:
    644
    @buggle52 thanks for dropping by, appreciate it!

    @Enoch thanks for dropping by too and I hope you really find this asset useful! - And no there's no issues. I haven't yet decided on what license I should go for. But I'd definitely choose one with the least restriction: take the code, modify it, improve it, redistribute it, make benefit from it etc, just give credits where it's due and don't claim that you wrote what you haven't. There's probably a license that gets applied to things that gets published on the forums, I think MIT. I'm not sure though. But like I said for me, the author of this asset I don't mind you using it in whatever shape or form you like. The asset is free and will always be. It's hosted in a private bitbucket repo so I might make that public as well someday...
     
  41. Enoch

    Enoch

    Joined:
    Mar 19, 2013
    Posts:
    198
    Looking over the framework in more detail this looks fantastic. You should play up the idea that you can Runtime serialize a bit more, its a fantastic feature. It took me some time but I got it working using something like this:
    Code (csharp):
    1.  
    2. [Show]
    3.     public void SaveData()
    4.     {      
    5.         var name = gameObject.name;
    6.         if (!String.IsNullOrEmpty(FileName))
    7.         {
    8.             name = FileName;
    9.         } else
    10.         {
    11.             FileName = name;
    12.         }
    13.  
    14.         var path = Application.dataPath + SavePath + name + FileExt;
    15.         var serializationData = new SerializationData();
    16.         Serializer.Save(this,serializationData);
    17.         StreamWriter sw = new StreamWriter(path);
    18.         BinaryFormatter serializer = new BinaryFormatter();
    19.         serializer.Serialize(sw.BaseStream,serializationData.serializedStrings);
    20.         sw.Close();
    21.     }
    22.  
    Which is horribly ugly and foolish on my part since you serialize the data for me to a nice <string,string> dictionary but I then use the binaryformatter to serialize that to a file. I am sure there is a way the serializers could serialize and deserialize to a stream but it didn't seem entirely obvious how to do that since your Serializer never exposed any methods for that purpose (that I could see).

    Clearly this is a use case where I would love to have a file dialog popup based on attribute of some sort.

    Also if your showing properties and methods then a attribute that allows you to set how often the editor calls and evaluates those functions could be useful. For instance if I have a property that returns a list of all objects with in a certain distance from a gameobject, it might be better to have the editor only call/evaluate that method every second or so versus everyframe. Perhaps an eval(t) attribute or something that only calls that method every t seconds.

    Writing a custom drawer using your API might be a good idea for another tutorial. I want to use some of your property drawers for an EditorWindow menu option that I am writing, its not precisely clear how I approach that but as I dig deeper into the editor side of your code I am hoping this becomes more clear.

    Again Excellent work. Fantastic asset even if you stopped right now and never touched another thing.
     
  42. vexe

    vexe

    Joined:
    May 18, 2013
    Posts:
    644
    @Enoch

    Taking runtime serialization/saving to the next step is exactly what I'm working on right now! I wrote a saving/loading system that works similar to Mike Talbot's UnitySerializer but much simpler, smaller and cleaner. Still a WIP

    • It currently uses FullSerializer to output JSON to files.
    • You could save/load an entire scene with it! (since the data is readable it means you could then modify it)
    • You could save/load GameObjects, Unity components such as Transform, Rigidbody, Collider, Mesh, MeshRenderer/Filter, etc and of course MonoBehaviours!
    • You can save a full game object hierarchy
    • You can selectively choose what components you want to save within a game object
    • There are however two restrictions currently to get this simple and flexible system is that the game objects "you want to save" must have unique names and unique components (ex one Rigidbody not two etc). IMO the first restriction is easily coped with. Just a single editor button/menu item key to detect and uniqify game objects with dup names. So if you have "GO" and "GO" you'd have "GO_0" and "GO_1" or something similar.
    With this saving feature I don't think I'd need to expose streams/output destinations to the serialization system is there?

    For your methods/properties query:
    • methods only get invoked if you press the invoke button, they don't get re-evaluated every frame.
    • the property example you mentioned sounds like a readonly property (with a getter only) currently DataMember only wraps read/write properties so that property wouldn't be even visible. Getting all game objects within a certain distance is more suited to be a method rather than a readonly property. properties should just set/get something really quick with minimum side effects and not so very expensive calculations. Maybe if you provide an example where this eval attribute would be really useful, I can consider it for sure.
    For your editor window: I'll try my best to make some more tuts once I finish this saving system. For now, I'll write you an example in the next post of how you could re-use drawers in EditorWindows
     
    Last edited: Nov 20, 2014
  43. vexe

    vexe

    Joined:
    May 18, 2013
    Posts:
    644
    Alright here it goes, pretty straight forward. You just have to understand that drawers need to have some sort of MemberInfo to draw (MethodInfo, FieldInfo, PropertyInfo, etc) so once you get your windows members you're set to draw them immediately:
    • Declare a some kind of GUI field (BaseGUI, RabbitGUI or TrutleGUI) to use for your drawings
    • Declare your window's properties/fields, as if you're in a BetterBehaviour, same rules applies, same attributes, etc
    • In OnEnable, get the member infos to your window, and use the default serialization logic to filter out the non visible ones (yes... you can define your won serialization logic if you're wondering...)
    • In OnGUI, use gui.Member to draw the window's visible members
    Result:


    Code:
    Code (csharp):
    1.  
    2. using System;
    3. using System.Collections.Generic;
    4. using System.Linq;
    5. using System.Reflection;
    6. using UnityEditor;
    7. using UnityEngine;
    8. using Vexe.Editor.Framework.GUIs; // for BaseGUI, RabbitGUI and TurtleGUI
    9. using Vexe.Runtime.Serialization; // required to use the SerializationLogic to fetch visible members
    10. using Vexe.Runtime.Types;      // required for attributes
    11.  
    12. public class DrawersInEditorWindowExample : EditorWindow
    13. {
    14.    private BaseGUI gui;
    15.    private IEnumerable<MemberInfo> visibleMembers;
    16.  
    17.    [Tags, FilterTags]
    18.    public string playerTag { get; set; }
    19.  
    20.    [Show, SelectEnum, FilterEnum]
    21.    private KeyCode jumpKey;
    22.  
    23.    [Comment("The GUI layouting system to use to draw this editor window"),
    24.    ShowType(typeof(BaseGUI))]
    25.    public Type guiType = typeof(RabbitGUI);
    26.  
    27.    public ITestInterface itface;
    28.  
    29.    [Show] void SomeMethod()
    30.    {
    31.      Debug.Log("SomeMethod");
    32.    }
    33.  
    34.    private void OnEnable()
    35.    {
    36.      // get all members in the editor window
    37.      var allMembers = GetType().GetMembers(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
    38.  
    39.      // then fetch those that are visible according to our serialization logic/rules
    40.      visibleMembers = allMembers.Where(SerializationLogic.Default.IsVisibleMember);
    41.    }
    42.  
    43.    private void OnGUI()
    44.    {
    45.      if (gui == null || guiType != gui.GetType())
    46.      {
    47.        gui = BaseGUI.Create(guiType);
    48.        Repaint();
    49.      }
    50.  
    51.      gui.OnGUI(GUICode, new Vector2(5f, 5f)); // the vector is just padding (or border offsets). x coord is left, y is right
    52.    }
    53.  
    54.    private void GUICode()
    55.    {
    56.      string id = GetType().Name;
    57.      foreach (var member in visibleMembers)
    58.        gui.Member(
    59.          member, // the member that we're drawing
    60.          this,   // the unity target object, used for undo
    61.          this,   // the object that the members belong to, in this case its the same object
    62.          id,    // a uniqe id, used to persist foldout values
    63.          false); // whether we want only core drawers for our members or not
    64.    }
    65.  
    66.    public interface ITestInterface
    67.    {
    68.      List<GameObject> List { get; set; }
    69.      Dictionary<string, Vector3> Dict { get; set; }
    70.    }
    71.  
    72.    public class TestClass1 : ITestInterface
    73.    {
    74.      float x, y;
    75.      public List<GameObject> List { get; set; }
    76.      public Dictionary<string, Vector3> Dict { get; set; }
    77.    }
    78.  
    79.    public class TestClass2 : ITestInterface
    80.    {
    81.      public int num;
    82.      public string name;
    83.      public List<GameObject> List { get; set; }
    84.      public Dictionary<string, Vector3> Dict { get; set; }
    85.    }
    86.  
    87.    public static class MenuItems
    88.    {
    89.      [MenuItem("Window/MyWindow")]
    90.      public static void ShowMyWindow()
    91.      {
    92.        EditorWindow.GetWindow<DrawersInEditorWindowExample>();
    93.      }
    94.    }
    95. }
    96.  
    If you don't like the order things are displayed, you can easily change it:
    Code (csharp):
    1.  
    2.     // ...
    3.     visibleMembers = allMembers.Where(SerializationLogic.Default.IsVisibleMember)
    4.                                .OrderBy<MemberInfo, int>(FieldsThenPropsThenMethods)
    5.                                .ThenBy(m => m.GetDataType().Name) // GetDataType is in Vexe.Runtime.Extensions
    6.                                .ThenBy(m => m.Name);
    7.     // ...
    8.  
    9.    private int FieldsThenPropsThenMethods(MemberInfo member)
    10.    {
    11.      switch (member.MemberType)
    12.      {
    13.        case MemberTypes.Field  : return 0;
    14.        case MemberTypes.Property : return 1;
    15.        case MemberTypes.Method  : return 3;
    16.        default: throw new NotSupportedException();
    17.      }
    18.    }
    19.  
    Notice I used BaseGUI cause I just wanted to state that there's two layouting systems. If you're using RabbitGUI alone in an EditorWindow, you could have wrote:
    Code (csharp):
    1.  
    2. RabbitGUI gui = new RabbitGUI();
    3. private void OnGUI()
    4. {
    5.     var start = new Rect(5f, 5f, EditorGUIUtility.currentViewWidth - 5f, 0f);
    6.     using (gui.Begin(start))
    7.     {
    8.          // gui code...
    9.     }
    10. }
    11.  
    One drawback right now is that the window's members won't survive assembly reloads. It's pretty easy fix though, just write a BetterWindow, use the same serialization code and store the window's member data in some asset file.
     
    Last edited: Nov 21, 2014
    rakkarage likes this.
  44. vexe

    vexe

    Joined:
    May 18, 2013
    Posts:
    644
    1.1.4 is out with a few editor/gui fixes and improvements. Also added the editor window example above (Plugins/Editor/Vexe/Examples)
     
    Last edited: Nov 20, 2014
  45. Enoch

    Enoch

    Joined:
    Mar 19, 2013
    Posts:
    198
    Wow vexe, thanks for the reply and update. I would advise that you move the Runtime files (everything but the DLLS) from the plugins folder to another folder under assets. Unity won't include those files in the project when it Generates project files for Visual Studio otherwise. I was able to get an editor window working via your example so thanks for that example it really helped.

    You upcoming serialization features sound fantastic. Being able to serialize/deserialize at runtime to any file is a fantastic feature. As for a good use case for exposing the read/write to stream of those serializers, the best I can think of is for Network related operations (so you can serialize to a newtork stream). If you expose them it would much easier to setup remote repositories for common configurations of complex Unity Objects.

    I don't know about others but I sometimes setup massive Unity objects in the form of Data Managers that hold huge lists of configuration game data. I do this because I can use unitys editor to edit the data efficiently without having to write my own ui for data entry. Being able to serialize this data anywhere I want, even to say a WWW network stream, would be incredibly useful. Your product could be the foundation for run-time configuring entire game levels via a network stream.

    As for the Eval attribute, I think a better name that is more fitting might be AutoInvoke(t). This would automatically invoke a game method and cache the data in the editor view every t time period. The biggest use I can see for such a attribute is for run-time debugging and getting real-time data from a component. Sometimes computing this data is computationally complex and it is something you wouldn't want to do each frame, however it also might very cumbersome to update it via a button click invoke (ie I am switching rapidly from one enemy object to another wanting to see some data).

    Again keep up the great work. Fantastic piece of code you have here.
     
  46. vexe

    vexe

    Joined:
    May 18, 2013
    Posts:
    644
    I'm not entirely sure what you mean. I'm using VS as well with no issues regarding the project files whatsoever. Could you elaborate?
     
  47. Enoch

    Enoch

    Joined:
    Mar 19, 2013
    Posts:
    198
    I am using VS 2012 and VS tools for Unity 1.9.8.0, and Unity 4.6.0f2. For whatever reason when I install that unitypackage, when I open the project I do not see Assets/Plugins/Vexe/Runtime in the Visual Studio solution explorer. I can see the folder in VS by showing all files (clicking the icon in the solution explorer icon menu bar). So the folders are there they just aren't included in the project for some reason.

    When I moved all script files from there to a folder Assets/Vexe, and left the DLLs right where they were, everything worked perfectly. It looks/seems like Unity really doesn't like anything in the plugins directory that isn't a DLL. I am not perfectly well versed in including DLLs but I thought that only DLLS were supposed to go in that Plugins folder.

    Its not a huge deal to me, like I said I was able to put everything where it would work in my environment, I just didn't want other users to experience the same thing and wonder what was going on.

    Edit: I just noticed that unity created two other projects (I didn't notice these before) CSharp.Editor.Plugins and CSharp.Plugins and I bet the runtime files would have shown up there. I would say this isn't really a problem but it did confuse me as to why the files were not in the UnityVs.<projectname>.CSharp project.
     
    Last edited: Nov 21, 2014
  48. vexe

    vexe

    Joined:
    May 18, 2013
    Posts:
    644
    Well you could always use the Unity Project Explorer if you have UnityVS, it will show you the project hierarchy as it is seen in Unity's project view. Alt-Shift-E in VS makes it show up for me. (BTW there's a new UnityVS version)

    As for the Plugins folder, you could put whatever you want in it. The reason I put my stuff there as opposed to directly under Assets (which is how i had it previously) is that usually most user scripts go under Assets, and it is highly likely that they will spend most their time working with these scripts, as opposed to mine. If you modify code directly under Assets then all source files directly under assets will get recompiled when there's an assembly reload, so I'd just slow down compilation with no good reason. Putting my stuff in Plugins (or Standard Assets for that matter) unity will compile my code into a different DLL than the user's codes. Since now there's two DLLs, when you change code that belongs to one DLL, Unity will not compile the code that's in the other DLL since it didn't change. See this for more info.
     
  49. vexe

    vexe

    Joined:
    May 18, 2013
    Posts:
    644
    So I've been busy with this Save System idea, I got good results. Issues I'm having so far are with persisting coroutines state, and animations: very tricky. The system might be available in next VFW update as a separate package. See my second reply to Enoch for a quick recap of what you could do with it. No need to uniquely name gameobjects now though, just assets (audio clips, animations textures, etc). You can pretty much save any complex structure you have. Here's a quick demo for now (the GC spike had around 1.4MB but I couldn't catch it for some reason lol - much better than the previous system I did though. yes this is a redesign)

     
    Last edited: Nov 26, 2014
    OnePxl likes this.
  50. osxhacker

    osxhacker

    Joined:
    Dec 2, 2014
    Posts:
    2
    First, awesome library man! Coming from a OOP/functional background, the ability to use properties and classes directly instead of public references is huge. And that doesn't even get into all of the goodies tucked away in this essential (to me) library!

    FWIW, I just downloaded v1.1.4 and am trying it with Unity 4.6.0f3. I ran into what I believe is a minor refactoring error. In BetterBehaviour.cs:181 I am getting a compilation error of:

    Assets/ThirdParty/Plugins/Vexe/Runtime/Types/Core/BetterBehaviour.cs(181,25): error CS0103: The name `Save' does not exist in the current context

    Doing a diff between v1.1 and v1.1.4 on this file shows:

    Code (CSharp):
    1. --- a/Assets/ThirdParty/Plugins/Vexe/Runtime/Types/Core/BetterBehaviour.cs
    2. +++ b/Assets/ThirdParty/Plugins/Vexe/Runtime/Types/Core/BetterBehaviour.cs
    3. @@ -17,7 +17,7 @@ namespace Vexe.Runtime.Types
    4.      [DefineCategory("Properties", 1f, MemberType = MemberType.Property, Exclusive = false)]
    5.      [DefineCategory("Methods", 2f, MemberType = MemberType.Method, Exclusive = false)]
    6.      [DefineCategory("Debug", 3f, Pattern = "^dbg")]
    7. -    public abstract class BetterBehaviour : MonoBehaviour, ISavable, IHasUniqueId, ISerializationCallbackReceiver
    8. +    public abstract class BetterBehaviour : MonoBehaviour, ISerializable, IHasUniqueId, ISerializationCallbackReceiver
    9.      {
    10.          /// <summary>
    11.          /// Use this to include members to the "Debug" categories
    12. @@ -175,7 +175,7 @@ namespace Vexe.Runtime.Types
    13. #if UNITY_EDITOR
    14.              if (UnityEditor.EditorApplication.isPlayingOrWillChangePlaymode && !UnityEditor.EditorApplication.isPlaying)
    15.              {
    16. -                Save();
    17. +                Serialize();
    18.              }
    19. #else
    20.              Save();
    21. @@ -184,17 +184,17 @@ namespace Vexe.Runtime.Types
    22.          public void OnAfterDeserialize()
    23.          {
    24. -            Load();
    25. +            Deserialize();
    26.          }
    27. -        public void Save()
    28. +        public void Serialize()
    29.          {
    30.              dbgLog("Saving " + GetType().Name);
    31.              SerializationData.Clear();
    32.              Serializer.Save(this, SerializationData);
    33.          }
    34. -        public void Load()
    35. +        public void Deserialize()
    36.          {
    37.              dbgLog("Loading" + GetType().Name);
    38.              Serializer.Load(this, SerializationData);
    39.  
    This appears to be just a missed renaming from Save to Serialize, but that's just a guess.

    HTH,
    Steve
     
    Last edited: Dec 2, 2014