Torque3D/Engine/source/gui/containers/guiSplitContainer.cpp

638 lines
21 KiB
C++
Raw Normal View History

2012-09-19 15:15:01 +00:00
//-----------------------------------------------------------------------------
// Copyright (c) 2012 GarageGames, LLC
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to
// deal in the Software without restriction, including without limitation the
// rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
// sell copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
// IN THE SOFTWARE.
//-----------------------------------------------------------------------------
#include "platform/platform.h"
#include "gui/containers/guiSplitContainer.h"
#include "gui/core/guiCanvas.h"
#include "console/consoleTypes.h"
#include "gfx/gfxDrawUtil.h"
IMPLEMENT_CONOBJECT( GuiSplitContainer );
ConsoleDocClass( GuiSplitContainer,
"@brief A container that splits its area between two child controls.\n\n"
"A GuiSplitContainer can be used to dynamically subdivide an area between two child controls. "
"A splitter bar is placed between the two controls and allows to dynamically adjust the sizing "
"ratio between the two sides. Splitting can be either horizontal (subdividing top and bottom) "
"or vertical (subdividing left and right) depending on #orientation.\n\n"
"By using #fixedPanel, one of the panels can be chosen to remain at a fixed size (#fixedSize)."
"@tsexample\n"
"// Create a vertical splitter with a fixed-size left panel.\n"
"%splitter = new GuiSplitContainer()\n"
"{\n"
" orientation = \"Vertical\";\n"
" fixedPanel = \"FirstPanel\";\n"
" fixedSize = 100;\n"
"\n"
" new GuiScrollCtrl()\n"
" {\n"
" new GuiMLTextCtrl()\n"
" {\n"
" text = %longText;\n"
" };\n"
" };\n"
"\n"
" new GuiScrollCtrl()\n"
" {\n"
" new GuiMLTextCtrl()\n"
" {\n"
" text = %moreLongText;\n"
" };\n"
" };\n"
"};\n"
"@endtsexample\n\n"
"@note The children placed inside GuiSplitContainers must be GuiContainers.\n\n"
"@ingroup GuiContainers"
);
ImplementEnumType( GuiSplitOrientation,
"Axis along which to divide the container's space.\n\n"
"@ingroup GuiContainers" )
{ GuiSplitContainer::Vertical, "Vertical", "Divide vertically placing one child left and one child right." },
{ GuiSplitContainer::Horizontal, "Horizontal", "Divide horizontally placing one child on top and one child below." }
EndImplementEnumType;
ImplementEnumType( GuiSplitFixedPanel,
"Which side of the splitter to keep at a fixed size (if any).\n\n"
"@ingroup GuiContainers" )
{ GuiSplitContainer::None, "None", "Allow both childs to resize (default)." },
{ GuiSplitContainer::FirstPanel, "FirstPanel", "Keep " },
{ GuiSplitContainer::SecondPanel, "SecondPanel" }
EndImplementEnumType;
//-----------------------------------------------------------------------------
GuiSplitContainer::GuiSplitContainer()
: mFixedPanel( None ),
mFixedPanelSize( 100 ),
mOrientation( Vertical ),
mSplitterSize( 2 ),
mSplitPoint( 0, 0 ),
mSplitRect( 0, 0, mSplitterSize, mSplitterSize ),
mDragging( false )
{
setMinExtent( Point2I(64,64) );
setExtent(200,200);
setDocking( Docking::dockNone );
mSplitPoint.set( 300, 100 );
// We only support client docked items in a split container
mValidDockingMask = Docking::dockClient;
}
//-----------------------------------------------------------------------------
void GuiSplitContainer::initPersistFields()
{
addGroup( "Splitter", "Options to configure split panels contained by this control" );
addField( "orientation", TYPEID< Orientation >(), Offset( mOrientation, GuiSplitContainer),
"Whether to split between top and bottom (horizontal) or between left and right (vertical)." );
addField( "splitterSize", TypeS32, Offset( mSplitterSize, GuiSplitContainer),
"Width of the splitter bar between the two sides. Default is 2." );
addField( "splitPoint", TypePoint2I, Offset( mSplitPoint, GuiSplitContainer),
"Point on control through which the splitter goes.\n\n"
"Changed relatively if size of control changes." );
addField( "fixedPanel", TYPEID< FixedPanel >(), Offset( mFixedPanel, GuiSplitContainer),
"Which (if any) side of the splitter to keep at a fixed size." );
addField( "fixedSize", TypeS32, Offset( mFixedPanelSize, GuiSplitContainer),
"Width of the fixed panel specified by #fixedPanel (if any)." );
endGroup( "Splitter" );
Parent::initPersistFields();
}
//-----------------------------------------------------------------------------
bool GuiSplitContainer::onAdd()
{
if ( !Parent::onAdd() )
return false;
return true;
}
//-----------------------------------------------------------------------------
bool GuiSplitContainer::onWake()
{
if ( !Parent::onWake() )
return false;
// Create Panel 1
if ( empty() )
{
GuiPanel *newPanel = new GuiPanel();
AssertFatal( newPanel, "GuiSplitContainer::onAdd - Cannot create subordinate panel #1!" );
newPanel->registerObject();
newPanel->setInternalName( "Panel1" );
newPanel->setDocking( Docking::dockClient );
addObject( (SimObject*)newPanel );
}
else
{
GuiContainer *containerCtrl = dynamic_cast<GuiContainer*>( at(0) );
if ( containerCtrl )
{
containerCtrl->setInternalName( "Panel1" );
containerCtrl->setDocking( Docking::dockClient );
}
}
if ( size() == 1 )
{
// Create Panel 2
GuiPanel *newPanel = new GuiPanel();
AssertFatal( newPanel, "GuiSplitContainer::onAdd - Cannot create subordinate panel #2!" );
newPanel->registerObject();
newPanel->setInternalName( "Panel2" );
newPanel->setDocking( Docking::dockClient );
addObject( (SimObject*)newPanel );
}
else
{
GuiContainer *containerCtrl = dynamic_cast<GuiContainer*>( at(1) );
if ( containerCtrl )
{
containerCtrl->setInternalName( "Panel2" );
containerCtrl->setDocking( Docking::dockClient );
}
}
// Has FixedWidth been specified?
if ( mFixedPanelSize == 0 )
{
// Nope, so try to guess as best we can
GuiContainer *firstPanel = dynamic_cast<GuiContainer*>( at(0) );
GuiContainer *secondPanel = dynamic_cast<GuiContainer*>( at(1) );
if ( mFixedPanel == FirstPanel )
{
if ( mOrientation == Horizontal )
mFixedPanelSize = firstPanel->getExtent().y;
else
mFixedPanelSize = firstPanel->getExtent().x;
mSplitPoint = Point2I( mFixedPanelSize, mFixedPanelSize );
}
else if ( mFixedPanel == SecondPanel )
{
if ( mOrientation == Horizontal )
mFixedPanelSize = getExtent().y - secondPanel->getExtent().y;
else
mFixedPanelSize = getExtent().x - secondPanel->getExtent().x;
mSplitPoint = getExtent() - Point2I( mFixedPanelSize, mFixedPanelSize );
}
}
setUpdateLayout();
return true;
}
//-----------------------------------------------------------------------------
void GuiSplitContainer::onRender( Point2I offset, const RectI &updateRect )
{
Parent::onRender( offset, updateRect );
// Only render if we're dragging the splitter
if ( mDragging && mSplitRect.isValidRect() )
{
// Splitter Rectangle (will adjust positioning only)
RectI splitterRect = mSplitRect;
// Currently being dragged to Rect
Point2I splitterPoint = localToGlobalCoord( mSplitRect.point );
splitterRect.point = localToGlobalCoord( mSplitPoint );
RectI clientRect = getClientRect();
clientRect.point = localToGlobalCoord( clientRect.point );
if ( mOrientation == Horizontal )
{
splitterRect.point.y -= mSplitterSize;
splitterRect.point.x = splitterPoint.x;
}
else
{
splitterRect.point.x -= mSplitterSize;
splitterRect.point.y = splitterPoint.y;
}
RectI oldClip = GFX->getClipRect();
GFX->setClipRect( clientRect );
GFX->getDrawUtil()->drawRectFill( splitterRect, mProfile->mFillColorHL );
GFX->setClipRect( oldClip );
}
else
{
RectI splitterRect = mSplitRect;
splitterRect.point += offset;
GFX->getDrawUtil()->drawRectFill( splitterRect, mProfile->mFillColor );
}
}
//-----------------------------------------------------------------------------
Point2I GuiSplitContainer::getMinExtent() const
{
GuiContainer *panelOne = dynamic_cast<GuiContainer*>( at(0) );
GuiContainer *panelTwo = dynamic_cast<GuiContainer*>( at(1) );
if ( !panelOne || !panelTwo )
return Parent::getMinExtent();
Point2I minExtent = Point2I(0,0);
Point2I panelOneMinExtent = panelOne->getMinExtent();
Point2I panelTwoMinExtent = panelTwo->getMinExtent();
if ( mOrientation == Horizontal )
{
minExtent.y = 2 * mSplitterSize + panelOneMinExtent.y + panelTwoMinExtent.y;
minExtent.x = getMax( panelOneMinExtent.x, panelTwoMinExtent.x );
}
else
{
minExtent.x = 2 * mSplitterSize + panelOneMinExtent.x + panelTwoMinExtent.x;
minExtent.y = getMax( panelOneMinExtent.y, panelTwoMinExtent.y );
}
return minExtent;
}
//-----------------------------------------------------------------------------
void GuiSplitContainer::parentResized( const RectI &oldParentRect, const RectI &newParentRect )
{
Parent::parentResized( oldParentRect, newParentRect );
return;
// TODO: Is this right James? This isn't needed anymore?
/*
// GuiSplitContainer overrides parentResized to make sure that the proper fixed frame's width/height
// is not compromised in the call
if ( size() < 2 )
return;
GuiContainer *panelOne = dynamic_cast<GuiContainer*>( at(0) );
GuiContainer *panelTwo = dynamic_cast<GuiContainer*>( at(1) );
AssertFatal( panelOne && panelTwo, "GuiSplitContainer::parentResized - Missing/Invalid Subordinate Controls! Split contained controls must derive from GuiContainer!" );
Point2I newDragPos;
if ( mFixedPanel == FirstPanel )
{
newDragPos = panelOne->getExtent();
newDragPos += Point2I( mSplitterSize, mSplitterSize );
}
else if ( mFixedPanel == SecondPanel )
{
newDragPos = getExtent() - panelTwo->getExtent();
newDragPos -= Point2I( mSplitterSize, mSplitterSize );
}
else // None
newDragPos.set( 1, 1);
RectI clientRect = getClientRect();
solvePanelConstraints( newDragPos, panelOne, panelTwo, clientRect );
setUpdateLayout();
*/
}
//-----------------------------------------------------------------------------
bool GuiSplitContainer::resize( const Point2I &newPosition, const Point2I &newExtent )
{
// Save previous extent.
Point2I oldExtent = getExtent();
// Resize ourselves.
if ( !Parent::resize( newPosition, newExtent ) || size() < 2 )
return false;
GuiContainer *panelOne = dynamic_cast<GuiContainer*>( at(0) );
GuiContainer *panelTwo = dynamic_cast<GuiContainer*>( at(1) );
//
AssertFatal( panelOne && panelTwo, "GuiSplitContainer::resize - Missing/Invalid Subordinate Controls! Split contained controls must derive from GuiContainer!" );
// We only need to update the split point if our second panel is fixed.
// If the first is fixed, then we can leave the split point alone because
// the remainder of the size will be added to or taken from the second panel
Point2I newDragPos;
if ( mFixedPanel == SecondPanel )
{
S32 deltaX = newExtent.x - oldExtent.x;
S32 deltaY = newExtent.y - oldExtent.y;
if( mOrientation == Horizontal )
mSplitPoint.y += deltaY;
else
mSplitPoint.x += deltaX;
}
// If we got here, parent returned true
return true;
}
//-----------------------------------------------------------------------------
bool GuiSplitContainer::layoutControls( RectI &clientRect )
{
if ( size() < 2 )
return false;
GuiContainer *panelOne = dynamic_cast<GuiContainer*>( at(0) );
GuiContainer *panelTwo = dynamic_cast<GuiContainer*>( at(1) );
//
AssertFatal( panelOne && panelTwo, "GuiSplitContainer::layoutControl - Missing/Invalid Subordinate Controls! Split contained controls must derive from GuiContainer!" );
RectI panelOneRect = RectI( clientRect.point, Point2I( 0, 0 ) );
RectI panelTwoRect;
RectI splitRect;
solvePanelConstraints( getSplitPoint(), panelOne, panelTwo, clientRect );
switch( mOrientation )
{
case Horizontal:
panelOneRect.extent = Point2I( clientRect.extent.x, getSplitPoint().y );
panelTwoRect = panelOneRect;
panelTwoRect.intersect( clientRect );
panelTwoRect.point.y = panelOneRect.extent.y;
panelTwoRect.extent.y = clientRect.extent.y - panelOneRect.extent.y;
// Generate new Splitter Rectangle
splitRect = panelTwoRect;
splitRect.extent.y = 0;
splitRect.inset( 0, -mSplitterSize );
panelOneRect.extent.y -= mSplitterSize;
panelTwoRect.point.y += mSplitterSize;
panelTwoRect.extent.y -= mSplitterSize;
break;
case Vertical:
panelOneRect.extent = Point2I( getSplitPoint().x, clientRect.extent.y );
panelTwoRect = panelOneRect;
panelTwoRect.intersect( clientRect );
panelTwoRect.point.x = panelOneRect.extent.x;
panelTwoRect.extent.x = clientRect.extent.x - panelOneRect.extent.x;
// Generate new Splitter Rectangle
splitRect = panelTwoRect;
splitRect.extent.x = 0;
splitRect.inset( -mSplitterSize, 0 );
panelOneRect.extent.x -= mSplitterSize;
panelTwoRect.point.x += mSplitterSize;
panelTwoRect.extent.x -= mSplitterSize;
break;
}
// Update Split Rect
mSplitRect = splitRect;
// Dock Appropriately
if( !( mFixedPanel == FirstPanel && !panelOne->isVisible() ) )
dockControl( panelOne, panelOne->getDocking(), panelOneRect );
if( !( mFixedPanel == FirstPanel && !panelTwo->isVisible() ) )
dockControl( panelTwo, panelOne->getDocking(), panelTwoRect );
//
return false;
}
//-----------------------------------------------------------------------------
void GuiSplitContainer::solvePanelConstraints(Point2I newDragPos, GuiContainer * firstPanel, GuiContainer * secondPanel, const RectI& clientRect)
2012-09-19 15:15:01 +00:00
{
if( !firstPanel || !secondPanel )
return;
if ( mOrientation == Horizontal )
{
// Constrain based on Y axis (Horizontal Splitter)
// This accounts for the splitter width
S32 splitterSize = (S32)(mSplitRect.extent.y * 0.5);
// Collapsed fixed panel
if ( mFixedPanel == SecondPanel && !secondPanel->isVisible() )
{
newDragPos = Point2I(mSplitPoint.x, getExtent().y - splitterSize );
}
else if( mFixedPanel == SecondPanel && !firstPanel->isVisible() )
{
newDragPos = Point2I(mSplitPoint.x, splitterSize );
}
else // Normal constraints
{
//newDragPos.y -= splitterSize;
S32 newPosition = mClamp( newDragPos.y,
firstPanel->getMinExtent().y + splitterSize,
getExtent().y - secondPanel->getMinExtent().y - splitterSize );
newDragPos = Point2I( mSplitPoint.x, newPosition );
}
}
else
{
// Constrain based on X axis (Vertical Splitter)
// This accounts for the splitter width
S32 splitterSize = (S32)(mSplitRect.extent.x * 0.5);
// Collapsed fixed panel
if ( mFixedPanel == SecondPanel && !secondPanel->isVisible() )
{
newDragPos = Point2I(getExtent().x - splitterSize, mSplitPoint.y );
}
else if ( mFixedPanel == FirstPanel && !firstPanel->isVisible() )
{
newDragPos = Point2I( splitterSize, mSplitPoint.x );
}
else // Normal constraints
{
S32 newPosition = mClamp( newDragPos.x, firstPanel->getMinExtent().x + splitterSize,
getExtent().x - secondPanel->getMinExtent().x - splitterSize );
newDragPos = Point2I( newPosition, mSplitPoint.y );
}
}
// Just in case, clamp to bounds of controls
newDragPos.x = mClamp( newDragPos.x, clientRect.point.x, clientRect.point.x + clientRect.extent.x );
newDragPos.y = mClamp( newDragPos.y, clientRect.point.y, clientRect.point.y + clientRect.extent.y );
mSplitPoint = newDragPos;
}
//-----------------------------------------------------------------------------
void GuiSplitContainer::getCursor( GuiCursor *&cursor, bool &showCursor, const GuiEvent &lastGuiEvent )
{
GuiCanvas *rootCtrl = getRoot();
if ( !rootCtrl )
return;
S32 desiredCursor = 0;
RectI splitRect = getSplitRect();
// Figure out which cursor we want if we need one
if ( mOrientation == Horizontal )
desiredCursor = PlatformCursorController::curResizeHorz;
else if ( mOrientation == Vertical )
desiredCursor = PlatformCursorController::curResizeVert;
PlatformWindow *platformWindow = static_cast<GuiCanvas*>(getRoot())->getPlatformWindow();
AssertFatal( platformWindow != NULL,"GuiControl without owning platform window! This should not be possible." );
PlatformCursorController *cusrorController = platformWindow->getCursorController();
AssertFatal( cusrorController != NULL,"PlatformWindow without an owned CursorController!" );
// Check to see if we need one or just the default...
Point2I localPoint = Point2I( globalToLocalCoord( lastGuiEvent.mousePoint ) );
if ( splitRect.pointInRect( localPoint ) || mDragging )
{
// Do we need to change it or is it already set?
if ( rootCtrl->mCursorChanged != desiredCursor )
{
// We've already changed the cursor, so set it back
if ( rootCtrl->mCursorChanged != -1 )
cusrorController->popCursor();
// Now change the cursor shape
cusrorController->pushCursor( desiredCursor );
rootCtrl->mCursorChanged = desiredCursor;
}
}
else if ( rootCtrl->mCursorChanged != -1 )
{
// Just the default
cusrorController->popCursor();
rootCtrl->mCursorChanged = -1;
}
}
//-----------------------------------------------------------------------------
void GuiSplitContainer::onMouseDown( const GuiEvent &event )
{
GuiContainer *firstPanel = dynamic_cast<GuiContainer*>(at(0));
GuiContainer *secondPanel = dynamic_cast<GuiContainer*>(at(1));
// This function will constrain the panels to their minExtents and update the mSplitPoint
if ( firstPanel && secondPanel )
{
mouseLock();
mDragging = true;
RectI clientRect = getClientRect();
Point2I newDragPos = globalToLocalCoord( event.mousePoint );
solvePanelConstraints(newDragPos, firstPanel, secondPanel, clientRect);
}
}
//-----------------------------------------------------------------------------
void GuiSplitContainer::onMouseUp( const GuiEvent &event )
{
// If we've been dragging, we need to update the fixed panel extent.
// NOTE : This should ONLY ever happen in this function. the Fixed panel
// is to REMAIN FIXED unless the user changes it.
if ( mDragging )
{
Point2I newSplitPoint = getSplitPoint();
// Update Fixed Panel Extent
if ( mFixedPanel == FirstPanel )
mFixedPanelSize = ( mOrientation == Horizontal ) ? newSplitPoint.y : newSplitPoint.x;
else
mFixedPanelSize = ( mOrientation == Horizontal ) ? getExtent().y - newSplitPoint.y : getExtent().x - newSplitPoint.x;
setUpdateLayout();
}
mDragging = false;
mouseUnlock();
}
//-----------------------------------------------------------------------------
void GuiSplitContainer::onMouseDragged( const GuiEvent &event )
{
GuiContainer *firstPanel = dynamic_cast<GuiContainer*>(at(0));
GuiContainer *secondPanel = dynamic_cast<GuiContainer*>(at(1));
// This function will constrain the panels to their minExtents and update the mSplitPoint
if ( mDragging && firstPanel && secondPanel )
{
RectI clientRect = getClientRect();
Point2I newDragPos = globalToLocalCoord( event.mousePoint );
solvePanelConstraints(newDragPos, firstPanel, secondPanel, clientRect);
}
}
void GuiSplitContainer::setSplitPoint(Point2I splitPoint)
{
GuiContainer *firstPanel = dynamic_cast<GuiContainer*>(at(0));
GuiContainer *secondPanel = dynamic_cast<GuiContainer*>(at(1));
// This function will constrain the panels to their minExtents and update the mSplitPoint
if (firstPanel && secondPanel)
{
RectI clientRect = getClientRect();
solvePanelConstraints(splitPoint, firstPanel, secondPanel, clientRect);
layoutControls(clientRect);
}
}
DefineEngineMethod(GuiSplitContainer, setSplitPoint, void, (Point2I splitPoint), ,
"Set the position of the split handle.")
{
object->setSplitPoint(splitPoint);
}