/** * @file lllayoutstack.cpp * @brief LLLayout class - dynamic stacking of UI elements * * $LicenseInfo:firstyear=2001&license=viewerlgpl$ * Second Life Viewer Source Code * Copyright (C) 2010, Linden Research, Inc. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; * version 2.1 of the License only. * * This 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA * * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA * $/LicenseInfo$ */ // Opaque view with a background and a border. Can contain LLUICtrls. #include "linden_common.h" #include "lllayoutstack.h" #include "lllocalcliprect.h" #include "llpanel.h" #include "llresizebar.h" #include "lluictrlfactory.h" #include "llcriticaldamp.h" #include "boost/foreach.hpp" static const F32 MIN_FRACTIONAL_SIZE = 0.0f; static const F32 MAX_FRACTIONAL_SIZE = 1.f; static const S32 RESIZE_BAR_OVERLAP = 1; static const S32 RESIZE_BAR_HEIGHT = 3; // // LLLayoutPanel // LLLayoutPanel::LLLayoutPanel(LLPanel* panelp, LLLayoutStack::eLayoutOrientation orientation, S32 min_width, S32 min_height, BOOL auto_resize, BOOL user_resize) : mPanel(panelp), mMinWidth(min_width), mMinHeight(min_height), mAutoResize(auto_resize), mUserResize(user_resize), mCollapsed(FALSE), mCollapseAmt(0.f), mVisibleAmt(1.f), // default to fully visible mResizeBar(NULL), mOrientation(orientation) { LLResizeBar::Side side = (orientation == LLLayoutStack::HORIZONTAL) ? LLResizeBar::RIGHT : LLResizeBar::BOTTOM; S32 min_dim; if (orientation == LLLayoutStack::HORIZONTAL) { min_dim = mMinHeight; } else { min_dim = mMinWidth; } LLResizeBar::Params p; p.name = "resizer"; p.resizing_view = mPanel; p.min_size = min_dim; p.max_size = S32_MAX; p.side = side; mResizeBar = LLUICtrlFactory::create(p); mResizeBar->setEnableSnapping(FALSE); // panels initialized as hidden should not start out partially visible if (!mPanel->getVisible()) { mVisibleAmt = 0.f; } } LLLayoutPanel::~LLLayoutPanel() { // probably not necessary, but... delete mResizeBar; mResizeBar = NULL; } F32 LLLayoutPanel::getCollapseFactor() { if (mOrientation == LLLayoutStack::HORIZONTAL) { F32 collapse_amt = clamp_rescale(mCollapseAmt, 0.f, 1.f, 1.f, (F32)mMinWidth / (F32)llmax(1, mPanel->getRect().getWidth())); return mVisibleAmt * collapse_amt; } else { F32 collapse_amt = clamp_rescale(mCollapseAmt, 0.f, 1.f, 1.f, llmin(1.f, (F32)mMinHeight / (F32)llmax(1, mPanel->getRect().getHeight()))); return mVisibleAmt * collapse_amt; } } static LLRegisterWidget r2("layout_stack"); LLLayoutStack::LLLayoutStack(eLayoutOrientation orientation) : mOrientation(orientation), mMinWidth(0), mMinHeight(0), mPanelSpacing(RESIZE_BAR_HEIGHT) { } LLLayoutStack::~LLLayoutStack() { e_panel_list_t panels = mPanels; // copy list of panel pointers mPanels.clear(); // clear so that removeChild() calls don't cause trouble std::for_each(panels.begin(), panels.end(), DeletePointer()); } void LLLayoutStack::draw() { updateLayout(); e_panel_list_t::iterator panel_it; for (panel_it = mPanels.begin(); panel_it != mPanels.end(); ++panel_it) { // clip to layout rectangle, not bounding rectangle LLRect clip_rect = (*panel_it)->mPanel->getRect(); // scale clipping rectangle by visible amount if (mOrientation == HORIZONTAL) { clip_rect.mRight = clip_rect.mLeft + llround((F32)clip_rect.getWidth() * (*panel_it)->getCollapseFactor()); } else { clip_rect.mBottom = clip_rect.mTop - llround((F32)clip_rect.getHeight() * (*panel_it)->getCollapseFactor()); } LLPanel* panelp = (*panel_it)->mPanel; LLLocalClipRect clip(clip_rect); // only force drawing invisible children if visible amount is non-zero drawChild(panelp, 0, 0, clip_rect.notEmpty()); } } void LLLayoutStack::removeChild(LLView* ctrl) { LLView::removeChild(ctrl); LLPanel* panel = dynamic_cast(ctrl); if(!panel) return; LLLayoutPanel* embedded_panelp = findEmbeddedPanel(panel); if (embedded_panelp) { mPanels.erase(std::find(mPanels.begin(), mPanels.end(), embedded_panelp)); delete embedded_panelp; } // need to update resizebars calcMinExtents(); } LLXMLNodePtr LLLayoutStack::getXML(bool save_children) const { LLXMLNodePtr node = LLView::getXML(); node->setName(LL_LAYOUT_STACK_TAG); if (mOrientation == HORIZONTAL) { node->createChild("orientation", TRUE)->setStringValue("horizontal"); } else { node->createChild("orientation", TRUE)->setStringValue("vertical"); } if (save_children) { LLView::child_list_const_reverse_iter_t rit; for (rit = getChildList()->rbegin(); rit != getChildList()->rend(); ++rit) { LLView* childp = *rit; if (childp->getSaveToXML()) { LLXMLNodePtr xml_node = childp->getXML(); if (xml_node->hasName(LL_PANEL_TAG)) { xml_node->setName(LL_LAYOUT_PANEL_TAG); } node->addChild(xml_node); } } } return node; } //static LLView* LLLayoutStack::fromXML(LLXMLNodePtr node, LLView *parent, LLUICtrlFactory *factory) { std::string orientation_string("vertical"); node->getAttributeString("orientation", orientation_string); eLayoutOrientation orientation = VERTICAL; if (orientation_string == "horizontal") { orientation = HORIZONTAL; } else if (orientation_string == "vertical") { orientation = VERTICAL; } else { llwarns << "Unknown orientation " << orientation_string << ", using vertical" << llendl; } LLLayoutStack* layout_stackp = new LLLayoutStack(orientation); node->getAttributeS32("border_size", layout_stackp->mPanelSpacing); // don't allow negative spacing values layout_stackp->mPanelSpacing = llmax(layout_stackp->mPanelSpacing, 0); std::string name("stack"); node->getAttributeString("name", name); layout_stackp->setName(name); layout_stackp->initFromXML(node, parent); LLXMLNodePtr child; for (child = node->getFirstChild(); child.notNull(); child = child->getNextSibling()) { S32 min_width = 0; S32 min_height = 0; BOOL auto_resize = TRUE; child->getAttributeS32("min_width", min_width); child->getAttributeS32("min_height", min_height); child->getAttributeBOOL("auto_resize", auto_resize); if (child->hasName("layout_panel")) { BOOL user_resize = TRUE; child->getAttributeBOOL("user_resize", user_resize); LLPanel* panelp = (LLPanel*)LLPanel::fromXML(child, layout_stackp, factory); if (panelp) { panelp->setFollowsNone(); layout_stackp->addPanel(panelp, min_width, min_height, auto_resize, user_resize); } } else { BOOL user_resize = FALSE; child->getAttributeBOOL("user_resize", user_resize); LLPanel* panelp = new LLPanel(std::string("auto_panel")); LLView* new_child = factory->createWidget(panelp, child); if (new_child) { // put child in new embedded panel layout_stackp->addPanel(panelp, min_width, min_height, auto_resize, user_resize); // resize panel to contain widget and move widget to be contained in panel panelp->setRect(new_child->getRect()); new_child->setOrigin(0, 0); } else { panelp->die(); } } } layout_stackp->updateLayout(); return layout_stackp; } S32 LLLayoutStack::getDefaultHeight(S32 cur_height) { // if we are spanning our children (crude upward propagation of size) // then don't enforce our size on our children if (mOrientation == HORIZONTAL) { cur_height = llmax(mMinHeight, getRect().getHeight()); } return cur_height; } S32 LLLayoutStack::getDefaultWidth(S32 cur_width) { // if we are spanning our children (crude upward propagation of size) // then don't enforce our size on our children if (mOrientation == VERTICAL) { cur_width = llmax(mMinWidth, getRect().getWidth()); } return cur_width; } void LLLayoutStack::addPanel(LLPanel* panel, S32 min_width, S32 min_height, BOOL auto_resize, BOOL user_resize, EAnimate animate, S32 index) { // panel starts off invisible (collapsed) if (animate == ANIMATE) { panel->setVisible(FALSE); } LLLayoutPanel* embedded_panel = new LLLayoutPanel(panel, mOrientation, min_width, min_height, auto_resize, user_resize); mPanels.insert(mPanels.begin() + llclamp(index, 0, (S32)mPanels.size()), embedded_panel); addChild(panel); addChild(embedded_panel->mResizeBar); // bring all resize bars to the front so that they are clickable even over the panels // with a bit of overlap for (e_panel_list_t::iterator panel_it = mPanels.begin(); panel_it != mPanels.end(); ++panel_it) { LLResizeBar* resize_barp = (*panel_it)->mResizeBar; sendChildToFront(resize_barp); } // start expanding panel animation if (animate == ANIMATE) { panel->setVisible(TRUE); } } void LLLayoutStack::removePanel(LLPanel* panel) { removeChild(panel); } void LLLayoutStack::collapsePanel(LLPanel* panel, BOOL collapsed) { LLLayoutPanel* panel_container = findEmbeddedPanel(panel); if (!panel_container) return; panel_container->mCollapsed = collapsed; } void LLLayoutStack::updateLayout(BOOL force_resize) { calcMinExtents(); // calculate current extents S32 total_width = 0; S32 total_height = 0; const F32 ANIM_OPEN_TIME = 0.02f; const F32 ANIM_CLOSE_TIME = 0.03f; e_panel_list_t::iterator panel_it; for (panel_it = mPanels.begin(); panel_it != mPanels.end(); ++panel_it) { LLPanel* panelp = (*panel_it)->mPanel; if (panelp->getVisible()) { (*panel_it)->mVisibleAmt = lerp((*panel_it)->mVisibleAmt, 1.f, LLCriticalDamp::getInterpolant(ANIM_OPEN_TIME)); if ((*panel_it)->mVisibleAmt > 0.99f) { (*panel_it)->mVisibleAmt = 1.f; } } else // not visible { (*panel_it)->mVisibleAmt = lerp((*panel_it)->mVisibleAmt, 0.f, LLCriticalDamp::getInterpolant(ANIM_CLOSE_TIME)); if ((*panel_it)->mVisibleAmt < 0.001f) { (*panel_it)->mVisibleAmt = 0.f; } } if ((*panel_it)->mCollapsed) { (*panel_it)->mCollapseAmt = lerp((*panel_it)->mCollapseAmt, 1.f, LLCriticalDamp::getInterpolant(ANIM_CLOSE_TIME)); } else { (*panel_it)->mCollapseAmt = lerp((*panel_it)->mCollapseAmt, 0.f, LLCriticalDamp::getInterpolant(ANIM_CLOSE_TIME)); } if (mOrientation == HORIZONTAL) { // enforce minimize size constraint by default if (panelp->getRect().getWidth() < (*panel_it)->mMinWidth) { panelp->reshape((*panel_it)->mMinWidth, panelp->getRect().getHeight()); } total_width += llround(panelp->getRect().getWidth() * (*panel_it)->getCollapseFactor()); // want n-1 panel gaps for n panels if (panel_it != mPanels.begin()) { total_width += mPanelSpacing; } } else //VERTICAL { // enforce minimize size constraint by default if (panelp->getRect().getHeight() < (*panel_it)->mMinHeight) { panelp->reshape(panelp->getRect().getWidth(), (*panel_it)->mMinHeight); } total_height += llround(panelp->getRect().getHeight() * (*panel_it)->getCollapseFactor()); if (panel_it != mPanels.begin()) { total_height += mPanelSpacing; } } } S32 num_resizable_panels = 0; S32 shrink_headroom_available = 0; S32 shrink_headroom_total = 0; for (panel_it = mPanels.begin(); panel_it != mPanels.end(); ++panel_it) { // panels that are not fully visible do not count towards shrink headroom if ((*panel_it)->getCollapseFactor() < 1.f) { continue; } // if currently resizing a panel or the panel is flagged as not automatically resizing // only track total available headroom, but don't use it for automatic resize logic if ((*panel_it)->mResizeBar->hasMouseCapture() || (!(*panel_it)->mAutoResize && !force_resize)) { if (mOrientation == HORIZONTAL) { shrink_headroom_total += (*panel_it)->mPanel->getRect().getWidth() - (*panel_it)->mMinWidth; } else //VERTICAL { shrink_headroom_total += (*panel_it)->mPanel->getRect().getHeight() - (*panel_it)->mMinHeight; } } else { num_resizable_panels++; if (mOrientation == HORIZONTAL) { shrink_headroom_available += (*panel_it)->mPanel->getRect().getWidth() - (*panel_it)->mMinWidth; shrink_headroom_total += (*panel_it)->mPanel->getRect().getWidth() - (*panel_it)->mMinWidth; } else //VERTICAL { shrink_headroom_available += (*panel_it)->mPanel->getRect().getHeight() - (*panel_it)->mMinHeight; shrink_headroom_total += (*panel_it)->mPanel->getRect().getHeight() - (*panel_it)->mMinHeight; } } } // calculate how many pixels need to be distributed among layout panels // positive means panels need to grow, negative means shrink S32 pixels_to_distribute; if (mOrientation == HORIZONTAL) { pixels_to_distribute = getRect().getWidth() - total_width; } else //VERTICAL { pixels_to_distribute = getRect().getHeight() - total_height; } // now we distribute the pixels... S32 cur_x = 0; S32 cur_y = getRect().getHeight(); for (panel_it = mPanels.begin(); panel_it != mPanels.end(); ++panel_it) { LLPanel* panelp = (*panel_it)->mPanel; S32 cur_width = panelp->getRect().getWidth(); S32 cur_height = panelp->getRect().getHeight(); S32 new_width = llmax((*panel_it)->mMinWidth, cur_width); S32 new_height = llmax((*panel_it)->mMinHeight, cur_height); S32 delta_size = 0; // if panel can automatically resize (not animating, and resize flag set)... if ((*panel_it)->getCollapseFactor() == 1.f && (force_resize || (*panel_it)->mAutoResize) && !(*panel_it)->mResizeBar->hasMouseCapture()) { if (mOrientation == HORIZONTAL) { // if we're shrinking if (pixels_to_distribute < 0) { // shrink proportionally to amount over minimum // so we can do this in one pass delta_size = (shrink_headroom_available > 0) ? llround((F32)pixels_to_distribute * ((F32)(cur_width - (*panel_it)->mMinWidth) / (F32)shrink_headroom_available)) : 0; shrink_headroom_available -= (cur_width - (*panel_it)->mMinWidth); } else { // grow all elements equally delta_size = llround((F32)pixels_to_distribute / (F32)num_resizable_panels); num_resizable_panels--; } pixels_to_distribute -= delta_size; new_width = llmax((*panel_it)->mMinWidth, cur_width + delta_size); } else { new_width = getDefaultWidth(new_width); } if (mOrientation == VERTICAL) { if (pixels_to_distribute < 0) { // shrink proportionally to amount over minimum // so we can do this in one pass delta_size = (shrink_headroom_available > 0) ? llround((F32)pixels_to_distribute * ((F32)(cur_height - (*panel_it)->mMinHeight) / (F32)shrink_headroom_available)) : 0; shrink_headroom_available -= (cur_height - (*panel_it)->mMinHeight); } else { delta_size = llround((F32)pixels_to_distribute / (F32)num_resizable_panels); num_resizable_panels--; } pixels_to_distribute -= delta_size; new_height = llmax((*panel_it)->mMinHeight, cur_height + delta_size); } else { new_height = getDefaultHeight(new_height); } } else { if (mOrientation == HORIZONTAL) { new_height = getDefaultHeight(new_height); } else // VERTICAL { new_width = getDefaultWidth(new_width); } } // adjust running headroom count based on new sizes shrink_headroom_total += delta_size; panelp->reshape(new_width, new_height); panelp->setOrigin(cur_x, cur_y - new_height); LLRect panel_rect = panelp->getRect(); LLRect resize_bar_rect = panel_rect; if (mOrientation == HORIZONTAL) { resize_bar_rect.mLeft = panel_rect.mRight - RESIZE_BAR_OVERLAP; resize_bar_rect.mRight = panel_rect.mRight + mPanelSpacing + RESIZE_BAR_OVERLAP; } else { resize_bar_rect.mTop = panel_rect.mBottom + RESIZE_BAR_OVERLAP; resize_bar_rect.mBottom = panel_rect.mBottom - mPanelSpacing - RESIZE_BAR_OVERLAP; } (*panel_it)->mResizeBar->setRect(resize_bar_rect); if (mOrientation == HORIZONTAL) { cur_x += llround(new_width * (*panel_it)->getCollapseFactor()) + mPanelSpacing; } else //VERTICAL { cur_y -= llround(new_height * (*panel_it)->getCollapseFactor()) + mPanelSpacing; } } // update resize bars with new limits LLResizeBar* last_resize_bar = NULL; for (panel_it = mPanels.begin(); panel_it != mPanels.end(); ++panel_it) { LLPanel* panelp = (*panel_it)->mPanel; if (mOrientation == HORIZONTAL) { (*panel_it)->mResizeBar->setResizeLimits( (*panel_it)->mMinWidth, (*panel_it)->mMinWidth + shrink_headroom_total); } else //VERTICAL { (*panel_it)->mResizeBar->setResizeLimits( (*panel_it)->mMinHeight, (*panel_it)->mMinHeight + shrink_headroom_total); } // toggle resize bars based on panel visibility, resizability, etc BOOL resize_bar_enabled = panelp->getVisible() && (*panel_it)->mUserResize; (*panel_it)->mResizeBar->setVisible(resize_bar_enabled); if (resize_bar_enabled) { last_resize_bar = (*panel_it)->mResizeBar; } } // hide last resize bar as there is nothing past it // resize bars need to be in between two resizable panels if (last_resize_bar) { last_resize_bar->setVisible(FALSE); } // not enough room to fit existing contents if (force_resize == FALSE // layout did not complete by reaching target position && ((mOrientation == VERTICAL && cur_y != -mPanelSpacing) || (mOrientation == HORIZONTAL && cur_x != getRect().getWidth() + mPanelSpacing))) { // do another layout pass with all stacked elements contributing // even those that don't usually resize llassert_always(force_resize == FALSE); updateLayout(TRUE); } } // end LLLayoutStack::updateLayout LLLayoutPanel* LLLayoutStack::findEmbeddedPanel(LLPanel* panelp) const { e_panel_list_t::const_iterator panel_it; for (panel_it = mPanels.begin(); panel_it != mPanels.end(); ++panel_it) { if ((*panel_it)->mPanel == panelp) { return *panel_it; } } return NULL; } void LLLayoutStack::calcMinExtents() { mMinWidth = 0; mMinHeight = 0; e_panel_list_t::iterator panel_it; for (panel_it = mPanels.begin(); panel_it != mPanels.end(); ++panel_it) { if (mOrientation == HORIZONTAL) { mMinHeight = llmax( mMinHeight, (*panel_it)->mMinHeight); mMinWidth += (*panel_it)->mMinWidth; if (panel_it != mPanels.begin()) { mMinWidth += mPanelSpacing; } } else //VERTICAL { mMinWidth = llmax( mMinWidth, (*panel_it)->mMinWidth); mMinHeight += (*panel_it)->mMinHeight; if (panel_it != mPanels.begin()) { mMinHeight += mPanelSpacing; } } } }