/*
* Jitsi, the OpenSource Java VoIP and Instant Messaging client.
*
* Distributable under LGPL license.
* See terms of license at gnu.org.
*/
package net.java.sip.communicator.util.swing;
import java.awt.*;
import java.awt.event.*;
import java.lang.reflect.*;
import net.java.sip.communicator.util.*;
/**
* Implements a Container for video/visual Components.
* VideoContainer uses {@link VideoLayout} to layout the video/visual
* Components it contains. A specific Component can be
* displayed by default at {@link VideoLayout#CENTER_REMOTE}.
*
* @author Lyubomir Marinov
* @author Yana Stamcheva
*/
public class VideoContainer
extends TransparentPanel
{
/**
* The Logger used by the VideoContainer class and its
* instances for logging output.
*/
private static final Logger logger = Logger.getLogger(VideoContainer.class);
public static final Color DEFAULT_BACKGROUND_COLOR
= OSUtils.IS_MAC ? Color.BLACK : null;
/**
* The name of the instance method of Components added to
* VideoContainer which creates a new Component acting as
* a canvas in which the other Components contained in the
* VideoContainer are painted.
*/
private static final String VIDEO_CANVAS_FACTORY_METHOD_NAME
= "createCanvas";
/**
* The Component which is the canvas, if any, in which the other
* Components contained in this VideoContainer are
* painted.
*/
private Component canvas;
/**
* The Component to be displayed by this VideoContainer
* at {@link VideoLayout#CENTER_REMOTE} when no other Component has
* been added to it to be displayed there. For example, the avatar of the
* remote peer may be displayed in place of the remote video when the remote
* video is not available.
*/
private final Component noVideoComponent;
/**
* Initializes a new VideoContainer with a specific
* Component to be displayed when no remote video is available.
*
* @param noVideoComponent the component to be displayed when no remote
* video is available
*/
public VideoContainer(Component noVideoComponent)
{
setLayout(new VideoLayout());
this.noVideoComponent = noVideoComponent;
/*
* JAWTRenderer on Mac OS X will by default occupy the whole video
* container and will, consequently, draw a black background. In certain
* problematic cases which it will try to provide a workaround, it will
* not occupy the whole video container. To make the experience
* relatively the same, always use a black background.
*/
if (DEFAULT_BACKGROUND_COLOR != null)
{
setBackground(DEFAULT_BACKGROUND_COLOR);
addContainerListener(
new ContainerListener()
{
public void componentAdded(ContainerEvent e)
{
int componentCount = getComponentCount();
if ((componentCount == 1)
&& (getComponent(0)
== VideoContainer.this.noVideoComponent))
componentCount = 0;
setOpaque(componentCount > 0);
}
public void componentRemoved(ContainerEvent e)
{
/*
* It's all the same with respect to the purpose of this
* ContainerListener.
*/
componentAdded(e);
}
});
}
if (this.noVideoComponent != null)
{
add(this.noVideoComponent, VideoLayout.CENTER_REMOTE, -1);
validate();
}
}
/**
* Adds the given component at the {@link VideoLayout#CENTER_REMOTE}
* position in the default video layout.
*
* @param comp the component to add
* @return the added component
*/
@Override
public Component add(Component comp)
{
add(comp, VideoLayout.CENTER_REMOTE);
return comp;
}
@Override
public Component add(Component comp, int index)
{
add(comp, null, index);
return comp;
}
@Override
public void add(Component comp, Object constraints)
{
add(comp, constraints, -1);
}
/**
* Overrides the default behavior of add in order to be sure to remove the
* default "no video" component when a remote video component is added.
*
* @param comp the component to add
* @param constraints
* @param index
*/
@Override
public void add(Component comp, Object constraints, int index)
{
if (VideoLayout.CENTER_REMOTE.equals(constraints)
&& (noVideoComponent != null)
&& !noVideoComponent.equals(comp))
{
remove(noVideoComponent);
validate();
}
if ((canvas == null) || (canvas.getParent() != this))
{
if (OSUtils.IS_MAC && (comp != canvas))
{
/*
* Unless the comp has a createCanvas() method, it makes no
* sense to consider any exception a problem.
*/
boolean ignoreException;
Throwable exception;
ignoreException = true;
exception = null;
canvas = null;
try
{
Method m
= comp.getClass().getMethod(
VIDEO_CANVAS_FACTORY_METHOD_NAME);
if (m != null)
{
ignoreException = false;
Object c = m.invoke(comp);
if (c instanceof Component)
canvas = (Component) c;
}
}
catch (ClassCastException cce)
{
exception = cce;
}
catch (ExceptionInInitializerError eiie)
{
exception = eiie;
}
catch (IllegalAccessException illegalAccessException)
{
exception = illegalAccessException;
}
catch (IllegalArgumentException illegalArgumentException)
{
exception = illegalArgumentException;
}
catch (InvocationTargetException ita)
{
exception = ita;
}
catch (NoSuchMethodException nsme)
{
exception = nsme;
}
catch (NullPointerException npe)
{
exception = npe;
}
catch (SecurityException se)
{
exception = se;
}
if (canvas != null)
add(canvas, VideoLayout.CANVAS, 0);
else if ((exception != null) && !ignoreException)
logger.error("Failed to create video canvas.", exception);
}
}
if ((canvas != null)
&& (canvas.getParent() == this)
&& OSUtils.IS_MAC
&& (comp != canvas))
{
/*
* The canvas in which the other components are to be painted should
* always be at index 0. And the order of adding is important so no
* index should be specified
*/
index = -1;
}
/*
* XXX Do not call #remove(Component) beyond this point and before
* #add(Component, Object, int) because #removeCanvasIfNecessary() will
* remove the canvas.
*/
super.add(comp, constraints, index);
}
/**
* Overrides the default remove behavior in order to add the default no
* video component when the remote video is removed.
*
* @param comp the component to remove
*/
@Override
public void remove(Component comp)
{
super.remove(comp);
if ((comp == canvas)
&& (canvas != null)
&& (canvas.getParent() != this))
{
canvas = null;
validate();
}
Component[] components = getComponents();
VideoLayout videoLayout = (VideoLayout) getLayout();
boolean hasComponentsAtCenterRemote = false;
for (Component c : components)
{
if (!c.equals(noVideoComponent)
&& VideoLayout.CENTER_REMOTE.equals(
videoLayout.getComponentConstraints(c)))
{
hasComponentsAtCenterRemote = true;
break;
}
}
if (!hasComponentsAtCenterRemote
&& (noVideoComponent != null)
&& !noVideoComponent.equals(comp))
{
add(noVideoComponent, VideoLayout.CENTER_REMOTE);
validate();
}
removeCanvasIfNecessary();
}
/**
* Ensures noVideoComponent is displayed even when the clients of the
* videoContainer invoke its #removeAll() to remove their previous visual
* Components representing video. Just adding noVideoComponent upon
* ContainerEvent#COMPONENT_REMOVED when there is no other Component left in
* the Container will cause an infinite loop because Container#removeAll()
* will detect that a new Component has been added while dispatching the
* event and will then try to remove the new Component.
*/
@Override
public void removeAll()
{
super.removeAll();
if ((canvas != null) && (canvas.getParent() != this))
canvas = null;
if (noVideoComponent != null)
{
add(noVideoComponent, VideoLayout.CENTER_REMOTE);
validate();
}
}
/**
* Removes {@link #canvas} from this VideoContainer if no sibling
* Component needs it.
*/
public void removeCanvasIfNecessary()
{
if ((canvas == null) || !OSUtils.IS_MAC)
return;
boolean removeCanvas = true;
for (Component component : getComponents())
{
if (component == canvas)
continue;
try
{
component.getClass().getMethod(
VIDEO_CANVAS_FACTORY_METHOD_NAME);
removeCanvas = false;
break;
}
catch (NoSuchMethodException nsme)
{
/*
* Ignore it because we already presume that component does not
* need the canvas.
*/
}
}
if (removeCanvas)
remove(canvas);
}
}