Place transparent composite over other composite in SWT
Asked Answered
P

2

5

I'm searching for a way to add an overlay over some composites in my application. The overlay will contain an label with text "No data available". The underlying composite need to be shown but the user cannot do anything. My application contains different composite part in one screen so I need a way to only place the overlay over one of the composites. Is there a way to implement this in SWT?

Pah answered 7/2, 2017 at 10:54 Comment(1)
See also the style SWT.TRANSPARENT, at least for Composites.Byzantium
L
10

A possible solution would be to put a semi-transparent Shell with no trimmings over the Composite you want to cover.

The tricky part is to update the overlay Shell to continuously match the size, position and visibility of the Composite and its parents (since they also could affect the children bounds and visibility).

So I decided to try to make a class Overlay to do that; it can be used to cover any Control and it uses control and paint listeners to track and match the underlying Control. These listeners are also attached to the whole hierarchy of parents of the Control.

You can set the color, the transparency and a text over the Overlay using the corresponding methods.

I made some simple tests and it seemed to work correctly, but I can't guarantee anything. You might want to give it a try it.

A simple example using it:

public class OverlayTest {

    public static void main(String[] args) {

        Display display = new Display();
        Shell shell = new Shell(display);
        shell.setLayout(new FillLayout(SWT.VERTICAL));
        shell.setSize(250, 250);

        // create the composite
        Composite composite = new Composite(shell, SWT.NONE);
        composite.setLayout(new FillLayout(SWT.VERTICAL));

        // add stuff to the composite
        for (int i = 0; i < 5; i++) {
            new Text(composite, SWT.BORDER).setText("Text " + i);
        }

        // create the overlay over the composite
        Overlay overlay = new Overlay(composite);
        overlay.setText("No data available");

        // create the button to show/hide the overlay
        Button button = new Button(shell, SWT.PUSH);
        button.setText("Show/hide overlay");
        button.addSelectionListener(new SelectionAdapter() {
            @Override
            public void widgetSelected(SelectionEvent arg0) {
                // if the overlay is showing we hide it, otherwise we show it
                if (overlay.isShowing()) {
                    overlay.remove();
                }
                else {
                    overlay.show();
                }
            }
        });

        shell.open();
        while (shell != null && !shell.isDisposed()) {
            if (!display.readAndDispatch()) {
                display.sleep();
            }
        }
    }

}

And the Overlay class:

import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

import org.eclipse.swt.SWT;
import org.eclipse.swt.events.ControlEvent;
import org.eclipse.swt.events.ControlListener;
import org.eclipse.swt.events.DisposeEvent;
import org.eclipse.swt.events.DisposeListener;
import org.eclipse.swt.events.PaintEvent;
import org.eclipse.swt.events.PaintListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.layout.GridData;
import org.eclipse.swt.layout.GridLayout;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Control;
import org.eclipse.swt.widgets.Label;
import org.eclipse.swt.widgets.Scrollable;
import org.eclipse.swt.widgets.Shell;

/**
 *  A customizable overlay over a control.
 *  
 *  @author Loris Securo
 */
public class Overlay {

    private List<Composite> parents;
    private Control objectToOverlay;
    private Shell overlay;
    private Label label;
    private ControlListener controlListener;
    private DisposeListener disposeListener;
    private PaintListener paintListener;
    private boolean showing;
    private boolean hasClientArea;
    private Scrollable scrollableToOverlay;

    public Overlay(Control objectToOverlay) {

        Objects.requireNonNull(objectToOverlay);

        this.objectToOverlay = objectToOverlay;

        // if the object to overlay is an instance of Scrollable (e.g. Shell) then it has 
        // the getClientArea method, which is preferable over Control.getSize
        if (objectToOverlay instanceof Scrollable) {
            hasClientArea = true;
            scrollableToOverlay = (Scrollable) objectToOverlay;
        }
        else {
            hasClientArea = false;
            scrollableToOverlay = null;
        }

        // save the parents of the object, so we can add/remove listeners to them
        parents = new ArrayList<Composite>();
        Composite parent = objectToOverlay.getParent();
        while (parent != null) {
            parents.add(parent);
            parent = parent.getParent();
        }

        // listener to track position and size changes in order to modify the overlay bounds as well
        controlListener = new ControlListener() {
            @Override
            public void controlMoved(ControlEvent e) {
                reposition();
            }

            @Override
            public void controlResized(ControlEvent e) {
                reposition();
            }
        };

        // listener to track paint changes, like when the object or its parents become not visible (for example changing tab in a TabFolder)
        paintListener = new PaintListener() {
            @Override
            public void paintControl(PaintEvent arg0) {
                reposition();
            }
        };

        // listener to remove the overlay if the object to overlay is disposed
        disposeListener = new DisposeListener() {
            @Override
            public void widgetDisposed(DisposeEvent e) {
                remove();
            }
        };

        // create the overlay shell
        overlay = new Shell(objectToOverlay.getShell(), SWT.NO_TRIM);

        // default values of the overlay
        overlay.setBackground(objectToOverlay.getDisplay().getSystemColor(SWT.COLOR_GRAY));
        overlay.setAlpha(200);

        // so the label can inherit the background of the overlay
        overlay.setBackgroundMode(SWT.INHERIT_DEFAULT);

        // label to display a text
        // style WRAP so if it is too long the text get wrapped
        label = new Label(overlay, SWT.WRAP);

        // to center the label
        overlay.setLayout(new GridLayout());
        label.setLayoutData(new GridData(SWT.CENTER, SWT.CENTER, true, true));

        showing = false;
        overlay.open();
        overlay.setVisible(showing);
    }

    public void show() {

        // if it's already visible we just exit
        if (showing) {
            return;
        }

        // set the overlay position over the object
        reposition();

        // show the overlay
        overlay.setVisible(true);

        // add listeners to the object to overlay
        objectToOverlay.addControlListener(controlListener);
        objectToOverlay.addDisposeListener(disposeListener);
        objectToOverlay.addPaintListener(paintListener);

        // add listeners also to the parents because if they change then also the visibility of our object could change
        for (Composite parent : parents) {
            parent.addControlListener(controlListener);
            parent.addPaintListener(paintListener);
        }

        showing = true;
    }

    public void remove() {

        // if it's already not visible we just exit
        if (!showing) {
            return;
        }

        // remove the listeners
        if (!objectToOverlay.isDisposed()) {
            objectToOverlay.removeControlListener(controlListener);
            objectToOverlay.removeDisposeListener(disposeListener);
            objectToOverlay.removePaintListener(paintListener);
        }

        // remove the parents listeners
        for (Composite parent : parents) {
            if (!parent.isDisposed()) {
                parent.removeControlListener(controlListener);
                parent.removePaintListener(paintListener);
            }
        }

        // remove the overlay shell
        if (!overlay.isDisposed()) {
            overlay.setVisible(false);
        }

        showing = false;
    }

    public void setBackground(Color background) {
        overlay.setBackground(background);
    }

    public Color getBackground() {
        return overlay.getBackground();
    }

    public void setAlpha(int alpha) {
        overlay.setAlpha(alpha);
    }

    public int getAlpha() {
        return overlay.getAlpha();
    }

    public boolean isShowing() {
        return showing;
    }

    public void setText(String text) {
        label.setText(text);

        // to adjust the label size accordingly
        overlay.layout();
    }

    public String getText() {
        return label.getText();
    }

    private void reposition() {

        // if the object is not visible, we hide the overlay and exit
        if (!objectToOverlay.isVisible()) {
            overlay.setBounds(new Rectangle(0, 0, 0, 0));
            return;
        }

        // if the object is visible we need to find the visible region in order to correctly place the overlay

        // get the display bounds of the object to overlay
        Point objectToOverlayDisplayLocation = objectToOverlay.toDisplay(0, 0);

        Point objectToOverlaySize;

        // if it has a client area, we prefer that instead of the size 
        if (hasClientArea) {
            Rectangle clientArea = scrollableToOverlay.getClientArea();
            objectToOverlaySize = new Point(clientArea.width, clientArea.height);
        }
        else {
            objectToOverlaySize = objectToOverlay.getSize();
        }

        Rectangle objectToOverlayBounds = new Rectangle(objectToOverlayDisplayLocation.x, objectToOverlayDisplayLocation.y, objectToOverlaySize.x,
                objectToOverlaySize.y);

        Rectangle intersection = objectToOverlayBounds;

        // intersect the bounds of the object with its parents bounds so we get only the visible bounds
        for (Composite parent : parents) {

            Rectangle parentClientArea = parent.getClientArea();
            Point parentLocation = parent.toDisplay(parentClientArea.x, parentClientArea.y);
            Rectangle parentBounds = new Rectangle(parentLocation.x, parentLocation.y, parentClientArea.width, parentClientArea.height);

            intersection = intersection.intersection(parentBounds);

            // if intersection has no size then it would be a waste of time to continue
            if (intersection.width == 0 || intersection.height == 0) {
                break;
            }
        }

        overlay.setBounds(intersection);
    }

}
Linet answered 16/2, 2017 at 23:50 Comment(1)
Why this is not marked as the correct answer? After trying many things this worked nicely. Thanks!Hannover
E
0
  1. Set layout to null on the parent

  2. Add a new Composite with style SWT.TRANSPARENT on the parent BEFORE the one you want to draw over

  3. Set bounds manually on the composites to the same area, use a resize listener if it can change.

  4. Use setVisible() to show the overlay or not.

Warning! Don't use TabFolder together with this method, it has a bug that will cause continous redraw. Also CTabFolder using setTopRight() will have a similar issue but can be avoided by overriding getRightItemEdge (GC gc) and not calling super.getRightItemEdge(gc) again unless size has changed. Will file this bug as soon as the move from bugzilla is complete

Eldaelden answered 6/11, 2023 at 10:4 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.