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

com.alee.extended.lazy.LazyContent 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.lazy;

import com.alee.api.annotations.NotNull;
import com.alee.api.annotations.Nullable;
import com.alee.extended.behavior.VisibilityBehavior;
import com.alee.extended.syntax.WebSyntaxArea;
import com.alee.extended.syntax.WebSyntaxScrollPane;
import com.alee.extended.window.PopOverAlignment;
import com.alee.extended.window.PopOverDirection;
import com.alee.extended.window.WebPopOver;
import com.alee.laf.WebLookAndFeel;
import com.alee.laf.label.WebLabel;
import com.alee.managers.style.StyleId;
import com.alee.managers.task.TaskGroup;
import com.alee.managers.task.TaskManager;
import com.alee.utils.CoreSwingUtils;
import com.alee.utils.ExceptionUtils;
import com.alee.utils.SwingUtils;
import com.alee.utils.TextUtils;
import com.alee.utils.swing.MouseButton;
import com.alee.utils.swing.extensions.MouseEventRunnable;

import javax.swing.*;
import javax.swing.event.EventListenerList;
import java.awt.event.MouseEvent;

import static com.alee.extended.syntax.SyntaxPreset.*;

/**
 * Custom utility allowing convenient UI initialization involving heavy data loading.
 * It can also be used to hide UI parts behind lazy load until they become necessary.
 *
 * @param  data type
 * @param  {@link JComponent} type
 * @author Mikle Garin
 */
public abstract class LazyContent
{
    /**
     * {@link JComponent} container to add resulting {@link JComponent} into.
     */
    @NotNull
    protected final JComponent container;

    /**
     * Constraints to add resulting {@link JComponent} under.
     */
    @Nullable
    protected final Object constraints;

    /**
     * Data loading trigger.
     */
    @NotNull
    protected final LazyLoadTrigger dataTrigger;

    /**
     * {@link JComponent} loading trigger.
     * Note that {@link JComponent} cannot be loaded before data loading is completed.
     * That means that even if this trigger can happen earlier it will have no effect until data is fully loaded.
     */
    @NotNull
    protected final LazyLoadTrigger uiTrigger;

    /**
     * {@link EventListenerList} for various {@link LazyContent} listeners.
     */
    @NotNull
    protected final EventListenerList listeners;

    /**
     * {@link VisibilityBehavior} for queuing load for {@link LazyLoadTrigger#onDisplay} setting.
     */
    @Nullable
    protected VisibilityBehavior onDisplayBehavior;

    /**
     * Current {@link LazyState} for data.
     */
    @NotNull
    protected LazyState dataState;

    /**
     * Current {@link LazyState} for UI elements.
     */
    @NotNull
    protected LazyState uiState;

    /**
     * Loaded data.
     */
    @Nullable
    protected D data;

    /**
     * Data load failure cause.
     */
    @Nullable
    protected Throwable dataCause;

    /**
     * {@link JComponent} currently placed as content.
     * This can be actual content {@link JComponent}, progress or exception component.
     */
    @Nullable
    protected JComponent content;

    /**
     * Z-index of {@link JComponent} currently placed as content.
     * Used to correctly restore Z-index to avoid issues with layouts relying on Z-index instead of constraints.
     */
    protected int contentIndex;

    /**
     * Content {@link JComponent} load failure cause.
     */
    @Nullable
    protected Throwable contentCause;

    /**
     * Constructs new {@link LazyContent}.
     *
     * @param container   {@link JComponent} container to add resulting {@link JComponent} into
     * @param constraints constraints to add resulting {@link JComponent} under
     * @param trigger     {@link LazyLoadTrigger} for lazy data and UI elements
     */
    public LazyContent ( @NotNull final JComponent container, @Nullable final Object constraints, @NotNull final LazyLoadTrigger trigger )
    {
        this ( container, constraints, trigger, trigger );
    }

    /**
     * Constructs new {@link LazyContent}.
     *
     * @param container   {@link JComponent} container to add resulting {@link JComponent} into
     * @param constraints constraints to add resulting {@link JComponent} under
     * @param dataTrigger {@link LazyLoadTrigger} for lazy data
     * @param uiTrigger   {@link LazyLoadTrigger} for UI elements
     */
    public LazyContent ( @NotNull final JComponent container, @Nullable final Object constraints,
                         @NotNull final LazyLoadTrigger dataTrigger, @NotNull final LazyLoadTrigger uiTrigger )
    {
        // Event Dispatch Thread check
        WebLookAndFeel.checkEventDispatchThread ();

        // Immutable fields
        this.container = container;
        this.constraints = constraints;
        this.dataTrigger = dataTrigger;
        this.uiTrigger = uiTrigger;
        this.listeners = new EventListenerList ();

        // Initial load states
        this.dataState = LazyState.awaiting;
        this.uiState = LazyState.awaiting;

        // Initial component
        final JComponent initialComponent = createInitialComponent ();
        if ( initialComponent != null )
        {
            this.content = initialComponent;
            this.container.add ( initialComponent, constraints );
            this.contentIndex = this.container.getComponentZOrder ( initialComponent );
        }
        else
        {
            this.content = null;
            this.contentIndex = -1;
        }

        // Queuing data loading
        if ( this.dataTrigger == LazyLoadTrigger.onInit )
        {
            // Loading asap
            queueDataLoading ( false );
        }
        else if ( this.dataTrigger == LazyLoadTrigger.onDisplay )
        {
            // Loading data upon display
            onDisplayBehavior = new VisibilityBehavior ( this.container, true )
            {
                @Override
                protected void displayed ( @NotNull final JComponent component )
                {
                    queueDataLoading ( false );
                }
            };
            onDisplayBehavior.install ();
        }
    }

    /**
     * Returns {@link LazyState} for data.
     *
     * @return {@link LazyState} for data
     */
    @NotNull
    public LazyState getDataState ()
    {
        return dataState;
    }

    /**
     * Updates {@link LazyState} for data.
     * Each state change is performed on a separate invoke to EDT for better UI responsiveness.
     *
     * @param state new {@link LazyState} for data
     */
    protected void setDataState ( @NotNull final LazyState state )
    {
        WebLookAndFeel.checkEventDispatchThread ();
        final LazyState old = this.dataState;
        dataState = state;
        fireDataStateChanged ( old, state );
    }

    /**
     * Returns {@link LazyState} for UI elements.
     *
     * @return {@link LazyState} for UI elements
     */
    @NotNull
    public LazyState getUIState ()
    {
        return uiState;
    }

    /**
     * Updates {@link LazyState} for UI elements.
     * Each state change is performed on a separate invoke to EDT for better UI responsiveness.
     *
     * @param state new {@link LazyState} for UI elements
     */
    protected void setUIState ( @NotNull final LazyState state )
    {
        WebLookAndFeel.checkEventDispatchThread ();
        final LazyState old = this.uiState;
        uiState = state;
        fireUIStateChanged ( old, state );
    }

    /**
     * Manually queues data or UI loading, whichever is necessary.
     * If data and UI are already loaded they will be fully reloaded.
     * Also ensures that actual operation is performed on Event Dispatch Thread if it isn't called from it.
     */
    public void reload ()
    {
        CoreSwingUtils.invokeOnEventDispatchThread ( new Runnable ()
        {
            @Override
            public void run ()
            {
                if ( dataState != LazyState.queued && dataState != LazyState.loading &&
                        uiState != LazyState.queued && uiState != LazyState.loading )
                {
                    if ( dataState == LazyState.awaiting || dataState == LazyState.failed ||
                            dataState == LazyState.loaded && uiState == LazyState.loaded )
                    {
                        queueDataLoading ( true );
                    }
                    else if ( dataState == LazyState.loaded && ( uiState == LazyState.awaiting || uiState == LazyState.failed ) )
                    {
                        queueUILoading ( true );
                    }
                }
            }
        } );
    }

    /**
     * Queues data loading.
     * This method is always executed on EDT.
     * This method can be called again to reload data if previous loading completed or failed.
     *
     * @param forced whether or not data loading is forced
     */
    protected void queueDataLoading ( final boolean forced )
    {
        WebLookAndFeel.checkEventDispatchThread ();

        // Ignore the request if data is not in appropriate state
        final LazyState dataState = getDataState ();
        if ( dataState == LazyState.awaiting || forced && ( dataState == LazyState.loaded || dataState == LazyState.failed ) )
        {
            // Destroy on-display behavior as it isn't necessary anymore
            if ( onDisplayBehavior != null )
            {
                onDisplayBehavior.uninstall ();
                onDisplayBehavior = null;
            }

            // Updating states
            setDataState ( LazyState.queued );
            setUIState ( forced || uiTrigger == LazyLoadTrigger.onInit || uiTrigger == LazyLoadTrigger.onDisplay && container.isShowing ()
                    ? LazyState.queued : LazyState.awaiting );

            // Adding visual progress
            final LazyDataLoadProgress progress = createDataLoadProgress ();
            setCurrentContent ( createProgressComponent ( progress ) );

            // Queuing data loading
            TaskManager.execute ( getTaskGroupId (), new Runnable ()
            {
                @Override
                public void run ()
                {
                    try
                    {
                        // Marking state as loading
                        CoreSwingUtils.invokeLater ( new Runnable ()
                        {
                            @Override
                            public void run ()
                            {
                                // Marking state as loading
                                setDataState ( LazyState.loading );

                                // Informing progress
                                progress.fireLoadingStarted ();
                            }
                        } );

                        // Loading data
                        final D data = loadData ( progress );

                        // Saving data
                        LazyContent.this.data = data;

                        // Performing UI updates
                        CoreSwingUtils.invokeLater ( new Runnable ()
                        {
                            @Override
                            public void run ()
                            {
                                // Marking state as loaded
                                setDataState ( LazyState.loaded );

                                // Informing progress
                                progress.fireLoaded ( data );

                                // Progress component is kept until UI is loaded
                                // removeCurrentContent ();

                                // Firing event
                                fireDataLoaded ( data );

                                // Proceeding to loading UI if necessary
                                if ( forced || uiState == LazyState.queued )
                                {
                                    // Loading component right away
                                    queueUILoading ( forced );
                                }
                                else if ( uiTrigger == LazyLoadTrigger.onDisplay )
                                {
                                    // Loading UI upon display
                                    onDisplayBehavior = new VisibilityBehavior ( container, true )
                                    {
                                        @Override
                                        protected void displayed ( @NotNull final JComponent component )
                                        {
                                            queueUILoading ( false );
                                        }
                                    };
                                    onDisplayBehavior.install ();
                                }
                            }
                        } );
                    }
                    catch ( final Throwable cause )
                    {
                        // Saving data
                        dataCause = cause;

                        // Performing UI updates
                        CoreSwingUtils.invokeLater ( new Runnable ()
                        {
                            @Override
                            public void run ()
                            {
                                // Marking state as failed
                                setDataState ( LazyState.failed );

                                // Returning UI state to awaiting in any case
                                setUIState ( LazyState.awaiting );

                                // Notifying progress
                                progress.fireFailed ( cause );

                                // Adding exception component
                                setCurrentContent ( createExceptionComponent ( dataCause ) );

                                // Firing event
                                fireDataFailed ( dataCause );
                            }
                        } );
                    }
                }
            } );
        }
    }

    /**
     * Queues UI elements loading and ensures it is performed on EDT.
     *
     * @param forced whether or not UI elements loading is forced
     */
    protected void queueUILoading ( final boolean forced )
    {
        WebLookAndFeel.checkEventDispatchThread ();

        // We need to make sure that data is available first
        final LazyState dataState = getDataState ();
        if ( dataState == LazyState.loaded )
        {
            // Destroy on-display behavior as it isn't necessary anymore
            if ( onDisplayBehavior != null )
            {
                onDisplayBehavior.uninstall ();
                onDisplayBehavior = null;
            }

            // Loading component if possible
            final LazyState uiState = getUIState ();
            if ( uiState == LazyState.awaiting || uiState == LazyState.queued ||
                    forced && ( uiState == LazyState.loaded || uiState == LazyState.failed ) )
            {
                // Marking state as queued
                setUIState ( LazyState.loading );

                // Separating UI loading to allow all previous updates to be finished
                CoreSwingUtils.invokeLater ( new Runnable ()
                {
                    @Override
                    public void run ()
                    {
                        try
                        {
                            // Loading component
                            final C content = loadUI ( data );
                            setCurrentContent ( content );

                            // Marking state as loaded
                            setUIState ( LazyState.loaded );

                            // Firing event
                            fireUILoaded ( data, content );
                        }
                        catch ( final Throwable cause )
                        {
                            // Must synchronize data and state changes
                            // Saving data
                            contentCause = cause;

                            // Marking state as loaded
                            setUIState ( LazyState.failed );

                            // Adding exception component
                            setCurrentContent ( createExceptionComponent ( contentCause ) );

                            // Firing event
                            fireUIFailed ( data, contentCause );
                        }
                        finally
                        {
                            // Clearing up resources
                            data = null;
                        }
                    }
                } );
            }
        }
        else
        {
            // Queueing data loading first
            queueDataLoading ( forced );
        }
    }

    /**
     * Sets new content {@link JComponent}.
     *
     * @param content content {@link JComponent}
     */
    protected void setCurrentContent ( @NotNull final JComponent content )
    {
        // Ensure old content is removed
        removeCurrentContent ();

        // Add new content
        this.content = content;
        container.add ( content, constraints, contentIndex );
        SwingUtils.update ( container );
    }

    /**
     * Removes currently added content {@link JComponent}.
     */
    protected void removeCurrentContent ()
    {
        // Event Dispatch Thread check
        WebLookAndFeel.checkEventDispatchThread ();

        // Ensure old content exists
        if ( content != null && content.getParent () == container )
        {
            contentIndex = container.getComponentZOrder ( content );
            container.remove ( content );
        }
    }

    /**
     * Returns identifier of the {@link TaskGroup} to execute data loading on.
     *
     * @return identifier of the {@link TaskGroup} to execute data loading on
     */
    @NotNull
    protected abstract String getTaskGroupId ();

    /**
     * Creates and returns {@link JComponent} to be displayed initially.
     * {@code null} can be returned for no initial {@link JComponent}.
     *
     * @return {@link JComponent} to be displayed initially
     */
    @Nullable
    protected JComponent createInitialComponent ()
    {
        return null;
    }

    /**
     * Creates and returns {@link LazyDataLoadProgress} provided to displayed custom progress {@link JComponent}.
     *
     * @return {@link LazyDataLoadProgress} provided to displayed custom progress {@link JComponent}
     */
    @NotNull
    protected LazyDataLoadProgress createDataLoadProgress ()
    {
        return new LazyDataLoadProgress ();
    }

    /**
     * Creates and returns custom progress {@link JComponent}.
     * It is displayed in {@link #container} upon data loading start and removed upon UI load completion.
     *
     * @param progress {@link DataLoadProgress}
     * @return custom progress {@link JComponent}
     */
    @NotNull
    protected JComponent createProgressComponent ( @NotNull final DataLoadProgress progress )
    {
        return new LazyProgressOverlay ( progress );
    }

    /**
     * Creates and returns custom {@link JComponent} for the specified {@link Throwable} cause.
     * It is displayed in {@link #container} in case data loading has failed.
     *
     * @param cause {@link Throwable} cause
     * @return custom {@link JComponent} for the specified {@link Throwable} cause
     */
    @NotNull
    protected JComponent createExceptionComponent ( @NotNull final Throwable cause )
    {
        final String message = TextUtils.shortenText ( cause.getMessage (), 100, true );
        final WebLabel component = new WebLabel ( message, WebLabel.CENTER );
        component.onMousePress ( MouseButton.left, new MouseEventRunnable ()
        {
            @Override
            public void run ( @NotNull final MouseEvent event )
            {
                final WebPopOver info = new WebPopOver ( component );
                info.setCloseOnFocusLoss ( true );
                final WebSyntaxArea area = new WebSyntaxArea ( ExceptionUtils.getStackTrace ( cause ), 12, 60 );
                area.applyPresets ( base, viewable, hideMenu, ideaTheme, nonOpaque );
                info.add ( new WebSyntaxScrollPane ( StyleId.syntaxareaScrollUndecorated, area, false ) );
                info.show ( component, PopOverDirection.down, PopOverAlignment.centered );
            }
        } );
        return component;
    }

    /**
     * Loads and returns data for {@link JComponent} to display.
     *
     * @param callback {@link ProgressCallback}
     * @return loaded data for {@link JComponent} to display
     */
    @Nullable
    protected abstract D loadData ( @NotNull ProgressCallback callback );

    /**
     * Creates and returns {@link JComponent} to be displayed for the loaded data.
     *
     * @param data loaded data
     * @return {@link JComponent} to be displayed for the loaded data
     */
    @NotNull
    protected abstract C loadUI ( @Nullable D data );

    /**
     * Adds {@link LazyStateListener} into this {@link LazyContent}.
     *
     * @param listener {@link LazyStateListener} to add
     */
    public void addLazyContentStateListener ( @NotNull final LazyStateListener listener )
    {
        listeners.add ( LazyStateListener.class, listener );
    }

    /**
     * Removes {@link LazyStateListener} from this {@link LazyContent}.
     *
     * @param listener {@link LazyStateListener} to remove
     */
    public void removeLazyContentStateListener ( @NotNull final LazyStateListener listener )
    {
        listeners.remove ( LazyStateListener.class, listener );
    }

    /**
     * Fires {@link LazyContent} data {@link LazyState} change.
     *
     * @param oldState new {@link LazyState} for data
     * @param newState new {@link LazyState} for data
     */
    public void fireDataStateChanged ( @NotNull final LazyState oldState, @NotNull final LazyState newState )
    {
        for ( final LazyStateListener listener : listeners.getListeners ( LazyStateListener.class ) )
        {
            listener.dataStateChanged ( oldState, newState );
        }
    }

    /**
     * Fires {@link LazyContent} UI elements {@link LazyState} change.
     *
     * @param oldState new {@link LazyState} for UI elements
     * @param newState new {@link LazyState} for UI elements
     */
    public void fireUIStateChanged ( @NotNull final LazyState oldState, @NotNull final LazyState newState )
    {
        for ( final LazyStateListener listener : listeners.getListeners ( LazyStateListener.class ) )
        {
            listener.uiStateChanged ( oldState, newState );
        }
    }

    /**
     * Adds {@link LazyContentListener} into this {@link LazyContent}.
     *
     * @param listener {@link LazyContentListener} to add
     */
    public void addLazyContentListener ( @NotNull final LazyContentListener listener )
    {
        listeners.add ( LazyContentListener.class, listener );
    }

    /**
     * Removes {@link LazyContentListener} from this {@link LazyContent}.
     *
     * @param listener {@link LazyContentListener} to remove
     */
    public void removeLazyContentListener ( @NotNull final LazyContentListener listener )
    {
        listeners.remove ( LazyContentListener.class, listener );
    }

    /**
     * Fires {@link LazyContent} data load completion.
     *
     * @param data loaded data
     */
    public void fireDataLoaded ( @Nullable final D data )
    {
        for ( final LazyContentListener listener : listeners.getListeners ( LazyContentListener.class ) )
        {
            listener.dataLoaded ( data );
        }
    }

    /**
     * Fires {@link LazyContent} data load failure.
     *
     * @param cause {@link Throwable}
     */
    public void fireDataFailed ( @NotNull final Throwable cause )
    {
        for ( final LazyContentListener listener : listeners.getListeners ( LazyContentListener.class ) )
        {
            listener.dataFailed ( cause );
        }
    }

    /**
     * Fires {@link LazyContent} UI elements load completion.
     *
     * @param data      loaded data
     * @param component loaded {@link JComponent}
     */
    public void fireUILoaded ( @Nullable final D data, @NotNull final C component )
    {
        for ( final LazyContentListener listener : listeners.getListeners ( LazyContentListener.class ) )
        {
            listener.uiLoaded ( data, component );
        }
    }

    /**
     * Fires {@link LazyContent} UI elements load failure.
     *
     * @param data  loaded data
     * @param cause {@link Throwable}
     */
    public void fireUIFailed ( @Nullable final D data, @NotNull final Throwable cause )
    {
        for ( final LazyContentListener listener : listeners.getListeners ( LazyContentListener.class ) )
        {
            listener.uiFailed ( data, cause );
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy