Experimental idea based on SharedResourcePointer


#1

Hello,

I've been experimenting with ways to provide LookAndFeel-like customisation of custom components, as I've got a big stack of them that would be worth sharing in a module or two.

One of the ideas I had was based on a slightly-butchered SharedResourcePointer, and is basically this:

///////////////////////////////////////////////////////////////////////////////
/**
    A pointer to an automatically-created shared instance of the templated type
    (see SharedResourcePointer).
    It is possible to override the local instance referenced by a specific 
    OverridableSharedResourcePtr object (if this is null, the default shared 
    instance is used). It is also possible to globally replace the default
    instance is used by all objects.
*/
///////////////////////////////////////////////////////////////////////////////
template <typename SharedObjectType>
class OverridableSharedResourcePtr
{
public:
    /** Creates an instance of the shared object, which can be overridden.
        If other OverridableSharedResourcePtr objects for this type already 
        exist, then this one will simply point to the same shared object that 
        they are already using. Otherwise, if this is the first pointer object 
        to be created, then a shared object will be created automatically.
    */
    OverridableSharedResourcePtr()
    {
        SharedObjectHolder& holder = getSharedObjectHolder();
        if (++(holder.refCount) == 1)
        {
            holder.resetInstance();
        }
    }
    /** Destructor.
        If no other OverridableSharedResourcePtr objects exist, this will also
        delete the shared object to which it refers.
    */
    ~OverridableSharedResourcePtr()
    {
        SharedObjectHolder& holder = getSharedObjectHolder();
        if (--(holder.refCount) == 0)
        {
            holder.clear ();
        }
    }
    /** Returns the current default shared instance. */
    SharedObjectType* getDefaultInstance () const
    { 
        return getSharedObjectHolder().get(); 
    }
    /** Replace the current default shared instance. */
    void setDefaultInstance (SharedObjectType* object)
    {
        getSharedObjectHolder().set (object);
    }
    /** Recreates the default shared instance. */
    void resetDefaultInstance ()
    {
        getSharedObjectHolder().resetInstance();
    }
    /** Overrides the local instance referred to by this pointer. */
    void setOverrideInstance (SharedObjectType* object, bool takeOwnership)
    {
        localOverride.set (object, takeOwnership);
    }
    /** Returns the current instance. */
    operator SharedObjectType*() const noexcept         { return &get(); }
    /** Returns the instance this pointer refers to (if no local override has
        been set, this will return the default shared instance. */
    SharedObjectType& get() const
    {
        if (localOverride.get() != nullptr)
        {
            return *localOverride.get();
        }
        return *getDefaultInstance();
    }
    SharedObjectType* operator->() const noexcept       { return &get(); }
private:
    struct SharedObjectHolder  : public ReferenceCountedObject
    {
        juce::SpinLock lock;
        juce::ScopedPointer<SharedObjectType> sharedInstance;
        int refCount;
        void resetInstance ()
        {
            const juce::SpinLock::ScopedLockType sl (lock);
            sharedInstance = new SharedObjectType ();
        }
        void set (SharedObjectType* newInstance)
        {
            const juce::SpinLock::ScopedLockType sl (lock);
            if (newInstance != sharedInstance)
            {
                jassert (newInstance != nullptr); // Must always have a valid default instance!
                sharedInstance = newInstance;
            }
        }
        void clear ()
        {
            const juce::SpinLock::ScopedLockType sl (lock);
            sharedInstance = nullptr;
        }
        SharedObjectType* get () const
        {
            const juce::SpinLock::ScopedLockType sl (lock);
            return sharedInstance;
        }
    };
    static SharedObjectHolder& getSharedObjectHolder() noexcept
    {
        static void* holder [(sizeof (SharedObjectHolder) + sizeof(void*) - 1) / sizeof(void*)] = { 0 };
        return *reinterpret_cast<SharedObjectHolder*> (holder);
    }
    juce::OptionalScopedPointer< SharedObjectType > localOverride;
    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (OverridableSharedResourcePtr)
};

I'm just wondering how dodgy an idea this is, really!

The main limitation (ignoring dangers for the moment) is that there isn't currently any notification when the default instance is changed. This means that it has to go via the SharedObjectHolder for all access to the default instance - though I guess it'd be possible to include a notifier in the SharedObjectHolder as long as it is cleared up properly in clear().

The only real danger would be that the default instance could be changed when something else doesn't expect it to... but I guess it should be obvious that you shouldn't expect that if you're using such an object.

 

Anyway, this template leads to a really basic starting base like this...

class ComponentStyle
{
public:
    ComponentStyle () {};
    virtual ~ComponentStyle () {};
    virtual void paintComponent (juce::Graphics& g, juce::Component& component) = 0;
private:
};

///////////////////////////////////////////////////////////////////////////////

template <class StyleClass>
class ComponentStyleHandle    :    public OverridableSharedResourcePtr< StyleClass >
{
public:
    void paintComponent (juce::Graphics& g, juce::Component& component)
    {
        get().paintComponent (g, component);
    }
private:
};

... which then means you can just have something like this:


class MyComp    :    public Component
{
public:
    class Style    :    public ComponentStyle
    {
    public:
        virtual void paintComponent (Graphics& g, Component& c)
        {
            // blah
        }
    };

    virtual void paint (Graphics& g)
    {
        style->paintComponent (g, *this);
    }

    ComponentStyleHandle<Style> style;
};

The default style is automatically created, and can also be replaced locally on a component quite easily.

You can also replace them globally by doing something like this:

// Create an instance of this at startup, and destroy it on shutdown...
class AppStyles
{
public:
    ComponentStyleHandle< MyComp::Style >        myCompStyle;
    ComponentStyleHandle< MyOtherComp::Style >   myOtherCompStyle;
    AppStyles ()
    {
        myCompStyle.setDefaultInstance      (new ReplacementMyCompStyle);
        myOtherCompStyle.setDefaultInstance (new ReplacementMyOtherCompStyle);
    }
};

 

I'm still experimenting with it, but I thought I'd post it here to see if it sparks any other thoughts/ideas/worries/suggestions.


#2

For what it's worth, I've found that this approach works really well, and makes turning a custom component's appearance into a replaceable (yet otherwise automatically assigned) block utterly trivial.

hopefully there's not some dark side to it that I've failed to spot!


#3

Oh I see.  It took a couple of reads and a quick experiment to see why the LookAndFeel type solution could be painful and this might be better. 

I can't see conceptually that there are any gotchas.  But I'm probably not a safe pair of hands in these matters :)

 


#4

Looks interesting!

Personally, I've been holding back on trying to improve the L+F stuff until the point where everyone has C++11, because it feels like there are interesting things that could be done with lambda functions that could do a cleaner job of it.. I've not actually explored this idea in detail though, so it might be terrible in practice, it's just a vague hunch!


#5

Yeah, I presumed that eventually you'd have some sneaky LookAndFeel replacement!

This is just something I've come up with in the mean time so that my own bespoke components can have the same kind of interchangeable styling functionality, without requiring that the user adopt a specific concrete LookAndFeel class capable of supporting them. I'm really surprised at just how easy it has made this :)