com.alee.extended.tab.WebDocumentPane Maven / Gradle / Ivy
/*
* 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.tab;
import com.alee.api.jdk.Function;
import com.alee.extended.behavior.VisibilityBehavior;
import com.alee.laf.panel.WebPanel;
import com.alee.laf.splitpane.WebSplitPane;
import com.alee.laf.tabbedpane.WebTabbedPane;
import com.alee.managers.drag.DragManager;
import com.alee.managers.settings.Configuration;
import com.alee.managers.settings.SettingsMethods;
import com.alee.managers.settings.SettingsProcessor;
import com.alee.managers.settings.UISettingsManager;
import com.alee.managers.style.StyleId;
import com.alee.utils.CollectionUtils;
import com.alee.utils.TextUtils;
import com.alee.utils.general.Pair;
import com.alee.utils.swing.Customizer;
import javax.swing.*;
import java.awt.*;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
/**
* {@link WebDocumentPane} is a special container for multiple documents described by {@link DocumentData} class.
* This component uses either single or multiple {@link JTabbedPane}s and allow tabs reorder, drag, split and closeability.
* It also allows usage of custom {@link DocumentData} implementations to adjust it's functionality or add custom data into it.
*
* @param document type
* @author Mikle Garin
* @see How to use WebDocumentPane
* @see PaneData
* @see SplitData
* @see DocumentData
*/
public class WebDocumentPane extends WebPanel implements DocumentPaneEventMethods, SettingsMethods
{
/**
* todo 1. Separate UI and make use of styling system
* todo 2. Possibility to edit tab title
*/
/**
* Constant key used to put pane element data into the UI component.
*/
protected static final String DATA_KEY = "document.pane.data";
/**
* Unique document pane ID.
* Used to allow or disallow documents drag between different document panes.
*/
protected final String id;
/**
* Root structure element.
* Might either be PaneData or SplitData.
*/
protected StructureData root;
/**
* Last active pane.
*/
protected PaneData activePane;
/**
* Tabbed panes customizer.
*/
protected Customizer tabbedPaneCustomizer;
/**
* Document tab title view customizer.
*/
protected TabTitleComponentProvider tabTitleComponentProvider;
/**
* Document customizer.
*/
protected Customizer splitPaneCustomizer;
/**
* Whether documents can be closed or not.
*/
protected boolean closable = true;
/**
* Whether documents drag enabled or not.
*/
protected boolean dragEnabled = true;
/**
* Whether documents drag between tabbed panes is enabled or not.
*/
protected boolean dragBetweenPanesEnabled = false;
/**
* Whether split creation is enabled or not.
*/
protected boolean splitEnabled = true;
/**
* Whether tab menu is enabled or not.
*/
protected boolean tabMenuEnabled = true;
/**
* Previously selected document.
*/
protected WeakReference previouslySelected = new WeakReference ( null );
/**
* {@link Function} that resolves {@link DocumentData} for provided identifier.
* It is used in to open documents by their identifiers instead of document references.
* This {@link Function} is essential for proper state restoration functioning.
*/
protected Function documentsProvider = null;
/**
* Document drag view handler.
*/
protected final DocumentDragViewHandler dragViewHandler;
/**
* Constructs new document pane.
*/
public WebDocumentPane ()
{
this ( StyleId.documentpane, null, null, null );
}
/**
* Constructs new document pane.
*
* @param tabbedPaneCustomizer tabbed pane customizer
*/
public WebDocumentPane ( final Customizer tabbedPaneCustomizer )
{
this ( StyleId.documentpane, null, tabbedPaneCustomizer, null );
}
/**
* Constructs new document pane.
*
* @param tabTitleComponentProvider tab title component customizer
*/
public WebDocumentPane ( final TabTitleComponentProvider tabTitleComponentProvider )
{
this ( StyleId.documentpane, null, null, tabTitleComponentProvider );
}
/**
* Constructs new document pane.
*
* @param tabbedPaneCustomizer tabbed pane customizer
* @param tabTitleComponentProvider tab title component customizer
*/
public WebDocumentPane ( final Customizer tabbedPaneCustomizer,
final TabTitleComponentProvider tabTitleComponentProvider )
{
this ( StyleId.documentpane, null, tabbedPaneCustomizer, tabTitleComponentProvider );
}
/**
* Constructs new document pane.
*
* @param splitPaneCustomizer split pane customizer
* @param tabbedPaneCustomizer tabbed pane customizer
*/
public WebDocumentPane ( final Customizer splitPaneCustomizer, final Customizer tabbedPaneCustomizer )
{
this ( StyleId.documentpane, splitPaneCustomizer, tabbedPaneCustomizer, null );
}
/**
* Constructs new document pane.
*
* @param splitPaneCustomizer split pane customizer
* @param tabbedPaneCustomizer tabbed pane customizer
* @param tabTitleComponentProvider tab title component customizer
*/
public WebDocumentPane ( final Customizer splitPaneCustomizer, final Customizer tabbedPaneCustomizer,
final TabTitleComponentProvider tabTitleComponentProvider )
{
this ( StyleId.documentpane, splitPaneCustomizer, tabbedPaneCustomizer, tabTitleComponentProvider );
}
/**
* Constructs new document pane.
*
* @param id style ID
*/
public WebDocumentPane ( final StyleId id )
{
this ( id, null, null, null );
}
/**
* Constructs new document pane.
*
* @param id style ID
* @param tabbedPaneCustomizer tabbed pane customizer
*/
public WebDocumentPane ( final StyleId id, final Customizer tabbedPaneCustomizer )
{
this ( id, null, tabbedPaneCustomizer, null );
}
/**
* Constructs new document pane.
*
* @param id style ID
* @param tabTitleComponentProvider tab title component customizer
*/
public WebDocumentPane ( final StyleId id, final TabTitleComponentProvider tabTitleComponentProvider )
{
this ( id, null, null, tabTitleComponentProvider );
}
/**
* Constructs new document pane.
*
* @param id style ID
* @param tabbedPaneCustomizer tabbed pane customizer
* @param tabTitleComponentProvider tab title component customizer
*/
public WebDocumentPane ( final StyleId id, final Customizer tabbedPaneCustomizer,
final TabTitleComponentProvider tabTitleComponentProvider )
{
this ( id, null, tabbedPaneCustomizer, tabTitleComponentProvider );
}
/**
* Constructs new document pane.
*
* @param id style ID
* @param splitPaneCustomizer split pane customizer
* @param tabbedPaneCustomizer tabbed pane customizer
*/
public WebDocumentPane ( final StyleId id, final Customizer splitPaneCustomizer,
final Customizer tabbedPaneCustomizer )
{
this ( id, splitPaneCustomizer, tabbedPaneCustomizer, null );
}
/**
* Constructs new document pane.
*
* @param id style ID
* @param splitPaneCustomizer split pane customizer
* @param tabbedPaneCustomizer tabbed pane customizer
* @param tabTitleComponentProvider tab title component customizer
*/
public WebDocumentPane ( final StyleId id, final Customizer splitPaneCustomizer,
final Customizer tabbedPaneCustomizer,
final TabTitleComponentProvider tabTitleComponentProvider )
{
super ( id );
// Customizers
this.tabbedPaneCustomizer = tabbedPaneCustomizer;
this.splitPaneCustomizer = splitPaneCustomizer;
// Tab title component provider
this.tabTitleComponentProvider =
tabTitleComponentProvider != null ? tabTitleComponentProvider : createDefaultTabTitleComponentProvider ();
// Generating unique document pane ID
this.id = TextUtils.generateId ( "WDP" );
// Registering drag view handler
dragViewHandler = new DocumentDragViewHandler ( this );
// Special behavior to keep view handler only while document pane is visible
new VisibilityBehavior ( this )
{
@Override
public void displayed ()
{
DragManager.registerViewHandler ( dragViewHandler );
}
@Override
public void hidden ()
{
DragManager.unregisterViewHandler ( dragViewHandler );
}
}.install ();
// Adding initial pane
initialize ();
}
/**
* Returns default tab title component provider.
*
* @return default tab title component provider
*/
protected TabTitleComponentProvider createDefaultTabTitleComponentProvider ()
{
return new DefaultTabTitleComponentProvider ();
}
/**
* Returns unique document pane ID.
* Might be used within D&D functionality to determine whether drag source is the same as destination.
*
* @return unique document pane ID
*/
public String getId ()
{
return id;
}
/**
* Returns tabbed pane customizer.
* It is null by default.
*
* @return tabbed pane customizer
*/
public Customizer getTabbedPaneCustomizer ()
{
return tabbedPaneCustomizer;
}
/**
* Sets tabbed pane customizer and applies it to existing panes.
* Note that changes made by previously set customizers are not reverted even if you set this to null.
*
* @param customizer new tabbed pane customizer
*/
public void setTabbedPaneCustomizer ( final Customizer customizer )
{
this.tabbedPaneCustomizer = customizer;
for ( final PaneData paneData : getAllPanes () )
{
paneData.updateTabbedPaneCustomizer ( this );
}
}
/**
* Returns document tab title view customizer.
*
* @return document tab title view customizer
*/
public TabTitleComponentProvider getTabTitleComponentProvider ()
{
return tabTitleComponentProvider;
}
/**
* Sets document tab title component provider.
*
* @param provider new document tab title component provider
*/
public void setTabTitleComponentProvider ( final TabTitleComponentProvider provider )
{
this.tabTitleComponentProvider = provider != null ? provider : createDefaultTabTitleComponentProvider ();
for ( final PaneData paneData : getAllPanes () )
{
paneData.updateTabTitleComponents ();
}
}
/**
* Returns split pane customizer.
* It is null by default.
*
* @return split pane customizer
*/
public Customizer getSplitPaneCustomizer ()
{
return splitPaneCustomizer;
}
/**
* Sets split pane customizer and applies it to existing panes.
* Note that changes made by previously set customizers are not reverted even if you set this to null.
*
* @param customizer new split pane customizer
*/
public void setSplitPaneCustomizer ( final Customizer customizer )
{
this.splitPaneCustomizer = customizer;
for ( final SplitData paneData : getAllSplitPanes () )
{
paneData.updateSplitPaneCustomizer ( this );
}
}
/**
* Returns whether tabs in this document pane are globally closable or not.
*
* @return true if tabs in this document pane are globally closable, false otherwise
*/
public boolean isClosable ()
{
return closable;
}
/**
* Sets whether tabs in this document pane should be globally closable or not.
*
* @param closable whether tabs in this document pane should be globally closable or not
*/
public void setClosable ( final boolean closable )
{
this.closable = closable;
}
/**
* Returns whether tabs drag is enabled or not.
*
* @return true if tabs drag is enabled, false otherwise
*/
public boolean isDragEnabled ()
{
return dragEnabled;
}
/**
* Sets whether tabs drag is enabled or not.
*
* @param dragEnabled whether tabs drag is enabled or not
*/
public void setDragEnabled ( final boolean dragEnabled )
{
this.dragEnabled = dragEnabled;
}
/**
* Returns whether tabs drag between different tabbed panes is enabled or not.
*
* @return true if tabs drag between different tabbed panes is enabled, false otherwise
*/
public boolean isDragBetweenPanesEnabled ()
{
return dragBetweenPanesEnabled;
}
/**
* Sets whether tabs drag between different tabbed panes is enabled or not.
*
* @param dragBetweenPanesEnabled whether tabs drag between different tabbed panes is enabled or not
*/
public void setDragBetweenPanesEnabled ( final boolean dragBetweenPanesEnabled )
{
this.dragBetweenPanesEnabled = dragBetweenPanesEnabled;
}
/**
* Returns whether split creation is enabled or not.
*
* @return true if split creation is enabled, false otherwise
*/
public boolean isSplitEnabled ()
{
return splitEnabled;
}
/**
* Sets whether split creation is enabled or not.
*
* @param splitEnabled true if split creation is enabled, false otherwise
*/
public void setSplitEnabled ( final boolean splitEnabled )
{
this.splitEnabled = splitEnabled;
}
/**
* Returns whether tab menu is enabled or not.
*
* @return true if tab menu is enabled, false otherwise
*/
public boolean isTabMenuEnabled ()
{
return tabMenuEnabled;
}
/**
* Sets whether tab menu is enabled or not.
*
* @param tabMenuEnabled whether tab menu is enabled or not
*/
public void setTabMenuEnabled ( final boolean tabMenuEnabled )
{
this.tabMenuEnabled = tabMenuEnabled;
}
/**
* Returns current root element data.
* This is either SplitData or PaneData object.
*
* @return current root element data
*/
public StructureData getStructureRoot ()
{
return root;
}
/**
* Sets new root element data.
* This call replaces all data stored in this document pane with new one.
*
* @param root new root element data
*/
public void setStructureRoot ( final StructureData root )
{
// Clearing root component
if ( this.root != null )
{
remove ( this.root.getComponent () );
}
// Initializing new root
if ( root != null )
{
// Adding root component
add ( root.getComponent (), BorderLayout.CENTER );
// Changing root
this.root = root;
this.activePane = root.findClosestPane ();
// Updating document pane view
revalidate ();
repaint ();
}
else
{
// Add initial pane
initialize ();
}
}
/**
* Initializes root and active pane.
*/
protected void initialize ()
{
// Creating data for root pane
final PaneData rootPane = new PaneData ( this );
// Adding root pane
add ( rootPane.getTabbedPane (), BorderLayout.CENTER );
// Applying initial values
root = rootPane;
activePane = rootPane;
}
/**
* Splits document's pane into two panes using the specified direction to decide split settings.
*
* @param movedDocument document that should be moved to new pane
* @param direction split direction
*/
public void split ( final T movedDocument, final int direction )
{
final PaneData pane = getPane ( movedDocument );
if ( pane != null )
{
split ( pane, movedDocument, direction );
}
}
/**
* Splits specified pane into two panes using the specified direction to decide split settings.
*
* @param splittedPane pane that will be splitted
* @param movedDocument document that should be moved from splitted pane to new one
* @param direction split direction
* @return second pane created in the split process
*/
protected PaneData split ( final PaneData splittedPane, final T movedDocument, final int direction )
{
final PaneData otherPane;
if ( splittedPane != null )
{
// Choosing course of action depending on splitted pane parent
final boolean ltr = direction == SwingConstants.RIGHT || direction == SwingConstants.BOTTOM;
final int orientation = direction == SwingConstants.LEFT || direction == SwingConstants.RIGHT ?
SwingConstants.VERTICAL : SwingConstants.HORIZONTAL;
final SplitData splitData;
if ( splittedPane.getTabbedPane ().getParent () == WebDocumentPane.this )
{
// Creating data for new pane
otherPane = new PaneData ( this );
// Saving sizes to restore split locations
final Dimension size = splittedPane.getTabbedPane ().getSize ();
// Adding root split
final PaneData first = ltr ? splittedPane : otherPane;
final PaneData last = ltr ? otherPane : splittedPane;
splitData = new SplitData ( WebDocumentPane.this, orientation, first, last );
remove ( splittedPane.getTabbedPane () );
add ( splitData.getSplitPane (), BorderLayout.CENTER );
// Restoring split locations
splitData.getSplitPane ().setDividerLocation ( orientation == SwingConstants.VERTICAL ? size.width / 2 : size.height / 2 );
// Changing root
root = splitData;
}
else
{
// Determining parent split
final WebSplitPane parentSplit = ( WebSplitPane ) splittedPane.getTabbedPane ().getParent ();
final SplitData parentSplitData = getData ( parentSplit );
if ( parentSplitData.getOrientation () == orientation && ltr && parentSplitData.getFirst () == splittedPane &&
parentSplitData.getLast () instanceof PaneData )
{
// Using existing split and pane
splitData = parentSplitData;
otherPane = ( PaneData ) parentSplitData.getLast ();
}
else if ( parentSplitData.getOrientation () == orientation && !ltr && parentSplitData.getLast () == splittedPane &&
parentSplitData.getFirst () instanceof PaneData )
{
// Using existing split and pane
splitData = parentSplitData;
otherPane = ( PaneData ) parentSplitData.getFirst ();
}
else
{
// Creating data for new pane
otherPane = new PaneData ( this );
// Saving sizes to restore split locations
final int parentSplitLocation = parentSplitData.getSplitPane ().getDividerLocation ();
final Dimension size = splittedPane.getTabbedPane ().getSize ();
// Adding inner split
final PaneData first = ltr ? splittedPane : otherPane;
final PaneData last = ltr ? otherPane : splittedPane;
splitData = new SplitData ( WebDocumentPane.this, orientation, first, last );
parentSplitData.replace ( splittedPane, splitData );
// Restoring split locations
final int location = orientation == SwingConstants.VERTICAL ? size.width / 2 : size.height / 2;
splitData.getSplitPane ().setDividerLocation ( location );
parentSplitData.getSplitPane ().setDividerLocation ( parentSplitLocation );
}
}
// Moving document to new pane if it is specified
if ( movedDocument != null )
{
splittedPane.remove ( movedDocument );
otherPane.add ( movedDocument );
}
// Updating document pane view
revalidate ();
repaint ();
// Activating other split
otherPane.getTabbedPane ().requestFocusInWindow ();
otherPane.activate ();
// Firing splitted event
fireSplitted ( splittedPane, splitData );
}
else
{
// Its not possible to split unspecified pane
otherPane = null;
}
return otherPane;
}
/**
* Merges specified structure element and its sub-elements if it is possible.
* If PaneData provided its parent split will be merged.
* If SplitData provided it will be merged.
*
* @param toMerge structure element to merge
*/
public void merge ( final StructureData toMerge )
{
// Retrieving split data that should be merged
if ( toMerge instanceof PaneData )
{
// When pane is forced to merge with opposite
final PaneData mergedPane = ( PaneData ) toMerge;
final Container parent = mergedPane.getTabbedPane ().getParent ();
// Merge only if actually inside of a split
// Otherwise this is a root pane which can't be merged
if ( parent instanceof WebSplitPane )
{
final WebSplitPane splitPane = ( WebSplitPane ) parent;
mergeImpl ( ( SplitData ) getData ( splitPane ) );
// Updating document pane view
revalidate ();
repaint ();
}
}
else
{
// When split is forced to merge into single pane
mergeImpl ( ( SplitData ) toMerge );
// Updating document pane view
revalidate ();
repaint ();
}
// Checking selection state
checkSelection ();
}
/**
* Merges specified split element and its sub-elements if it is possible.
*
* @param splitData split element to merge
*/
protected void mergeImpl ( final SplitData splitData )
{
StructureData first = splitData.getFirst ();
StructureData last = splitData.getLast ();
// Determining the resulting element
final StructureData result;
if ( isEmptyPane ( first ) || isEmptyPane ( last ) )
{
result = isEmptyPane ( first ) ? last : first;
}
else
{
// Merge inner content first so we have split with tabs only inside
if ( first instanceof SplitData )
{
mergeImpl ( ( SplitData ) first );
first = splitData.getFirst ();
}
if ( last instanceof SplitData )
{
mergeImpl ( ( SplitData ) last );
last = splitData.getLast ();
}
// Moving all documents from second pane to first
final PaneData firstPane = ( PaneData ) first;
final PaneData lastPane = ( PaneData ) last;
final PaneData toPane = firstPane.count () > lastPane.count () ? firstPane : lastPane;
final PaneData fromPane = firstPane.count () > lastPane.count () ? lastPane : firstPane;
for ( final T document : CollectionUtils.copy ( fromPane.getData () ) )
{
fromPane.remove ( document );
toPane.add ( document );
}
result = toPane;
}
// Update active pane
if ( activePane == first || activePane == last )
{
activePane = result.findClosestPane ();
}
// Removing merged split
final WebSplitPane splitPane = splitData.getSplitPane ();
if ( splitPane.getParent () == WebDocumentPane.this )
{
// Removing root split and adding tab pane
remove ( splitPane );
add ( result.getComponent (), BorderLayout.CENTER );
// Changing root
root = result;
}
else
{
// Retrieving parent split
final WebSplitPane parentSplit = ( WebSplitPane ) splitPane.getParent ();
final SplitData parentSplitData = getData ( parentSplit );
final int dividerLocation = parentSplit.getDividerLocation ();
// Changing parent split component
if ( parentSplit.getLeftComponent () == splitPane )
{
parentSplitData.setFirst ( result );
}
else
{
parentSplitData.setLast ( result );
}
// Restoring divider location
parentSplit.setDividerLocation ( dividerLocation );
}
// Firing merged event
fireMerged ( splitData, result );
}
/**
* Returns currently active pane data.
* This is the last pane that had focus within this document pane.
*
* @return currently active pane data
*/
public PaneData getActivePane ()
{
return activePane;
}
/**
* Sets active pane.
*
* @param paneData new active pane
*/
protected void activate ( final PaneData paneData )
{
if ( paneData != null && paneData != activePane )
{
activePane = paneData;
checkSelection ();
}
}
/**
* Sets active pane.
*
* @param document document to activate
*/
protected void activate ( final T document )
{
activate ( getPane ( document ) );
setSelected ( document );
}
/**
* Returns selected document data.
*
* @return selected document data
*/
public T getSelectedDocument ()
{
return activePane != null ? activePane.getSelected () : null;
}
/**
* Returns document at the specified tab index of the active pane.
*
* @param index active pane tab index
* @return document at the specified tab index of the active pane
*/
public T getDocument ( final int index )
{
return activePane != null ? activePane.get ( index ) : null;
}
/**
* Returns document with the specified ID or null if it is not inside this document pane.
*
* @param id document ID
* @return document with the specified ID or null if it is not inside this document pane
*/
public T getDocument ( final String id )
{
for ( final PaneData paneData : getAllPanes () )
{
final T document = paneData.get ( id );
if ( document != null )
{
return document;
}
}
return null;
}
/**
* Returns all documents opened in this document pane.
*
* @return all documents opened in this document pane
*/
public List getDocuments ()
{
final List documents = new ArrayList ();
for ( final PaneData paneData : getAllPanes () )
{
documents.addAll ( paneData.getData () );
}
return documents;
}
/**
* Returns amount of documents opened in this document pane.
*
* @return amount of documents opened in this document pane
*/
public int getDocumentsCount ()
{
int count = 0;
for ( final PaneData paneData : getAllPanes () )
{
count += paneData.count ();
}
return count;
}
/**
* Returns list of all available panes within this document pane.
*
* @return list of all available panes within this document pane
*/
public List> getAllPanes ()
{
final List> panes = new ArrayList> ();
collectPanes ( root, panes );
return panes;
}
/**
* Collects all PaneData available under the specified structure element into list.
*
* @param structureData structure element
* @param panes PaneData list
*/
protected void collectPanes ( final StructureData structureData, final List> panes )
{
if ( structureData instanceof PaneData )
{
panes.add ( ( PaneData ) structureData );
}
else if ( structureData instanceof SplitData )
{
final SplitData splitData = ( SplitData ) structureData;
collectPanes ( splitData.getFirst (), panes );
collectPanes ( splitData.getLast (), panes );
}
else
{
throw new RuntimeException ( "Unknown structure data type: " + structureData.getClass () );
}
}
/**
* Returns list of all available split panes within this document pane.
*
* @return list of all available split panes within this document pane
*/
public List> getAllSplitPanes ()
{
final List> panes = new ArrayList> ();
collectSplitPanes ( root, panes );
return panes;
}
/**
* Collects all SplitData available under the specified structure element into list.
*
* @param structureData structure element
* @param splits SplitData list
*/
protected void collectSplitPanes ( final StructureData structureData, final List> splits )
{
if ( structureData instanceof SplitData )
{
final SplitData splitData = ( SplitData ) structureData;
splits.add ( splitData );
collectSplitPanes ( splitData.getFirst (), splits );
collectSplitPanes ( splitData.getLast (), splits );
}
}
/**
* Returns pane that contains specified document.
*
* @param document document to look for
* @return pane that contains specified document
*/
public PaneData getPane ( final T document )
{
return document != null ? getPane ( document.getId () ) : null;
}
/**
* Returns pane that contains document with the specified ID.
*
* @param documentId ID of the document to look for
* @return pane that contains document with the specified ID
*/
public PaneData getPane ( final String documentId )
{
for ( final PaneData paneData : getAllPanes () )
{
if ( paneData.contains ( documentId ) )
{
return paneData;
}
}
return null;
}
/**
* Sets selected document index inside the active pane.
*
* @param index index of the document to select
*/
public void setSelected ( final int index )
{
if ( activePane != null )
{
activePane.setSelected ( index );
}
}
/**
* Decrements selected document index inside the active pane.
*/
public void selectPrevious ()
{
if ( activePane != null )
{
activePane.selectPrevious ();
}
}
/**
* Increments selected document index inside the active pane.
*/
public void selectNext ()
{
if ( activePane != null )
{
activePane.selectNext ();
}
}
/**
* Sets document selected inside its pane.
*
* @param document document to select
*/
public void setSelected ( final DocumentData document )
{
setSelected ( document.getId () );
}
/**
* Sets document with the specified ID selected inside its pane.
*
* @param id ID of the document to select
*/
public void setSelected ( final String id )
{
for ( final PaneData paneData : getAllPanes () )
{
final T document = paneData.get ( id );
if ( document != null )
{
paneData.setSelected ( document );
paneData.activate ();
}
}
}
/**
* Returns whether specified document is opened inside this document pane or not.
*
* @param document document to look for
* @return true if specified document is opened inside this document pane, false otherwise
*/
public boolean isDocumentOpened ( final T document )
{
return isDocumentOpened ( document.getId () );
}
/**
* Returns whether document with the specified ID is opened inside this document pane or not.
*
* @param documentId ID of the document to look for
* @return true if document with the specified ID is opened inside this document pane, false otherwise
*/
public boolean isDocumentOpened ( final String documentId )
{
for ( final PaneData paneData : getAllPanes () )
{
if ( paneData.contains ( documentId ) )
{
return true;
}
}
return false;
}
/**
* Opens document with the specified ID in this document pane.
* This method won't work in case you didn't set custom DocumentDataProvider.
*
* @param documentId ID of the document to open
*/
public void openDocument ( final String documentId )
{
openDocument ( documentId, true );
}
/**
* Opens document with the specified ID in this document pane.
* This method won't work in case you didn't set custom DocumentDataProvider.
*
* @param documentId ID of the document to open
* @param select whether or not document tab should be selected
*/
public void openDocument ( final String documentId, final boolean select )
{
if ( documentsProvider != null )
{
openDocument ( documentsProvider.apply ( documentId ), select );
}
}
/**
* Opens document in this document pane.
*
* @param document document to open
*/
public void openDocument ( final T document )
{
openDocument ( document, true );
}
/**
* Opens document in this document pane.
*
* @param document document to open
* @param select whether or not document tab should be selected
*/
public void openDocument ( final T document, final boolean select )
{
if ( document != null )
{
if ( isDocumentOpened ( document ) )
{
if ( select )
{
setSelected ( document );
}
}
else if ( activePane != null )
{
activePane.open ( document );
if ( select )
{
activePane.setSelected ( document );
}
}
else
{
throw new NullPointerException ( "Something went wrong, active pane is not available" );
}
}
}
/**
* Closes document at the specified index in the active pane.
*
* @param index index of the document to close
* @return {@code true} if document was successfully closed, {@code false} otherwise
*/
public boolean closeDocument ( final int index )
{
return activePane != null && activePane.close ( index );
}
/**
* Closes document with the specified ID.
*
* @param id ID of the document to close
* @return {@code true} if document was successfully closed, {@code false} otherwise
*/
public boolean closeDocument ( final String id )
{
for ( final PaneData paneData : getAllPanes () )
{
if ( paneData.close ( id ) )
{
return true;
}
}
return false;
}
/**
* Closes the specified document.
*
* @param document document to close
* @return {@code true} if document was successfully closed, {@code false} otherwise
*/
public boolean closeDocument ( final T document )
{
for ( final PaneData paneData : getAllPanes () )
{
if ( paneData.close ( document ) )
{
return true;
}
}
return false;
}
/**
* Closes all documents.
*
* @return {@code true} if all documents were successfully closed, {@code false} otherwise
*/
public boolean closeAll ()
{
boolean success = true;
for ( final PaneData paneData : getAllPanes () )
{
success &= paneData.closeAll ();
}
return success;
}
/**
* Checks selection state and fires selection event if required.
* This method is separated from actual selection due to various difficulties implementing it on-place.
* Selection is checked on pane activation, documents tab changes.
*/
protected void checkSelection ()
{
final T selected = getSelectedDocument ();
if ( previouslySelected.get () != selected )
{
// Updating reference
previouslySelected = new WeakReference ( selected );
// Firing event only when something was actually selected
if ( selected != null )
{
final PaneData pane = getPane ( selected );
fireDocumentSelected ( selected, pane, pane.indexOf ( selected ) );
}
}
}
/**
* Returns {@link Function} that resolves {@link DocumentData} for provided identifier.
* It is used in to open documents by their identifiers instead of document references.
* This {@link Function} is essential for proper state restoration functioning.
*
* @return {@link Function} that resolves {@link DocumentData} for provided identifier
*/
public Function getDocumentsProvider ()
{
return documentsProvider;
}
/**
* Sets {@link Function} that resolves {@link DocumentData} for provided identifier.
* It is used in to open documents by their identifiers instead of document references.
* This {@link Function} is essential for proper state restoration functioning.
*
* @param provider new {@link Function} for resolving {@link DocumentData} by provided identifier
*/
public void setDocumentsProvider ( final Function provider )
{
this.documentsProvider = provider;
}
/**
* Returns current {@link DocumentPaneState} for this {@link WebDocumentPane}.
* This {@link DocumentPaneState} contains opened document IDs references and structure composition.
* It can be used to save and restore {@link WebDocumentPane} structure and opened documents.
*
* @return current {@link DocumentPaneState} for this {@link WebDocumentPane}
* @see DocumentPaneState
* @see #setDocumentPaneState(DocumentPaneState)
*/
public DocumentPaneState getDocumentPaneState ()
{
return root != null ? root.getDocumentPaneState () : null;
}
/**
* Updates document pane state.
* This will remove all added documents and reopen them according to restored state.
* Make sure that you add all required documents before loading restoring the state.
* Otherwise in some cases you might get unwanted effect like lost structure parts.
*
* Also be aware that this call might generate some unwanted events like documents close and such.
* So make sure to add your listeners after this call in case you don't want to listen to initial state restore events.
*
* @param state document pane state to restore
* @see DocumentPaneState
* @see #getDocumentPaneState()
*/
public void setDocumentPaneState ( final DocumentPaneState state )
{
// Mapping all opened documents
// In case some are missing they can be filled-in later using document provider
// It doesn't really matter where these documents come from but we assume that they might already be opened
final List openedDocuments = getDocuments ();
final Map documents = new HashMap ( openedDocuments.size () );
for ( final T document : openedDocuments )
{
documents.put ( document.getId (), document );
}
// Temporarily removing all documents to place them properly after
closeAll ();
// Restoring document pane state
setStructureRoot ( restoreStructureStateImpl ( state, documents ) );
}
/**
* Restores {@link StructureData} restored from provided {@link DocumentPaneState}.
*
* @param state document pane state to restore
* @param documents existing documents
* @return {@link StructureData} restored from provided {@link DocumentPaneState}
*/
protected StructureData restoreStructureStateImpl ( final DocumentPaneState state, final Map documents )
{
final StructureData restored;
if ( state.isSplit () )
{
// Restoring split data
final Pair splitState = state.getSplitState ();
final StructureData first = restoreStructureStateImpl ( splitState.getKey (), documents );
final StructureData last = restoreStructureStateImpl ( splitState.getValue (), documents );
final SplitData splitData = new SplitData ( this, state.getSplitOrientation (), first, last );
// todo Location updates should be queued and performed 1 by 1 from top to bottom to avoid incorrect layout
// We have to wait until split is properly sized before restoring divider location
splitData.getSplitPane ().addComponentListener ( new ComponentAdapter ()
{
@Override
public void componentResized ( final ComponentEvent e )
{
// Waiting until split is properly sized
final WebSplitPane splitPane = splitData.getSplitPane ();
if ( splitPane.getWidth () > 0 || splitPane.getHeight () > 0 )
{
// Updating proportional divider location
splitData.setDividerLocation ( state.getDividerLocation () );
splitPane.removeComponentListener ( this );
}
}
} );
restored = splitData;
}
else
{
// Restoring pane data
final PaneData paneData = new PaneData ( this );
if ( state.getDocumentIds () != null )
{
for ( final String id : state.getDocumentIds () )
{
// In case document doesn't exist, try requesting it from provider if we have one
if ( documentsProvider != null && !documents.containsKey ( id ) )
{
documents.put ( id, documentsProvider.apply ( id ) );
}
// Simply open document if it exists
if ( documents.containsKey ( id ) )
{
final T document = documents.get ( id );
if ( document != null )
{
paneData.open ( document );
}
}
}
}
// Update selected document
final String selectedId = state.getSelectedId ();
if ( selectedId != null && documents.containsKey ( selectedId ) )
{
paneData.setSelected ( documents.get ( selectedId ) );
}
restored = paneData;
}
return restored;
}
/**
* Adds document pane listener.
*
* @param listener new document pane listener
*/
public void addDocumentPaneListener ( final DocumentPaneListener listener )
{
listenerList.add ( DocumentPaneListener.class, listener );
}
/**
* Removes document pane listener.
*
* @param listener document pane listener
*/
public void removeDocumentPaneListener ( final DocumentPaneListener listener )
{
listenerList.remove ( DocumentPaneListener.class, listener );
}
/**
* Fires PaneData split event.
*
* @param splittedPane splitted PaneData
* @param newSplitData newly created SplitData
*/
public void fireSplitted ( final PaneData splittedPane, final SplitData newSplitData )
{
for ( final DocumentPaneListener listener : listenerList.getListeners ( DocumentPaneListener.class ) )
{
listener.splitted ( this, splittedPane, newSplitData );
}
}
/**
* Fires SplitData merge event.
*
* @param mergedSplit merged SplitData
* @param newStructureData newly created StructureData
*/
public void fireMerged ( final SplitData mergedSplit, final StructureData newStructureData )
{
for ( final DocumentPaneListener listener : listenerList.getListeners ( DocumentPaneListener.class ) )
{
listener.merged ( this, mergedSplit, newStructureData );
}
}
/**
* Fires SplitData orientation change event.
*
* @param splitData SplitData which orientation has changed
*/
public void fireOrientationChanged ( final SplitData splitData )
{
for ( final DocumentPaneListener listener : listenerList.getListeners ( DocumentPaneListener.class ) )
{
listener.orientationChanged ( this, splitData );
}
}
/**
* Fires SplitData sides swap event.
*
* @param splitData SplitData which sides were swapped
*/
public void fireSidesSwapped ( final SplitData splitData )
{
for ( final DocumentPaneListener listener : listenerList.getListeners ( DocumentPaneListener.class ) )
{
listener.sidesSwapped ( this, splitData );
}
}
/**
* Fires SplitData divider location change event.
*
* @param splitData SplitData which divider location has changed
*/
public void fireDividerLocationChanged ( final SplitData splitData )
{
for ( final DocumentPaneListener listener : listenerList.getListeners ( DocumentPaneListener.class ) )
{
listener.dividerLocationChanged ( this, splitData );
}
}
/**
* Adds document listener.
*
* @param listener new document listener
*/
public void addDocumentListener ( final DocumentListener listener )
{
listenerList.add ( DocumentListener.class, listener );
}
/**
* Removes document listener.
*
* @param listener document listener
*/
public void removeDocumentListener ( final DocumentListener listener )
{
listenerList.remove ( DocumentListener.class, listener );
}
/**
* Fires document opened event.
*
* @param document opened document
* @param pane document's pane
* @param index document's index
*/
public void fireDocumentOpened ( final T document, final PaneData pane, final int index )
{
for ( final DocumentListener listener : listenerList.getListeners ( DocumentListener.class ) )
{
listener.opened ( document, pane, index );
}
}
/**
* Fires document selected event.
*
* @param document selected document
* @param pane document's pane
* @param index document's index
*/
public void fireDocumentSelected ( final T document, final PaneData pane, final int index )
{
for ( final DocumentListener listener : listenerList.getListeners ( DocumentListener.class ) )
{
listener.selected ( document, pane, index );
}
}
/**
* Fires document closing event.
* Returns whether document is allowed to close or not.
*
* @param document closing document
* @param pane document's pane
* @param index document's index
* @return true if document is allowed to close, false otherwise
*/
public boolean fireDocumentClosing ( final T document, final PaneData pane, final int index )
{
boolean allow = true;
for ( final DocumentListener listener : listenerList.getListeners ( DocumentListener.class ) )
{
allow = allow && listener.closing ( document, pane, index );
}
return allow;
}
/**
* Fires document closed event.
*
* @param document closed document
* @param pane document's pane
* @param index document's index
*/
public void fireDocumentClosed ( final T document, final PaneData pane, final int index )
{
for ( final DocumentListener listener : listenerList.getListeners ( DocumentListener.class ) )
{
listener.closed ( document, pane, index );
}
}
@Override
public DocumentListener onDocumentOpen ( final DocumentDataRunnable runnable )
{
return DocumentPaneEventMethodsImpl.onDocumentOpen ( this, runnable );
}
@Override
public DocumentListener onDocumentSelection ( final DocumentDataRunnable runnable )
{
return DocumentPaneEventMethodsImpl.onDocumentSelection ( this, runnable );
}
@Override
public DocumentListener onDocumentClosing ( final DocumentDataCancellableRunnable runnable )
{
return DocumentPaneEventMethodsImpl.onDocumentClosing ( this, runnable );
}
@Override
public DocumentListener onDocumentClose ( final DocumentDataRunnable runnable )
{
return DocumentPaneEventMethodsImpl.onDocumentClose ( this, runnable );
}
@Override
public void registerSettings ( final Configuration configuration )
{
UISettingsManager.registerComponent ( this, configuration );
}
@Override
public void registerSettings ( final SettingsProcessor processor )
{
UISettingsManager.registerComponent ( this, processor );
}
@Override
public void unregisterSettings ()
{
UISettingsManager.unregisterComponent ( this );
}
@Override
public void loadSettings ()
{
UISettingsManager.loadSettings ( this );
}
@Override
public void saveSettings ()
{
UISettingsManager.saveSettings ( this );
}
/**
* Returns pane data stored inside the tabbed pane component.
*
* @param tabbedPane tabbed pane component
* @param document type
* @return pane data stored inside the tabbed pane component
*/
public static PaneData getData ( final WebTabbedPane tabbedPane )
{
return ( PaneData ) tabbedPane.getClientProperty ( DATA_KEY );
}
/**
* Returns split data stored inside the split pane component.
*
* @param splitPane split pane component
* @param document type
* @return split data stored inside the split pane component
*/
public static SplitData getData ( final WebSplitPane splitPane )
{
return ( SplitData ) splitPane.getClientProperty ( DATA_KEY );
}
/**
* Returns whether the specified element is an empty pane or not.
*
* @param data structure element to check
* @return true if the specified element is an empty pane, false otherwise
*/
public static boolean isEmptyPane ( final StructureData data )
{
return data instanceof PaneData && ( ( PaneData ) data ).count () == 0;
}
}