All Downloads are FREE. Search and download functionalities are using the official Maven repository.

it.tidalwave.netbeans.visual.LayeredSceneView Maven / Gradle / Ivy

The newest version!
/***********************************************************************************************************************
 *
 * OpenBlueSky - NetBeans Platform Enhancements
 * Copyright (C) 2006-2012 by Tidalwave s.a.s. (http://www.tidalwave.it)
 *
 ***********************************************************************************************************************
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations under the License.
 *
 ***********************************************************************************************************************
 *
 * WWW: http://openbluesky.java.net
 * SCM: https://bitbucket.org/tidalwave/openbluesky-src
 *
 **********************************************************************************************************************/
package it.tidalwave.netbeans.visual;

import javax.annotation.CheckForNull;
import javax.annotation.Nullable;
import javax.annotation.Nonnull;
import java.lang.reflect.Method;
import java.beans.PropertyChangeListener;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.Point;
import java.awt.event.MouseListener;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelListener;
import javax.swing.BorderFactory;
import javax.swing.JComponent;
import javax.swing.JLayeredPane;
import javax.swing.OverlayLayout;
import java.io.Serializable;
import org.openide.util.WeakListeners;
import org.netbeans.api.visual.widget.Widget;
import org.netbeans.api.visual.widget.Scene.SceneListener;
import org.netbeans.api.visual.model.ObjectScene;
import it.tidalwave.util.logging.Logger;
import it.tidalwave.netbeans.visual.impl.MouseEventForwarder;
import it.tidalwave.netbeans.visual.impl.RevalidateTrigger;
import it.tidalwave.netbeans.visual.impl.SceneDropTarget;
import java.awt.datatransfer.DataFlavor;

/***********************************************************************************************************************
 *
 * This class allows to create a layered view of an {@link ObjectScene}, in which contents can be rendered below
 * (background) and in front (foreground) of the {@code Widget}s, that are achored to meaningful positions in function
 * of the background. This means that {@code Widget}s can be made "stick" to background contents even when the latter
 * change or move. A typical use is with a map in the foreground and {@code Widget}s represent features sticking on it.
 *
 * The position of each {@code Widget} is computed by a scene-wise {@link LocationProvider} that is called whenever
 * the {@code Scene} is revalidated. The {@code Scene} must be revalidated each time the background or foreground
 * contents are changed or moved. If the component responsible for rendering the background or the foreground fires
 * event in such a case, the {@link #addRevalidateTrigger(java.lang.Object, java.lang.String[])} method can be
 * conveniently used on that purpose.
 *
 * @author  Fabrizio Giudici
 * @version $Id$
 *
 **********************************************************************************************************************/
public class LayeredSceneView extends JLayeredPane
  {
    private static final String CLASS = LayeredSceneView.class.getName();
    private static final Logger logger = Logger.getLogger(CLASS);

    /*******************************************************************************************************************
     *
     * An instance of this class must be provided to each {@link LayeredSceneView} to compute the positions of
     * {@link Widget}s.
     *
     ******************************************************************************************************************/
    public static interface LocationProvider extends Serializable
      {
        /***************************************************************************************************************
         *
         * This method computes the position where the {@link Widget} associated to the given {@code Object} should be
         * placed. This method is called whenever the {@code Scene} is validated. The {@code Object} is the same
         * instance that has been associated to the {@link Widget} by the {@code ObjectScene}.
         *
         * It is possible to return {@code null} to mean that the relevant {@code Widget} position must not be updated.
         * This can happen, for instance, in mixed contexts where only part of the {@code Widget}s must get stuck on the
         * background or when some of the {@code Widget}s are anchored to others.
         *
         * @param  object  the object
         * @return         the position
         *
         **************************************************************************************************************/
        @Nullable
        public Point findLocation (@Nonnull T object);
      }

    private final static Dimension ZERO = new Dimension(0, 0);

    private static final LocationProvider VOID_LOCATION_PROVIDER =  new LayeredSceneView.LocationProvider()
      {
        @Override
        public Point findLocation (final @Nonnull Object object)
          {
           return null;
          }
      };

    @Nonnull
    private final ObjectScene scene;

    @Nonnull
    private final JComponent sceneComponent;
    
    @Nonnull
    private final MouseEventForwarder mouseEventForwarder;
    
    private final LocationProvider locationProvider;

    /** Keeps alive the triggers - needed because we're using WeakListeners. */
    private final List revalidateTriggersKeeper = new ArrayList();

    /** The handler of drop events. */
    @Nonnull
    private final SceneDropTarget dropTarget;

    /*******************************************************************************************************************
     *
     *
     ******************************************************************************************************************/
    private final SceneListener sceneListener = new SceneListener()
      {
        @Override
        public void sceneRepaint()
          {
          }

        @Override
        public void sceneValidating()
          {
            updateWidgetPositions();
          }

        @Override
        public void sceneValidated()
          {
          }
      };

    /*******************************************************************************************************************
     *
     * Creates a new instance of {@code LayeredSceneView}. 
     *
     * @param   scene                the {@link ObjectScene}
     *
     ******************************************************************************************************************/
    public LayeredSceneView (final @Nonnull ObjectScene scene)
      {
        this(scene, VOID_LOCATION_PROVIDER, (MouseEventForwarder)null);
      }

    /*******************************************************************************************************************
     *
     * Creates a new instance of {@code LayeredSceneView}.
     *
     * @param   scene                 the {@link ObjectScene}
     * @param   locationProvider      the provider of {@link Widget} positions
     * @param   backgroundComponent   the component to be rendered in background
     *
     ******************************************************************************************************************/
    public LayeredSceneView (final @Nonnull ObjectScene scene,
                             final @Nonnull LocationProvider locationProvider,
                             final @Nonnull Component backgroundComponent)
      {
        this(scene, locationProvider, new MouseEventForwarder(scene, backgroundComponent));
        add(backgroundComponent, Integer.valueOf(100)); // autoboxing won't work here!
        sceneComponent.addMouseMotionListener(WeakListeners.create(MouseMotionListener.class, mouseEventForwarder, sceneComponent));
        sceneComponent.addMouseWheelListener(WeakListeners.create(MouseWheelListener.class, mouseEventForwarder, sceneComponent));
        sceneComponent.addMouseListener(WeakListeners.create(MouseListener.class, mouseEventForwarder, sceneComponent));
      }

    /*******************************************************************************************************************
     *
     * Creates a new instance of {@code LayeredSceneView}.
     *
     * @param   scene                 the {@link ObjectScene}
     * @param   locationProvider      the provider of {@link Widget} positions
     * @param   backgroundComponent   the component to be rendered in background
     * @param   foregroundComponent   the component to be rendered in foreground
     *
     ******************************************************************************************************************/
    public LayeredSceneView (final @Nonnull ObjectScene scene,
                             final @Nonnull LocationProvider locationProvider,
                             final @Nonnull Component backgroundComponent,
                             final @Nonnull Component foregroundComponent)
      {
        this(scene, locationProvider, backgroundComponent);
        add(foregroundComponent, Integer.valueOf(300));
      }

    /*******************************************************************************************************************
     *
     * Creates a new instance of {@code LayeredSceneView}.
     *
     ******************************************************************************************************************/
    private LayeredSceneView (final @Nonnull ObjectScene scene,
                              final @Nonnull LocationProvider locationProvider,
                              final @CheckForNull MouseEventForwarder mouseEventForwarder)
      {
        this.scene = scene;
        this.sceneComponent = scene.createView();
        this.locationProvider = locationProvider;
        this.mouseEventForwarder = mouseEventForwarder;

        setBorder(BorderFactory.createEmptyBorder());
        setLayout(new OverlayLayout(this));
        add(sceneComponent, Integer.valueOf(200));

        scene.addSceneListener(sceneListener);
//        scene.addSceneListener(WeakListeners.create(SceneListener.class, sceneListener, scene)); FIXME
        dropTarget = new SceneDropTarget(scene);
        sceneComponent.setDropTarget(dropTarget);
      }

    /*******************************************************************************************************************
     *
     * Adds a trigger that automatically revalidates the {@link Scene}, this updating the positions of all the
     * {@link Widget}s, whenever one of the specified properties is changed on the specified object. The object must
     * support bound properties according to the JavaBeans specification.
     *
     * @param   object               the object firing the properties
     * @param   triggerProperties    the list of properties
     *
     ******************************************************************************************************************/
    public void addRevalidateTrigger (final @Nonnull Object object, final @Nonnull String ... triggerProperties)
      {
        final RevalidateTrigger revalidateTrigger = new RevalidateTrigger(scene, triggerProperties);
        revalidateTriggersKeeper.add(revalidateTrigger);
        
        try
          {
            final Method method = object.getClass().getMethod("addPropertyChangeListener", PropertyChangeListener.class);
            method.invoke(object, WeakListeners.propertyChange(revalidateTrigger, object));
          }
        catch (RuntimeException e)
          {
            throw e;
          }
        catch (Exception e)
          {
            throw new IllegalArgumentException("Can't call addPropertyChangeListener() on " + object, e);
          }
      }

    /*******************************************************************************************************************
     *
     * Registers a {@link DropAcceptor} for a given {@DataFlavor}.
     *
     * @param  dataFlavor    the {@code DataFlavor}
     * @param  dropAcceptor  the {@code DropAcceptor}
     *
     ******************************************************************************************************************/
    public void registerDropAcceptor (final @Nonnull DataFlavor dataFlavor,
                                      final @Nonnull DropAcceptor dropAcceptor)
      {
        dropTarget.registerDropAcceptor(dataFlavor, dropAcceptor);
      }
    
    /*******************************************************************************************************************
     *
     *
     ******************************************************************************************************************/
    private void updateWidgetPositions()
      {
        logger.fine("updateWidgetPositions()");

        for (final Widget widget : getChildrenRecursively(scene))
          {
            updateWidgetPosition(widget);
          }

        logger.finer(">>>> updateWidgetPositions done");
      }

    /*******************************************************************************************************************
     *
     *
     ******************************************************************************************************************/
    private void updateWidgetPosition (final @Nonnull Widget widget)
      {
        logger.fine("updateWidgetPosition(%s)", widget);
        Point viewLocation = null;
        final Object object = scene.findObject(widget);

        if (object != null)
          {
            viewLocation = locationProvider.findLocation(object);
          }

        if (viewLocation != null)
          {
//            if (viewLocation.getX() < -1000) // FIXME: sometimes here get some large negatives that screw up
//              {
//                logger.warning("BUGGY COORDINATES? " + viewLocation);
//              }
//            else
//              {
                final Point sceneLocation = scene.convertViewToScene(viewLocation);
                final Point localLocation = scene.convertSceneToLocal(sceneLocation);
                Dimension size = widget.getPreferredSize();

                if (size == null)
                  {
                    size = ZERO;
                  }
                
                widget.setPreferredLocation(new Point(localLocation.x - size.width / 2,
                                                      localLocation.y - size.height / 2));
//              }
          }
      }

    /*******************************************************************************************************************
     *
     *
     ******************************************************************************************************************/
    @Nonnull
    private static Collection getChildrenRecursively (final @Nonnull Widget parent)
      {
        final List result = new ArrayList();
        final List children = parent.getChildren();
        result.addAll(children);

        for (final Widget widget : children)
          {
            result.addAll(getChildrenRecursively(widget));
          }

        return result;
      }
  }