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

com.alee.extended.dock.WebDockablePaneModel Maven / Gradle / Ivy

There is a newer version: 1.2.14
Show newest version
/*
 * This file is part of WebLookAndFeel library.
 *
 * WebLookAndFeel library is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * WebLookAndFeel library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with WebLookAndFeel library.  If not, see .
 */

package com.alee.extended.dock;

import com.alee.api.annotations.NotNull;
import com.alee.api.annotations.Nullable;
import com.alee.api.data.CompassDirection;
import com.alee.api.data.Orientation;
import com.alee.api.jdk.Objects;
import com.alee.extended.dock.data.*;
import com.alee.extended.dock.drag.FrameDragData;
import com.alee.extended.dock.drag.FrameDropData;
import com.alee.extended.dock.drag.FrameTransferable;
import com.alee.laf.grouping.AbstractGroupingLayout;
import com.alee.laf.window.WebDialog;
import com.alee.painter.decoration.DecorationUtils;
import com.alee.utils.CollectionUtils;
import com.alee.utils.CoreSwingUtils;
import com.alee.utils.general.Pair;
import com.thoughtworks.xstream.annotations.XStreamAlias;

import javax.swing.*;
import java.awt.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static com.alee.api.data.CompassDirection.*;
import static com.alee.api.data.Orientation.horizontal;
import static com.alee.api.data.Orientation.vertical;

/**
 * Basic {@link DockablePaneModel} implementation.
 * It handles elements location data and also provides appropriate layout for all elements on the dockable pane.
 * It also provides frames drag data and various methods to modify element locations.
 * todo Separate layout from the model
 *
 * @author Mikle Garin
 * @see How to use WebDockablePane
 * @see WebDockablePane
 * @see DockablePaneModel
 */
@XStreamAlias ( "DockablePaneModel" )
public class WebDockablePaneModel extends AbstractGroupingLayout implements DockablePaneModel
{
    /**
     * Side width used to calculated side frame attachment area.
     */
    protected static final int dropSide = 40;

    /**
     * Root element on the {@link WebDockablePane}.
     */
    @SuppressWarnings ( "NullableProblems" )
    @NotNull
    protected DockableContainer root;

    /**
     * Content element reference.
     */
    @SuppressWarnings ( "NullableProblems" )
    @NotNull
    protected DockableContentElement content;

    /**
     * Sidebar widths cache.
     */
    @NotNull
    protected transient final Map sidebarSizes;

    /**
     * Resizable areas cache.
     * Used to optimize resize areas detection.
     */
    @NotNull
    protected transient final List resizableAreas;

    /**
     * Preview frame bounds.
     * Saved to check intersection with resizable areas.
     */
    @Nullable
    protected transient Rectangle previewBounds;

    /**
     * Constructs new {@link DockablePaneModel} implementation with only content in its structure.
     */
    public WebDockablePaneModel ()
    {
        this ( new DockableListContainer ( horizontal, new DockableContentElement () ) );
    }

    /**
     * Constructs new {@link DockablePaneModel} implementation with the specified structure of the elements.
     *
     * @param root root container on the {@link WebDockablePane}
     */
    public WebDockablePaneModel ( @NotNull final DockableContainer root )
    {
        this.groupButtons = false;
        this.sidebarSizes = new HashMap ();
        this.resizableAreas = new ArrayList ();
        this.previewBounds = null;
        setRoot ( root );
    }

    @NotNull
    @Override
    public DockableContainer getRoot ()
    {
        return root;
    }

    @Override
    public void setRoot ( @NotNull final DockableContainer root )
    {
        this.root = root;
        this.content = root.get ( DockableContentElement.ID );
        this.root.added ( null );
    }

    @NotNull
    @Override
    public  T getElement ( @NotNull final String id )
    {
        return root.get ( id );
    }

    @Override
    public void updateFrame ( @NotNull final WebDockablePane dockablePane, @NotNull final WebDockableFrame frame )
    {
        // Check whether or not frame data is already available
        if ( root.contains ( frame.getId () ) )
        {
            // Restoring frame states from model
            final DockableFrameElement element = root.get ( frame.getId () );
            frame.setState ( element.getState () );
            frame.setRestoreState ( element.getRestoreState () );
        }
        else
        {
            // Model didn't store state for this frame, creating new one
            final DockableFrameElement element = new DockableFrameElement ( frame );
            addStructureElement ( content, element, frame.getPosition () );
        }

        // Ensuring frame position is correct
        final CompassDirection position = getFramePosition ( frame );
        frame.setPosition ( position );

        // Updating grouping
        resetDescriptors ();
    }

    @Override
    public void removeFrame ( @NotNull final WebDockablePane dockablePane, @NotNull final WebDockableFrame frame )
    {
        if ( root.contains ( frame.getId () ) )
        {
            // Removing frame state
            final DockableFrameElement element = root.get ( frame.getId () );
            removeStructureElement ( element );

            // Updating grouping
            resetDescriptors ();
        }
    }

    /**
     * Adds specified {@code newElement} relative to another {@code element} shifting to the specified {@code direction}.
     *
     * @param element    element relative to which {@code newElement} should be added
     * @param newElement element to add
     * @param direction  placement direction
     */
    protected void addStructureElement ( @NotNull final DockableElement element, @NotNull final DockableElement newElement,
                                         @NotNull final CompassDirection direction )
    {
        final Orientation orientation = direction == north || direction == south ? vertical : horizontal;
        final DockableContainer parent = element.getParent ();
        if ( parent == null )
        {
            if ( orientation == root.getOrientation () || root.getElementCount () <= 1 )
            {
                if ( direction == north || direction == west )
                {
                    // Redirecting addition
                    addStructureElement ( root.get ( 0 ), newElement, direction );
                }
                else if ( direction == south || direction == east )
                {
                    // Redirecting addition
                    addStructureElement ( root.get ( root.getElementCount () - 1 ), newElement, direction );
                }
                else
                {
                    throw new RuntimeException ( "Unknown element position specified" );
                }
            }
            else
            {
                // Creating new root container
                root = new DockableListContainer ( orientation, element );

                // Redirecting addition
                addStructureElement ( element, newElement, direction );
            }
        }
        else
        {
            final int index = parent.indexOf ( element );
            if ( orientation == parent.getOrientation () || parent.getElementCount () <= 1 )
            {
                // Ensure orientation is correct
                parent.setOrientation ( orientation );

                // Add element to either start or end of the container
                if ( orientation.isHorizontal () && direction == west || orientation.isVertical () && direction == north )
                {
                    parent.add ( index, newElement );
                }
                else if ( orientation.isHorizontal () && direction == east || orientation.isVertical () && direction == south )
                {
                    parent.add ( index + 1, newElement );
                }
                else
                {
                    throw new RuntimeException ( "Unknown element position specified" );
                }
            }
            else
            {
                // Saving fraction to keep it intact
                final Dimension size = element.getSize ();

                // Removing initial relation element
                parent.remove ( element );

                // Creating new list container
                final DockableListContainer list = new DockableListContainer ( orientation, element );
                if ( !list.isContent () )
                {
                    list.setSize ( size );
                }
                parent.add ( index, list );

                // Redirecting addition
                addStructureElement ( element, newElement, direction );
            }
        }
    }

    /**
     * Removes specified {@code element} from the model structure.
     *
     * @param element element to remove
     */
    protected void removeStructureElement ( @NotNull final DockableElement element )
    {
        if ( element.getParent () == null )
        {
            throw new RuntimeException ( "Structure element cannot be removed" );
        }
        else if ( element == content )
        {
            throw new RuntimeException ( "Content element cannot be removed" );
        }
        else
        {
            final DockableContainer container = element.getParent ();

            // Removing element from container
            container.remove ( element );

            // Removing redundant container
            if ( container.getParent () != null && container.getElementCount () <= 1 )
            {
                // Moving single child up
                if ( container.getElementCount () == 1 )
                {
                    // Retrieving container's location in it's parent
                    final DockableContainer containerParent = container.getParent ();
                    final int index = containerParent.indexOf ( container );

                    // Updating last child that we're moving up
                    // This is only needed for non-content elements or containers
                    final DockableElement lastChild = container.get ( 0 );
                    if ( !( lastChild instanceof DockableContentElement ) )
                    {
                        // We need to preserve length container has before we get rid of it
                        final Dimension oldChildSize = lastChild.getSize ();
                        final Dimension containerSize = container.getSize ();
                        final boolean horizontal = containerParent.getOrientation ().isHorizontal ();
                        lastChild.setSize ( new Dimension (
                                horizontal ? containerSize.width : oldChildSize.width,
                                horizontal ? oldChildSize.height : containerSize.height
                        ) );
                    }

                    // Moving last child up
                    containerParent.add ( index, lastChild );
                }

                // Removing empty container
                removeStructureElement ( container );
            }
        }
    }

    @Nullable
    @Override
    public FrameDropData dropData ( @NotNull final WebDockablePane dockablePane, @NotNull final TransferHandler.TransferSupport support )
    {
        FrameDropData dropData = null;
        if ( support.getTransferable ().isDataFlavorSupported ( FrameTransferable.dataFlavor ) )
        {
            try
            {
                // Retrieving draged frame data
                final FrameDragData drag = ( FrameDragData ) support.getTransferable ().getTransferData ( FrameTransferable.dataFlavor );

                // Checking frame existence on this dockable pane
                // This is needed to avoid drag between different panes
                if ( dockablePane.findFrame ( drag.getId () ) != null )
                {
                    // Basic drag information
                    final String id = drag.getId ();
                    final Point dropPoint = support.getDropLocation ().getDropPoint ();

                    // Checking global side drop
                    final Rectangle innerBounds = getInnerBounds ( dockablePane );
                    final FrameDropData globalDropData = createDropData ( id, root, innerBounds, dropPoint, dropSide );
                    if ( globalDropData != null )
                    {
                        // Global drop data
                        dropData = globalDropData;
                    }
                    else
                    {
                        // Checking content side drop
                        // We cannot use "getComponentAt" method here as it will point at glass pane
                        final JComponent paneContent = dockablePane.getContent ();
                        final Point paneLocation = paneContent != null ? paneContent.getLocation () : null;
                        if ( paneContent != null && paneContent.contains ( dropPoint.x - paneLocation.x, dropPoint.y - paneLocation.y ) )
                        {
                            final Rectangle dropBounds = getDropBounds ( paneContent );
                            dropData = createDropData ( id, content, dropBounds, dropPoint, dropSide * 2 );
                        }
                        else
                        {
                            // Checking frame side drop
                            // We cannot use "getComponentAt" method here as it will point at glass pane
                            for ( final WebDockableFrame paneFrame : dockablePane.getFrames () )
                            {
                                // Ensure frame is showing and it is not the dragged frame
                                if ( paneFrame.isDocked () && Objects.notEquals ( paneFrame.getId (), id ) )
                                {
                                    final Point location = paneFrame.getLocation ();
                                    if ( paneFrame.contains ( dropPoint.x - location.x, dropPoint.y - location.y ) )
                                    {
                                        final DockableElement element = root.get ( paneFrame.getId () );
                                        final Rectangle dropBounds = getDropBounds ( paneFrame );
                                        dropData = createDropData ( id, element, dropBounds, dropPoint, dropSide * 2 );
                                        break;
                                    }
                                }
                            }
                        }
                    }
                }
            }
            catch ( final Exception e )
            {
                throw new RuntimeException ( "Unknown frame drop exception occurred", e );
            }
        }
        return dropData;
    }

    /**
     * Returns drop location bounds.
     *
     * @param component drop location component
     * @return drop location bounds
     */
    @NotNull
    protected Rectangle getDropBounds ( @NotNull final JComponent component )
    {
        final Rectangle bounds = component.getBounds ();
        final Insets i = component.getInsets ();
        bounds.x += i.left - 1;
        bounds.y += i.top - 1;
        bounds.width -= i.left + i.right - 1;
        bounds.height -= i.top + i.bottom - 1;
        return bounds;
    }

    /**
     * Returns component drop data.
     *
     * @param id        dragged frame ID
     * @param element   element currently placed at the drop location
     * @param bounds    drop location bounds
     * @param dropPoint drop point
     * @param dropSide  size of droppable side
     * @return component drop data
     */
    @Nullable
    protected FrameDropData createDropData ( @NotNull final String id, @NotNull final DockableElement element,
                                             @NotNull final Rectangle bounds, @NotNull final Point dropPoint, final int dropSide )
    {
        // Calculating drop direction
        final boolean wSmall = dropSide > bounds.width / 2;
        final boolean hSmall = dropSide > bounds.height / 2;
        final int xSide = Math.min ( dropSide, bounds.width / 2 );
        final int ySide = Math.min ( dropSide, bounds.height / 2 );
        final boolean w = dropPoint.x <= bounds.x + xSide;
        final boolean e = dropPoint.x >= bounds.x + bounds.width - xSide;
        final boolean n = dropPoint.y <= bounds.y + ySide;
        final boolean s = dropPoint.y >= bounds.y + bounds.height - ySide;
        final CompassDirection direction;
        if ( wSmall || hSmall )
        {
            // Special behavior in case width or height are too small
            if ( wSmall )
            {
                // Prioritize north and south zones
                direction = n ? north : s ? south : w ? west : e ? east : null;
            }
            else
            {
                // Prioritize west and east zones
                direction = w ? west : e ? east : n ? north : s ? south : null;
            }
        }
        else if ( w && n )
        {
            // NW corner drop behavior
            direction = dropPoint.x - bounds.x > dropPoint.y - bounds.y ? north : west;
        }
        else if ( w && s )
        {
            // SW corner drop behavior
            direction = dropPoint.x - bounds.x > bounds.y + bounds.height - dropPoint.y ? south : west;
        }
        else if ( e && n )
        {
            // NE corner drop behavior
            direction = bounds.x + bounds.width - dropPoint.x > dropPoint.y - bounds.y ? north : east;
        }
        else if ( e && s )
        {
            // SE corner drop behavior
            direction = bounds.x + bounds.width - dropPoint.x > bounds.y + bounds.height - dropPoint.y ? south : east;
        }
        else
        {
            // General drop behavior
            direction = n ? north : s ? south : w ? west : e ? east : null;
        }

        // Creating drop data
        final FrameDropData dropData;
        if ( direction != null )
        {
            switch ( direction )
            {
                case west:
                    bounds.width = xSide;
                    break;

                case east:
                    bounds.x = bounds.x + bounds.width - xSide;
                    bounds.width = xSide;
                    break;

                case north:
                    bounds.height = ySide;
                    break;

                case south:
                    bounds.y = bounds.y + bounds.height - ySide;
                    bounds.height = ySide;
                    break;
            }
            dropData = new FrameDropData ( id, bounds, element, direction );
        }
        else
        {
            dropData = null;
        }
        return dropData;
    }

    @Override
    public boolean drop ( @NotNull final WebDockablePane dockablePane, @NotNull final TransferHandler.TransferSupport support )
    {
        final FrameDropData dropData = dropData ( dockablePane, support );
        if ( dropData != null )
        {
            // Dropped element
            final DockableElement element = root.get ( dropData.getId () );

            // Removing frame from initial location first
            removeStructureElement ( element );

            // Adding frame to target location
            addStructureElement ( dropData.getElement (), element, dropData.getDirection () );

            // Updating frame position
            final WebDockableFrame frame = dockablePane.getFrame ( element.getId () );
            final CompassDirection position = getFramePosition ( frame );
            frame.setPosition ( position );

            // Updating grouping
            resetDescriptors ();

            // Updating dockable
            dockablePane.revalidate ();
            dockablePane.repaint ();

            // Informing frame listeners
            frame.fireFrameMoved ( position );
        }
        return false;
    }

    @Override
    public void layoutContainer ( @NotNull final Container container )
    {
        // Base settings
        final WebDockablePane dockablePane = ( WebDockablePane ) container;
        final int w = dockablePane.getWidth ();
        final int h = dockablePane.getHeight ();
        final boolean ltr = dockablePane.getComponentOrientation ().isLeftToRight ();

        // Outer bounds, sidebars are placed within these bounds
        final Rectangle outer = getOuterBounds ( dockablePane );

        // Positioning sidebar elements
        final List locations = CollectionUtils.asList ( north, west, south, east );
        final Map> allButtons = new HashMap> ();
        for ( final CompassDirection location : locations )
        {
            final List barButtons = getVisibleButtons ( dockablePane, location );
            allButtons.put ( location, barButtons );
            sidebarSizes.put ( location, calculateBarSize ( location, barButtons ) );
        }
        for ( final CompassDirection location : locations )
        {
            // Retrieving bar cache
            final List buttons = allButtons.get ( location );
            if ( buttons.size () > 0 )
            {
                // Calculating bar bounds
                final int barWidth = sidebarSizes.get ( location );
                final Rectangle bounds;
                final int fw;
                final int lw;
                switch ( location )
                {
                    case north:
                        fw = sidebarSizes.get ( west );
                        lw = sidebarSizes.get ( east );
                        bounds = new Rectangle ( outer.x + fw, outer.y, outer.width - fw - lw, barWidth );
                        break;

                    case west:
                        fw = sidebarSizes.get ( north );
                        lw = sidebarSizes.get ( south );
                        if ( ltr )
                        {
                            bounds = new Rectangle ( outer.x, outer.y + fw, barWidth, outer.height - fw - lw );
                        }
                        else
                        {
                            bounds = new Rectangle ( outer.x + outer.width - barWidth, outer.y + fw, barWidth, outer.height - fw - lw );
                        }
                        break;

                    case south:
                        fw = sidebarSizes.get ( west );
                        lw = sidebarSizes.get ( east );
                        bounds = new Rectangle ( outer.x + fw, outer.y + outer.height - barWidth, outer.width - fw - lw, barWidth );
                        break;

                    case east:
                        fw = sidebarSizes.get ( north );
                        lw = sidebarSizes.get ( south );
                        if ( ltr )
                        {
                            bounds = new Rectangle ( outer.x + outer.width - barWidth, outer.y + fw, barWidth, outer.height - fw - lw );
                        }
                        else
                        {
                            bounds = new Rectangle ( outer.x, outer.y + fw, barWidth, outer.height - fw - lw );
                        }
                        break;

                    default:
                        throw new RuntimeException ( "Unknown location specified: " + location );
                }

                // Placing buttons
                int x = bounds.x;
                int y = bounds.y;
                for ( final JComponent button : buttons )
                {
                    final Dimension bps = button.getPreferredSize ();
                    if ( location == north || location == south )
                    {
                        // Horizontal placement
                        button.setBounds ( x, y, bps.width, barWidth );
                        x += button.getWidth ();
                    }
                    else
                    {
                        // Vertical placement
                        button.setBounds ( x, y, barWidth, bps.height );
                        y += button.getHeight ();
                    }
                }
            }
        }

        // Inner bounds, frames and content are placed within these bounds
        final Rectangle inner = getInnerBounds ( dockablePane );

        // Looking for special frames
        // There could be only single preview and maximized frame at a time
        WebDockableFrame preview = null;
        WebDockableFrame maximized = null;
        for ( final WebDockableFrame frame : dockablePane.frames )
        {
            if ( frame.isPreview () )
            {
                preview = frame;
            }
            else if ( frame.isDocked () && frame.isMaximized () )
            {
                maximized = frame;
            }
        }

        // Positioning frames and content
        resizableAreas.clear ();
        if ( maximized == null )
        {
            // Placing frames normally
            root.setSize ( inner.getSize () );
            root.layout ( dockablePane, inner, resizableAreas );
        }
        else
        {
            // Simply hide all frames behind visible area
            // This is required to avoid issues with opaque components on other frames overlapping with maximized one
            // Plus this also brings some level of rendering optimization so it is a convenient thing to do
            for ( final WebDockableFrame frame : dockablePane.frames )
            {
                if ( frame != preview && frame != maximized && frame.isDocked () )
                {
                    frame.setBounds ( 0, 0, 0, 0 );
                }
            }
        }

        // Positioning preview frame
        if ( preview != null )
        {
            // Retrieving preview bounds
            previewBounds = getPreviewBounds ( dockablePane, preview );

            // Updating frame bounds
            preview.setBounds ( previewBounds );
            root.get ( preview.getId () ).setBounds ( previewBounds );

            // Moving frame to the topmost possible Z-index after glass pane
            dockablePane.setComponentZOrder ( preview, 1 );
        }
        else
        {
            // Resetting preview bounds
            previewBounds = null;
        }

        // Positioning maximized frame
        if ( maximized != null )
        {
            // Updating frame bounds
            maximized.setBounds ( outer );
            root.get ( maximized.getId () ).setBounds ( outer );

            // Moving frame to the topmost possible Z-index after glass pane and preview frame
            dockablePane.setComponentZOrder ( maximized, preview != null ? 2 : 1 );
        }

        // Positioning glass layer
        // It is placed over whole dockable pane for calculations convenience
        final JComponent glassLayer = dockablePane.getGlassLayer ();
        if ( glassLayer != null )
        {
            glassLayer.setBounds ( 0, 0, w, h );
        }
    }

    @NotNull
    @Override
    public Rectangle getOuterBounds ( @NotNull final WebDockablePane dockablePane )
    {
        final int w = dockablePane.getWidth ();
        final int h = dockablePane.getHeight ();
        final Insets bi = dockablePane.getInsets ();
        return new Rectangle ( bi.left, bi.top, w - bi.left - bi.right, h - bi.top - bi.bottom );
    }

    @NotNull
    @Override
    public Rectangle getInnerBounds ( @NotNull final WebDockablePane dockablePane )
    {
        final Rectangle bounds = getOuterBounds ( dockablePane );
        if ( sidebarSizes.size () > 0 )
        {
            final boolean ltr = dockablePane.getComponentOrientation ().isLeftToRight ();

            int northSide = sidebarSizes.get ( north );
            if ( northSide > 0 )
            {
                northSide += dockablePane.getSidebarSpacing ();
            }

            int westSide = ltr ? sidebarSizes.get ( west ) : sidebarSizes.get ( east );
            if ( westSide > 0 )
            {
                westSide += dockablePane.getSidebarSpacing ();
            }

            int southSide = sidebarSizes.get ( south );
            if ( southSide > 0 )
            {
                southSide += dockablePane.getSidebarSpacing ();
            }

            int eastSide = ltr ? sidebarSizes.get ( east ) : sidebarSizes.get ( west );
            if ( eastSide > 0 )
            {
                eastSide += dockablePane.getSidebarSpacing ();
            }

            bounds.x += westSide;
            bounds.width -= westSide + eastSide;
            bounds.y += northSide;
            bounds.height -= northSide + southSide;
        }
        return bounds;
    }

    /**
     * Returns {@link DockableFrameState#preview} state bounds for the specified {@link WebDockableFrame}.
     *
     * @param dockablePane {@link WebDockablePane}
     * @param frame        {@link WebDockableFrame}
     * @return {@link DockableFrameState#preview} state bounds for the specified {@link WebDockableFrame}
     */
    @NotNull
    protected Rectangle getPreviewBounds ( @NotNull final WebDockablePane dockablePane, @NotNull final WebDockableFrame frame )
    {
        final Rectangle pb;
        final DockableElement element = root.get ( frame.getId () );
        final Rectangle inner = getInnerBounds ( dockablePane );
        switch ( frame.getPosition () )
        {
            case north:
            {
                final int height = Math.min ( element.getSize ().height, inner.height );
                pb = new Rectangle ( inner.x, inner.y, inner.width, height );
                break;
            }
            case west:
            {
                final int width = Math.min ( element.getSize ().width, inner.width );
                pb = new Rectangle ( inner.x, inner.y, width, inner.height );
                break;
            }
            case south:
            {
                final int height = Math.min ( element.getSize ().height, inner.height );
                pb = new Rectangle ( inner.x, inner.y + inner.height - height, inner.width, height );
                break;
            }
            case east:
            {
                final int width = Math.min ( element.getSize ().width, inner.width );
                pb = new Rectangle ( inner.x + inner.width - width, inner.y, width, inner.height );
                break;
            }
            default:
            {
                throw new RuntimeException ( "Unknown frame position: " + frame.getPosition () );
            }
        }
        return pb;
    }

    /**
     * Returns current position of the specified {@link WebDockableFrame} relative to content.
     *
     * @param frame {@link WebDockableFrame}
     * @return current position of the specified {@link WebDockableFrame} relative to content
     */
    @NotNull
    protected CompassDirection getFramePosition ( @NotNull final WebDockableFrame frame )
    {
        CompassDirection position = null;

        // Looking for frame relative to content position
        DockableContainer parent = content.getParent ();
        DockableElement previous = content;
        int divider;
        while ( parent != null )
        {
            // Divider element index
            divider = parent.indexOf ( previous );

            // Elements before content
            for ( int i = 0; i < divider; i++ )
            {
                final DockableElement element = parent.get ( i );
                if ( Objects.equals ( element.getId (), frame.getId () ) ||
                        element instanceof DockableContainer && ( ( DockableContainer ) element ).contains ( frame.getId () ) )
                {
                    position = parent.getOrientation ().isHorizontal () ? west : north;
                    break;
                }
            }
            if ( position != null )
            {
                break;
            }

            // Elements after content
            for ( int i = divider + 1; i < parent.getElementCount (); i++ )
            {
                final DockableElement element = parent.get ( i );
                if ( Objects.equals ( element.getId (), frame.getId () ) ||
                        element instanceof DockableContainer && ( ( DockableContainer ) element ).contains ( frame.getId () ) )
                {
                    position = parent.getOrientation ().isHorizontal () ? east : south;
                    break;
                }
            }
            if ( position != null )
            {
                break;
            }

            // Going higher in the structure
            previous = parent;
            parent = parent.getParent ();
        }

        // Position must be found
        if ( position == null )
        {
            throw new RuntimeException ( "Specified frame cannot be found in model: " + frame.getId () );
        }

        return position;
    }

    /**
     * Returns {@link List} of visible {@link SidebarButton}.
     * This method is necessary to ensure that {@link SidebarButton}s order doesn't depend on {@link WebDockableFrame}s order.
     * That is why we collect them from the elements structure rather than going through all {@link WebDockableFrame}s.
     *
     * @param dockablePane {@link WebDockablePane}
     * @param side         buttons side
     * @return {@link List} of visible {@link SidebarButton}
     */
    @NotNull
    protected List getVisibleButtons ( @NotNull final WebDockablePane dockablePane, @NotNull final CompassDirection side )
    {
        final List buttons = new ArrayList ( 3 );
        DockableContainer parent = content.getParent ();
        DockableElement previous = content;
        int divider;
        while ( parent != null )
        {
            // Divider element index
            divider = parent.indexOf ( previous );

            if ( parent.getOrientation ().isHorizontal () ? side == west : side == north )
            {
                // Elements before content
                for ( int i = 0; i < divider; i++ )
                {
                    collectVisibleButtons ( dockablePane, parent.get ( i ), buttons );
                }
            }
            else if ( parent.getOrientation ().isHorizontal () ? side == east : side == south )
            {
                // Elements after content
                for ( int i = divider + 1; i < parent.getElementCount (); i++ )
                {
                    collectVisibleButtons ( dockablePane, parent.get ( i ), buttons );
                }
            }

            // Going higher in the structure
            previous = parent;
            parent = parent.getParent ();
        }
        return buttons;
    }

    /**
     * Collects visible {@link SidebarButton}s from the specified {@link DockableElement}.
     *
     * @param dockablePane {@link WebDockablePane}
     * @param element      {@link DockableElement} to collect sidebar buttons from
     * @param buttons      {@link List} to add {@link SidebarButton} to
     */
    protected void collectVisibleButtons ( @NotNull final WebDockablePane dockablePane, @NotNull final DockableElement element,
                                           @NotNull final List buttons )
    {
        if ( element instanceof DockableFrameElement )
        {
            // Frame might not yet be added to the pane so we have to be careful here
            final WebDockableFrame frame = dockablePane.findFrame ( element.getId () );
            if ( frame != null )
            {
                if ( frame.isSidebarButtonVisible () )
                {
                    buttons.add ( frame.getSidebarButton () );
                }
            }
        }
        else if ( element instanceof DockableContainer )
        {
            final DockableContainer container = ( DockableContainer ) element;
            for ( int i = 0; i < container.getElementCount (); i++ )
            {
                collectVisibleButtons ( dockablePane, container.get ( i ), buttons );
            }
        }
    }

    /**
     * Returns sidebar width.
     *
     * @param side       buttons side
     * @param barButtons sidebar buttons
     * @return sidebar width
     */
    protected int calculateBarSize ( @NotNull final CompassDirection side, @NotNull final List barButtons )
    {
        int width = 0;
        for ( final JComponent component : barButtons )
        {
            final Dimension ps = component.getPreferredSize ();
            width = Math.max ( width, side == north || side == south ? ps.height : ps.width );
        }
        return width;
    }

    @NotNull
    @Override
    public Dimension preferredLayoutSize ( @NotNull final Container container )
    {
        // todo Use structure to recursively go through sizes
        return new Dimension ( 0, 0 );
    }

    @NotNull
    @Override
    public Pair getDescriptors ( @NotNull final Container container, @NotNull final Component component, final int index )
    {
        final Pair descriptors;
        final WebDockablePane dockablePane = ( WebDockablePane ) container;
        if ( dockablePane.isGroupElements () && dockablePane.getContentSpacing () == 0 )
        {
            if ( component == dockablePane.getContent () )
            {
                // We don't want to affect content decoration, so we'll just let developer handle it's visuals
                // Normally you wouldn't want to have any extra borders in content as they will be provided by frames and sidebars
                descriptors = new Pair ();
            }
            else if ( component instanceof SidebarButton )
            {
                // Buttons do not have any sides or lines
                // todo Probably let buttons have everything and only group them between eachother?
                // todo Or let them have left/right sides and other two are attached
                /*final SidebarButton sidebarButton = ( SidebarButton ) component;
                final WebDockableFrame frame = sidebarButton.getFrame ();
                final CompassDirection position = frame.getPosition ();*/
                descriptors = new Pair (
                        DecorationUtils.toString ( false, false, false, false ),
                        DecorationUtils.toString (
                                false,
                                false,
                                false /*position == west || position == east*/,
                                false /*position == north || position == south*/
                        )
                );
            }
            else if ( component instanceof WebDockableFrame )
            {
                final WebDockableFrame frame = ( WebDockableFrame ) component;
                if ( frame.isMaximized () )
                {
                    // Maximized frames don't need any sides or lines displayed
                    descriptors = new Pair (
                            DecorationUtils.toString ( false, false, false, false ),
                            DecorationUtils.toString ( false, false, false, false )
                    );
                }
                else
                {
                    // For non-maximized frames we have to account for their location
                    // It is pretty complex so it's separated into separate check methods
                    final DockableElement frameElement = root.get ( frame.getId () );
                    final DockableContainer frameContainer = frameElement.getParent ();
                    if ( frameContainer != null )
                    {
                        final String sides = DecorationUtils.toString ( false, false, false, false );
                        final String lines = DecorationUtils.toString (
                                isStartLineNeeded ( dockablePane, frameElement, frameContainer, vertical ),
                                isStartLineNeeded ( dockablePane, frameElement, frameContainer, horizontal ),
                                isEndLineNeeded ( dockablePane, frameElement, frameContainer, vertical ),
                                isEndLineNeeded ( dockablePane, frameElement, frameContainer, horizontal )
                        );
                        descriptors = new Pair ( sides, lines );
                    }
                    else
                    {
                        // This shouldn't normally happen
                        throw new RuntimeException ( "Frame doesn't have container: " + frame );
                    }
                }
            }
            else
            {
                // This shouldn't normally happen
                throw new RuntimeException ( "Unknown layout element: " + component );
            }
        }
        else
        {
            // Whenever grouping is disabled or spacing is greater than zero we let components handle their borders
            // It is simply not reasonable to setup any kinds of grouping when components will be at least few pixels away from each other
            descriptors = new Pair ();
        }
        return descriptors;
    }

    /**
     * Returns whether or not start line is needed for the specified {@link DockableElement}.
     * Condition is: (At start in pane for orientation && Start sidebar visible) || (Follows content side-to-side in line)
     *
     * @param dockablePane {@link WebDockablePane}
     * @param element      {@link DockableElement}
     * @param container    {@link DockableContainer} of the {@link DockableElement}
     * @param orientation  check {@link Orientation}
     * @return {@code true} if start line is needed for the specified {@link DockableElement}, {@code false} otherwise
     */
    protected boolean isStartLineNeeded ( @NotNull final WebDockablePane dockablePane, @NotNull final DockableElement element,
                                          @NotNull final DockableContainer container, @NotNull final Orientation orientation )
    {
        final boolean needed;
        final Orientation containerOrientation = container.getOrientation ();
        if ( containerOrientation == orientation )
        {
            // Our element side check orientation is equal to it's container orientation
            final DockableElement previous = getPreviousVisible ( dockablePane, container, element );
            if ( previous != null )
            {
                // Element is not first in container
                // We need to check if it is content or a container with content at the end
                needed = isContentAtTheEnd ( dockablePane, previous, orientation );
            }
            else
            {
                // Element is first in container
                // If we have parent - we don't know yet if we're first overall and we need to check further
                // If not - we are first element and start line is not needed, it will be handled by the sidebar or outer decoration
                final DockableContainer containerParent = container.getParent ();
                needed = containerParent != null && isStartLineNeeded ( dockablePane, container, containerParent, orientation );
            }
        }
        else
        {
            // Our element side check orientation is not equal to it's container orientation, we can proceed to parent right away
            // If we have parent - we don't know yet if we're first overall and we need to check further
            // If not - we are first element and start line is not needed, it will be handled by the sidebar or outer decoration
            final DockableContainer containerParent = container.getParent ();
            needed = containerParent != null && isStartLineNeeded ( dockablePane, container, containerParent, orientation );
        }
        return needed;
    }

    /**
     * Returns whether or not end line is needed for the specified {@link DockableElement}.
     * Condition is: (At end in pane for orientation && End sidebar visible) || (At end of container and content is at either side)
     *
     * @param dockablePane {@link WebDockablePane}
     * @param element      {@link DockableElement}
     * @param container    {@link DockableContainer} of the {@link DockableElement}
     * @param orientation  check {@link Orientation}
     * @return {@code true} if end line is needed for the specified {@link DockableElement}, {@code false} otherwise
     */
    protected boolean isEndLineNeeded ( @NotNull final WebDockablePane dockablePane, @NotNull final DockableElement element,
                                        @NotNull final DockableContainer container, @NotNull final Orientation orientation )
    {
        final boolean needed;
        final Orientation containerOrientation = container.getOrientation ();
        if ( containerOrientation == orientation )
        {
            // Our element side check orientation is equal to it's container orientation
            final DockableElement next = getNextVisible ( dockablePane, container, element );
            if ( next != null )
            {
                // Element is not last in container, end line always needed
                needed = true;
            }
            else
            {
                // Element is last in container
                // If we have parent - we don't know yet if we're last overall and we need to check further
                // If not - we are last element and end line is not needed, it will be handled by the sidebar or outer decoration
                final DockableContainer containerParent = container.getParent ();
                needed = containerParent != null && isEndLineNeeded ( dockablePane, container, containerParent, orientation );
            }
        }
        else
        {
            // Checking all neighbour elements to see if they have content at end
            boolean contentAtEndOfNearbyElement = false;
            for ( int index = 0; index < container.getElementCount (); index++ )
            {
                final DockableElement other = container.get ( index );
                if ( other != element && isContentAtTheEnd ( dockablePane, other, orientation ) )
                {
                    contentAtEndOfNearbyElement = true;
                    break;
                }
            }
            if ( contentAtEndOfNearbyElement )
            {
                // We don't need end line when we're in line with content end
                // Even if there is no other elements in front of us line will be handled by sidebar or outer decoration
                needed = false;
            }
            else
            {

                // Our element side check orientation is not equal to it's container orientation, we can proceed to parent right away
                // If we have parent - we don't know yet if we're last overall and we need to check further
                // If not - we are last element and end line is not needed, it will be handled by the sidebar or outer decoration
                final DockableContainer containerParent = container.getParent ();
                needed = containerParent != null && isEndLineNeeded ( dockablePane, container, containerParent, orientation );
            }
        }
        return needed;
    }

    /**
     * Returns whether or not specified {@link DockableElement} is content or has content at the start.
     *
     * @param dockablePane {@link WebDockablePane}
     * @param element      {@link DockableElement}
     * @param orientation  check {@link Orientation}
     * @return {@code true} if specified {@link DockableElement} is content or has content at the start, {@code false} otherwise
     */
    protected boolean isContentAtTheStart ( @NotNull final WebDockablePane dockablePane, @NotNull final DockableElement element,
                                            @NotNull final Orientation orientation )
    {
        final boolean contentAtTheStart;
        if ( element instanceof DockableContentElement )
        {
            contentAtTheStart = true;
        }
        else if ( element instanceof DockableContainer )
        {
            final DockableContainer container = ( DockableContainer ) element;
            if ( container.getOrientation () == orientation )
            {
                final DockableElement firstElement = getFirstVisible ( dockablePane, container );
                contentAtTheStart = firstElement != null && isContentAtTheStart ( dockablePane, firstElement, orientation );
            }
            else
            {
                boolean contentAtTheStartOfOne = false;
                for ( int index = 0; index < container.getElementCount (); index++ )
                {
                    if ( isContentAtTheStart ( dockablePane, container.get ( index ), orientation ) )
                    {
                        contentAtTheStartOfOne = true;
                        break;
                    }
                }
                contentAtTheStart = contentAtTheStartOfOne;
            }
        }
        else
        {
            contentAtTheStart = false;
        }
        return contentAtTheStart;
    }

    /**
     * Returns whether or not specified {@link DockableElement} is content or has content at the end.
     *
     * @param dockablePane {@link WebDockablePane}
     * @param element      {@link DockableElement}
     * @param orientation  check {@link Orientation}
     * @return {@code true} if specified {@link DockableElement} is content or has content at the end, {@code false} otherwise
     */
    protected boolean isContentAtTheEnd ( @NotNull final WebDockablePane dockablePane, @NotNull final DockableElement element,
                                          @NotNull final Orientation orientation )
    {
        final boolean contentAtTheEnd;
        if ( element instanceof DockableContentElement )
        {
            contentAtTheEnd = true;
        }
        else if ( element instanceof DockableContainer )
        {
            final DockableContainer container = ( DockableContainer ) element;
            if ( container.getOrientation () == orientation )
            {
                final DockableElement lastElement = getLastVisible ( dockablePane, container );
                contentAtTheEnd = lastElement != null && isContentAtTheEnd ( dockablePane, lastElement, orientation );
            }
            else
            {
                boolean contentAtTheEndOfOne = false;
                for ( int index = 0; index < container.getElementCount (); index++ )
                {
                    if ( isContentAtTheEnd ( dockablePane, container.get ( index ), orientation ) )
                    {
                        contentAtTheEndOfOne = true;
                        break;
                    }
                }
                contentAtTheEnd = contentAtTheEndOfOne;
            }
        }
        else
        {
            contentAtTheEnd = false;
        }
        return contentAtTheEnd;
    }

    /**
     * Returns previous visible sibling for the specified {@link DockableElement} in {@link DockableContainer}.
     *
     * @param dockablePane {@link WebDockablePane}
     * @param container    {@link DockableContainer}
     * @param element      {@link DockableElement} to find previous visible sibling for
     * @return previous visible sibling for the specified {@link DockableElement} in {@link DockableContainer}
     */
    @Nullable
    protected DockableElement getPreviousVisible ( @NotNull final WebDockablePane dockablePane, @NotNull final DockableContainer container,
                                                   @NotNull final DockableElement element )
    {
        DockableElement previousVisible = null;
        for ( int i = container.indexOf ( element ) - 1; i >= 0; i-- )
        {
            final DockableElement sibling = container.get ( i );
            if ( sibling.isVisible ( dockablePane ) )
            {
                previousVisible = sibling;
                break;
            }
        }
        return previousVisible;
    }

    /**
     * Returns next visible sibling for the specified {@link DockableElement} in {@link DockableContainer}.
     *
     * @param dockablePane {@link WebDockablePane}
     * @param container    {@link DockableContainer}
     * @param element      {@link DockableElement} to find next visible sibling for
     * @return next visible sibling for the specified {@link DockableElement} in {@link DockableContainer}
     */
    @Nullable
    protected DockableElement getNextVisible ( @NotNull final WebDockablePane dockablePane, @NotNull final DockableContainer container,
                                               @NotNull final DockableElement element )
    {
        DockableElement nextVisible = null;
        for ( int i = container.indexOf ( element ) + 1; i < container.getElementCount (); i++ )
        {
            final DockableElement sibling = container.get ( i );
            if ( sibling.isVisible ( dockablePane ) )
            {
                nextVisible = sibling;
                break;
            }
        }
        return nextVisible;
    }

    /**
     * Returns first visible {@link DockableElement} in {@link DockableContainer}.
     *
     * @param dockablePane {@link WebDockablePane}
     * @param container    {@link DockableContainer}
     * @return first visible {@link DockableElement} in {@link DockableContainer}
     */
    @Nullable
    protected DockableElement getFirstVisible ( @NotNull final WebDockablePane dockablePane, @NotNull final DockableContainer container )
    {
        DockableElement lastVisible = null;
        for ( int i = 0; i < container.getElementCount (); i++ )
        {
            final DockableElement element = container.get ( i );
            if ( element.isVisible ( dockablePane ) )
            {
                lastVisible = element;
                break;
            }
        }
        return lastVisible;
    }

    /**
     * Returns last visible {@link DockableElement} in {@link DockableContainer}.
     *
     * @param dockablePane {@link WebDockablePane}
     * @param container    {@link DockableContainer}
     * @return last visible {@link DockableElement} in {@link DockableContainer}
     */
    @Nullable
    protected DockableElement getLastVisible ( @NotNull final WebDockablePane dockablePane, @NotNull final DockableContainer container )
    {
        DockableElement lastVisible = null;
        for ( int i = container.getElementCount () - 1; i >= 0; i-- )
        {
            final DockableElement element = container.get ( i );
            if ( element.isVisible ( dockablePane ) )
            {
                lastVisible = element;
                break;
            }
        }
        return lastVisible;
    }

    @NotNull
    @Override
    public Rectangle getFloatingBounds ( @NotNull final WebDockablePane dockablePane, @NotNull final WebDockableFrame frame,
                                         @NotNull final WebDialog dialog )
    {
        final DockableFrameElement element = root.get ( frame.getId () );
        final Rectangle previewBounds;
        if ( element.getFloatingBounds () == null )
        {
            // Frame haven't been in floating state before
            // We will either use provided last bounds within dockable pane or simply use preview bounds
            final Rectangle lastBounds = element.getBounds ();
            previewBounds = lastBounds != null ? lastBounds : getPreviewBounds ( dockablePane, frame );

            // Adjusting bounds to location on screen
            final Point los = CoreSwingUtils.locationOnScreen ( dockablePane );
            previewBounds.x += los.x;
            previewBounds.y += los.y;
        }
        else
        {
            // Restoring previously saved floating bounds
            previewBounds = new Rectangle ( element.getFloatingBounds () );
        }

        // Adjusting location for dialog decorations
        final Insets rpi = dialog.getRootPane ().getInsets ();
        final Insets ci = dialog.getContentPane ().getInsets ();
        previewBounds.x -= rpi.left + ci.left;
        previewBounds.y -= rpi.top + ci.top;
        previewBounds.width += rpi.left + ci.left + ci.right + rpi.right;
        previewBounds.height += rpi.top + ci.top + ci.bottom + rpi.bottom;

        return previewBounds;
    }

    @Nullable
    @Override
    public ResizeData getResizeData ( final int x, final int y )
    {
        ResizeData resizeData = null;
        if ( previewBounds == null || !previewBounds.contains ( x, y ) )
        {
            for ( final ResizeData area : resizableAreas )
            {
                if ( area.bounds ().contains ( x, y ) )
                {
                    resizeData = area;
                    break;
                }
            }
        }
        return resizeData;
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy