How to fix a region in Viewport

Hi all!

I’m actually working on a grid which consist of multiple Tracks (this component has 2 sub components: titles in the left one, multiple steps in the right one) stacked vertically.
I would like to scroll vertically over all the tracks and horizontally. However regarding the horizontal scroll I would like to scroll only in the right box.

Because an image is better than words here is a fantastic paint powered image:
grid

Is there a way to tell the viewport to scroll horizontally only in the [C, D] region but vertically in the whole y axis ( [A, B] region) ?

I didn’t find any information about it. So maybe my architecture wouldn’t be the right one…

Thanks.

1 Like

There are two ways, either have a ListBox and make each track consisting of the track description and the scrollable area. I used the observer pattern, so each scrollable area reacts to a global scroll bar, that is shown below.

The second would be to have two Viewports, the left one only allows vertical scroll and link the vertical movement.

ScrollBar has a Listener for that, IIRC

1 Like

Hi there.
You need 2 viewports: one for the area in the [B, C] region and one for the area in the [C, D] region. Let’s call them ViewportA and ViewportB.
ViewportA scrolls only vertically. ViewportB can scroll in both ways.
You need to get notifications when ViewportB’s viewed area changes.
Since the Viewport class doesn’t implement the observer pattern (Listener), your only way to achieve this is to subclass the Viewport class and override the visibleAreaChanged function, implementing your own custom Listener.
Here’s an example:

#pragma once

#include <JuceHeader.h>

//==============================================================================
/*
    Custom Viewport with notifications.
*/
class CustomViewport : public Viewport
{
public:
    //==============================================================================
    /** Viewport override */
    
    void visibleAreaChanged(const Rectangle<int>& newVisibleArea) override
    {
        listeners.call(&Listener::viewportAreaChanged, this, newVisibleArea);
    }
    
    //==============================================================================
    /** Listening */
    
    class Listener
    {
    public:
        
        virtual ~Listener() {}
        virtual void viewportAreaChanged(Viewport* viewport, const Rectangle<int>& newVisibleArea) = 0;
    };
    
    void addListener(Listener* listener)
    {
        listeners.add(listener);
    }
    
    void removeListener(Listener* listener)
    {
        listeners.remove(listener);
    }

private:
    //==============================================================================
    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (CustomViewport)

    ListenerList<Listener> listeners;
};

Make your ViewportB an instance of CustomViewport. In your parent Component override the CustomViewport::Listener class and attach by calling addListener. In your implementation of the viewportAreaChanged callback you can now set the vertical offset of ViewportA using the newVisibleArea parameter.

You can do the same thing also with ViewportA, so every time you scroll it vertically, the other viewport will scroll. You can use the first parameter of the callback to determine which one changed.

4 Likes

Just a friendly reminder about viewports and timelines.
If you plan on putting things on a timeline you’ll be limited to 32bit resolution which might overflow when you try to represent many hours in samples resolution.

Also, for complex designs with many elements consider caching and not keeping entire model as components within viewport.

3 Likes

Thank all for your quick replies.

I will go with the 2 Viewports solution since I’m used to it. @daniel ListBox is a great idea that I didn’t even think about but I’m more confortable with Viewport at the moment.

@masshacker masshacker do I really need to subclass the Viewport? I quickly looked at the documentation and it seems that it’s possible to get a reference of the scrollbars getVerticalScrollBar(). This way I could potentially add a listner to this one and get a feedback with scrollBarMoved(ScrollBar *scrollBarThatHasMoved, double newRangeStart) as @daniel said.
Am I wrong?

@ttg ok I will keep it in mind. Hopefully I won’t encounter this kind of issues since it’s basic and not so many components. But yes you’re definitely right to point it.

1 Like

By looking at the source, it is safe to do so, since the virtual method viewportAreaChanged is empty, so it is meant to be overridden. However, by looking where it is called, you get the same effect by reacting to both ScrollBars as Listener:

However, if your drawing is expensive, inheriting might have the advantage, that there is only one call, while the two scrollbars might fire individually.

1 Like

Hi,
Yeah, it looks like you can use the scrollbars attachment as well, since those are always available, even if they could be invisible.

EDIT: Now I remembered a detail about this (and this is the reason I usually go with the subclass approach). Scrollbars are created in the recreateScrollbars function, which may be called from an another class. If this happens you need to reattach to the new scrollbars, but you don’t get a notification that tells you the scrollbars have changed. In your case this shouldn’t be an issue though.

Thanks @daniel for sharing the code. Hopefully for the moment the drawing is not expensive.

Thanks to you @masshacker too for the details. Indeed it could be an issue in certain case.

Hi everyone
I am quite new to Juce and C++ and trying to build my first gui app, so please excuse my noob question.
@masshacker : I have been trying and reading a lot over the last days to get your example of the CostumViewport working, but I just don’t get how to make my ViewportB an instance of CustomViewport. VS2019 keeps telling me there is no appropriate default constructor. Even though I succeeded using the scrollbar Listener, I really would like to know how to make use of the CustomViewport Class since this will definetly help me doing other cool stuff in the future. Any help is much appreciated.
Thanks in advance

Another possible solution is nested viewports:

  • Everything is in a vertically scrolling viewport
  • The right side (C-D) is in a horizontally scrolling viewport. Possibly not the juce viewport but something more sophisticated due to the concerned @ttg raised
1 Like

Yeah, VS is telling you what the problem is: the Viewport base class’ constructor needs to be initialised correctly.
I can’t seem to find a way to edit my previous post, so here is a fixed version :

#pragma once

#include <JuceHeader.h>

//==============================================================================
/*
    Custom Viewport with notifications.
*/
class CustomViewport : public Viewport
{
public:
    //==============================================================================
    CustomViewport(const String& componentName = String())
    : Viewport(componentName)
    {
        
    }
    
    //==============================================================================
    /** Viewport override */
    
    void visibleAreaChanged(const Rectangle<int>& newVisibleArea) override
    {
        listeners.call(&Listener::viewportAreaChanged, this, newVisibleArea);
    }
    
    //==============================================================================
    /** Listening */
    
    class Listener
    {
    public:
        
        virtual ~Listener() {}
        virtual void viewportAreaChanged(Viewport* viewport, const Rectangle<int>& newVisibleArea) = 0;
    };
    
    void addListener(Listener* listener)
    {
        listeners.add(listener);
    }
    
    void removeListener(Listener* listener)
    {
        listeners.remove(listener);
    }

private:
    //==============================================================================
    JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR(CustomViewport)

    ListenerList<Listener> listeners;
};
1 Like

Thank You so much, all thumps up!