/** * @file piemenu.cpp * @brief Pie menu class * * $LicenseInfo:firstyear=2001&license=viewerlgpl$ * Second Life Viewer Source Code * Copyright (C) 2010, Linden Research, Inc. * Copyright (C) 2011, Zi Ree @ Second Life * * 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$ */ #include "llviewerprecompiledheaders.h" #include "piemenu.h" #include "pieslice.h" #include "pieseparator.h" #include "llviewercontrol.h" #include "llviewerwindow.h" #define PIE_POPUP_EFFECT 1 // debug #define PIE_DRAW_BOUNDING_BOX 0 // debug // register the pie menu globally as child widget static LLDefaultChildRegistry::Register r1("pie_menu"); // register all possible child widgets a pie menu can have static PieChildRegistry::Register pie_r1("pie_menu"); static PieChildRegistry::Register pie_r2("pie_slice"); static PieChildRegistry::Register pie_r3("pie_separator"); // pie slice label text positioning constexpr S32 PIE_X[] = {64, 45, 0, -45, -63, -45, 0, 45}; constexpr S32 PIE_Y[] = { 0, 44, 73, 44, 0, -44, -73, -44}; constexpr S32 PIE_INNER_SIZE = 20; // radius of the inner pie circle constexpr F32 PIE_POPUP_FACTOR = 1.7f; // pie menu size factor on popup constexpr F32 PIE_POPUP_TIME = 0.25f; // time to shrink from popup size to regular size constexpr S32 PIE_OUTER_SIZE = 96; // radius of the outer pie circle constexpr F32 PIE_OUTER_SHADE_FACTOR = 1.09f; // size factor of the outer shading ring constexpr F32 PIE_SLICE_DIVIDER_WIDTH = 0.04f; // width of a slice divider in radians constexpr F32 PIE_MAX_SLICES_F = F32(PIE_MAX_SLICES); PieMenu::PieMenu(const Params& p) : LLMenuGL(p), PieAutohide(p.autohide, p.start_autohide), mCurrentSegment(-1), mOldSlice(nullptr), mSlice(nullptr), mFirstClick(true) { LL_DEBUGS("Pie") << "PieMenu::PieMenu()" << LL_ENDL; // radius, so we need this *2 reshape(PIE_OUTER_SIZE * 2, PIE_OUTER_SIZE * 2, false); // set up the font for the menu mFont = LLFontGL::getFont(LLFontDescriptor("SansSerif", "Pie", LLFontGL::NORMAL)); if (!mFont) { LL_WARNS("Pie") << "Could not find font size for Pie menu, falling back to Small font." << LL_ENDL; mFont = LLFontGL::getFont(LLFontDescriptor("SansSerif", "Small", LLFontGL::NORMAL)); } // set slices pointer to our own slices mSlices = &mMySlices; } bool PieMenu::addChild(LLView* child, S32 tab_group) { // don't add invalid slices if (!child) { return false; } // add a new slice to the menu mSlices->emplace_back(child); // tell the view that our menu has changed and reshape it back to correct size LLUICtrl::addChild(child); reshape(PIE_OUTER_SIZE * 2, PIE_OUTER_SIZE * 2, false); return true; } void PieMenu::removeChild(LLView* child) { // remove a slice from the menu slice_list_t::iterator found_it = std::find(mSlices->begin(), mSlices->end(), child); if (found_it != mSlices->end()) { mSlices->erase(found_it); } // tell the view that our menu has changed and reshape it back to correct size LLUICtrl::removeChild(child); reshape(PIE_OUTER_SIZE * 2, PIE_OUTER_SIZE * 2, false); } bool PieMenu::handleHover(S32 x, S32 y, MASK mask) { // initialize pie scale factor for popup effect F32 factor = getScaleFactor(); // initially, the current segment is marked as invalid mCurrentSegment = -1; // move mouse coordinates to be relative to the pie center LLVector2 mouseVector((F32)(x - PIE_OUTER_SIZE), (F32)(y - PIE_OUTER_SIZE)); // get the distance from the center point F32 distance = mouseVector.length(); // check if our mouse pointer is within the pie slice area if (distance > PIE_INNER_SIZE && (distance < (PIE_OUTER_SIZE * factor) || mFirstClick)) { // get the angle of the mouse pointer from the center in radians F32 angle = acos(mouseVector.mV[VX] / distance); // if the mouse is below the middle of the pie, reverse the angle if (mouseVector.mV[VY] < 0.f) { angle = F_PI * 2.f - angle; } // rotate the angle slightly so the slices' centers are aligned correctly angle += F_PI / PIE_MAX_SLICES_F; // calculate slice number from the angle mCurrentSegment = (S32) (PIE_MAX_SLICES_F * angle / (F_PI * 2.f)) % PIE_MAX_SLICES; } return true; } void PieMenu::show(S32 x, S32 y, LLView* spawning_view) { // if the menu is already there, do nothing if (getVisible()) { return; } mCurrentSegment = -1; // play a sound make_ui_sound("UISndPieMenuAppear"); LL_DEBUGS("Pie") << "PieMenu::show(): " << x << " " << y << LL_ENDL; // make sure the menu is always the correct size reshape(PIE_OUTER_SIZE * 2, PIE_OUTER_SIZE * 2, false); // get the 3D view rectangle LLRect screen = LLMenuGL::sMenuContainer->getMenuRect(); // check if the pie menu is out of bounds and move it accordingly if (x - PIE_OUTER_SIZE < 0) { x = PIE_OUTER_SIZE; } else if (x + PIE_OUTER_SIZE > screen.getWidth()) { x = screen.getWidth() - PIE_OUTER_SIZE; } if (y - PIE_OUTER_SIZE < screen.mBottom) { y = PIE_OUTER_SIZE + screen.mBottom; } else if (y + PIE_OUTER_SIZE - screen.mBottom > screen.getHeight()) { y = screen.getHeight() - PIE_OUTER_SIZE + screen.mBottom; } // move the mouse pointer into the center of the menu LLUI::getInstance()->setMousePositionLocal(getParent(), x, y); // set our drawing origin to the center of the menu setOrigin(x - PIE_OUTER_SIZE, y - PIE_OUTER_SIZE); // grab mouse control gFocusMgr.setMouseCapture(this); // this was the first click for the menu mFirstClick = true; // set up the slices pointer to the menu's own slices mSlices = &mMySlices; // reset enable update checks for slices for (auto slice : *mSlices) { if (auto resetSlice = dynamic_cast(slice); resetSlice) { resetSlice->resetUpdateEnabledCheck(); } } // cleanup mSlice = nullptr; mOldSlice = nullptr; // draw the menu on screen setVisible(true); LLView::setVisible(true); } void PieMenu::hide() { // if the menu is already hidden, do nothing if (!getVisible()) { return; } // make a sound when hiding make_ui_sound("UISndPieMenuHide"); LL_DEBUGS("Pie") << "Clearing selections" << LL_ENDL; mCurrentSegment = -1; mSlices = &mMySlices; #if PIE_POPUP_EFFECT // safety in case the timer was still running mPopupTimer.stop(); #endif LLView::setVisible(false); } void PieMenu::setVisible(bool visible) { // hide the menu if needed if (!visible) { hide(); // clear all menus and temporary selections sMenuContainer->hideMenus(); } } void PieMenu::draw() { // save the current OpenGL drawing matrix so we can freely modify it gGL.pushMatrix(); // save the widget's rectangle for later use const LLRect& r = getRect(); // initialize pie scale factor for popup effect F32 factor = getScaleFactor(); #if PIE_DRAW_BOUNDING_BOX // draw a bounding box around the menu for debugging purposes gl_rect_2d(0, r.getHeight(), r.getWidth(), 0, LLColor4::white, false); #endif LLUIColorTable& colortable = LLUIColorTable::instance(); // set up pie menu colors LLColor4 lineColor = colortable.getColor("PieMenuLineColor"); LLColor4 selectedColor = colortable.getColor("PieMenuSelectedColor"); LLColor4 textColor = colortable.getColor("PieMenuTextColor"); LLColor4 bgColor = colortable.getColor("PieMenuBgColor"); LLColor4 borderColor = bgColor % 0.3f; // if the user wants their own colors, apply them here static LLCachedControl sOverridePieColors(gSavedSettings, "OverridePieColors", false); if (sOverridePieColors) { static LLCachedControl sPieMenuOpacity(gSavedSettings, "PieMenuOpacity"); static LLCachedControl sPieMenuFade(gSavedSettings, "PieMenuFade"); bgColor = colortable.getColor("PieMenuBgColorOverride") % sPieMenuOpacity; borderColor = bgColor % (1.f - sPieMenuFade); selectedColor = colortable.getColor("PieMenuSelectedColorOverride"); } static LLCachedControl sPieMenuPopupFontEffect(gSavedSettings, "PieMenuPopupFontEffect"); static LLCachedControl sPieMenuOuterRingShade(gSavedSettings, "PieMenuOuterRingShade"); // on first click, make the menu fade out to indicate "borderless" operation if (mFirstClick) { borderColor %= 0.f; } constexpr S32 steps = 100; // move origin point to the center of our rectangle LLUI::instance().translate(r.getWidth() / 2.f, r.getHeight() / 2.f); // draw the general pie background gl_washer_2d(PIE_OUTER_SIZE * factor, PIE_INNER_SIZE, steps, bgColor, borderColor); // set up an item list iterator to point at the beginning of the item list slice_list_t::iterator cur_item_iter{ mSlices->begin() }; // clear current slice pointer mSlice = nullptr; // current slice number is 0 S32 num{ 0 }; bool wasAutohide{ false }; do { // standard item text color LLColor4 itemColor = textColor; // clear the label and set up the starting angle to draw in std::string label{ "" }; F32 segmentStart = F_PI / (PIE_MAX_SLICES_F / 2.f) * (F32)num - F_PI / PIE_MAX_SLICES_F; // iterate through the list of slices if (cur_item_iter != mSlices->end()) { // get current slice item LLView* item = (*cur_item_iter); cur_item_iter++; bool isSliceOrSubmenu{ false }; auto checkAutohide = [&](PieAutohide* autohideSlice) { // if the current slice is the start of an autohide chain, clear out previous chains if (autohideSlice->getStartAutohide()) { wasAutohide = false; } // check if the current slice is part of an autohide chain if (autohideSlice->getAutohide()) { // if the previous item already won the autohide, skip this item if (wasAutohide) { return true; } // look at the next item in the pie LLView* lookAhead = (*cur_item_iter); // check if this is a normal click slice if (PieSlice* lookSlice = dynamic_cast(lookAhead)) { // if the next item is part of the current autohide chain as well ... if (lookSlice->getAutohide() && !lookSlice->getStartAutohide()) { // ... it's visible and it's enabled, skip the current one. // the first visible and enabled item in autohide chains wins // this is useful for Sit/Stand toggles lookSlice->updateEnabled(); lookSlice->updateVisible(); if (lookSlice->getVisible() && lookSlice->getEnabled()) { return true; } // this item won the autohide contest wasAutohide = true; } } else if (PieMenu* lookSlice = dynamic_cast(lookAhead)) { if (lookSlice->getAutohide() && !lookSlice->getStartAutohide()) { if (/*lookSlice->getVisible() &&*/ lookSlice->getEnabled()) // Menu is somehow always set to not visible... { return true; } // this item won the autohide contest wasAutohide = true; } } } else { // reset autohide chain wasAutohide = false; } return false; }; // in case it is regular click slice if (PieSlice* currentSlice = dynamic_cast(item)) { isSliceOrSubmenu = true; // get the slice label and tell the slice to check if it's supposed to be visible label = currentSlice->getLabel(); currentSlice->updateVisible(); // disable it if it's not visible, pie slices never really disappear bool slice_visible = currentSlice->getVisible(); currentSlice->setEnabled(slice_visible); if (!slice_visible) { //LL_DEBUGS("Pie") << label << " is not visible" << LL_ENDL; label = ""; } if (checkAutohide(currentSlice)) continue; // check if the slice is currently enabled currentSlice->updateEnabled(); if (!currentSlice->getEnabled()) { //LL_DEBUGS("Pie") << label << " is disabled" << LL_ENDL; // fade the item color alpha to mark the item as disabled itemColor %= 0.3f; } } // if it's a submenu else if (PieMenu* currentSubmenu = dynamic_cast(item)) { isSliceOrSubmenu = true; if (checkAutohide(currentSubmenu)) continue; label = currentSubmenu->getLabel(); if (sPieMenuOuterRingShade) { gl_washer_segment_2d(PIE_OUTER_SIZE * PIE_OUTER_SHADE_FACTOR * factor, PIE_OUTER_SIZE * factor, segmentStart + PIE_SLICE_DIVIDER_WIDTH / 2.f, segmentStart + F_PI / (PIE_MAX_SLICES_F / 2.f) - PIE_SLICE_DIVIDER_WIDTH / 2.f, steps / 8, selectedColor, selectedColor); } } // if it's a slice or submenu, the mouse pointer is over the same segment as our counter and the item is enabled if (isSliceOrSubmenu && (mCurrentSegment == num) && item->getEnabled()) { // memorize the currently highlighted slice for later mSlice = item; // if we highlighted a different slice than before, we must play a sound if (mOldSlice != mSlice) { // get the appropriate UI sound and play it std::string soundName = llformat("UISndPieMenuSliceHighlight%d", num); make_ui_sound(soundName.c_str()); // remember which slice we highlighted last, so we only play the sound once mOldSlice = mSlice; } // draw the currently highlighted pie slice gl_washer_segment_2d(PIE_OUTER_SIZE * factor, PIE_INNER_SIZE, segmentStart + PIE_SLICE_DIVIDER_WIDTH / 2.f, segmentStart + F_PI / (PIE_MAX_SLICES_F / 2.f) - PIE_SLICE_DIVIDER_WIDTH / 2.f, steps / 8, selectedColor, borderColor); } } // draw the divider line for this slice gl_washer_segment_2d(PIE_OUTER_SIZE * factor, PIE_INNER_SIZE, segmentStart - PIE_SLICE_DIVIDER_WIDTH / 2.f, segmentStart + PIE_SLICE_DIVIDER_WIDTH / 2.f, steps / 8, lineColor, borderColor); // draw the slice labels around the center mFont->renderUTF8(label, 0, PIE_X[num] * (sPieMenuPopupFontEffect() ? factor : 1.f), PIE_Y[num] * (sPieMenuPopupFontEffect() ? factor : 1.f), itemColor, LLFontGL::HCENTER, LLFontGL::VCENTER, LLFontGL::NORMAL, LLFontGL::DROP_SHADOW_SOFT); // next slice num++; } while (num < PIE_MAX_SLICES); // do this until the menu is full // draw inner and outer circle, outer only if it was not the first click if (!mFirstClick) { gl_washer_2d(PIE_OUTER_SIZE * factor, PIE_OUTER_SIZE * factor - 2.f, steps, lineColor, borderColor); if (sPieMenuOuterRingShade) { gl_washer_2d(PIE_OUTER_SIZE * PIE_OUTER_SHADE_FACTOR * factor, PIE_OUTER_SIZE * factor - 2.f, steps, lineColor, borderColor); } } gl_washer_2d(PIE_INNER_SIZE + 1, PIE_INNER_SIZE - 1, steps, borderColor, lineColor); // restore OpenGL drawing matrix gGL.popMatrix(); // give control back to the UI library LLView::draw(); } bool PieMenu::appendContextSubMenu(PieMenu* menu) { LL_DEBUGS("Pie") << "PieMenu::appendContextSubMenu()" << LL_ENDL; if (!menu) { return false; } LL_DEBUGS("Pie") << "PieMenu::appendContextSubMenu() appending " << menu->getLabel() << " to " << getLabel() << LL_ENDL; // add the submenu to the list of items mSlices->push_back(menu); // tell the view that our menu has changed LLUICtrl::addChild(menu); return true; } bool PieMenu::handleMouseUp(S32 x, S32 y, MASK mask) { // left and right mouse buttons both do the same thing currently return handleMouseButtonUp(x, y, mask); } bool PieMenu::handleRightMouseUp(S32 x, S32 y, MASK mask) { // left and right mouse buttons both do the same thing currently return handleMouseButtonUp(x, y, mask); } // left and right mouse buttons both do the same thing currently bool PieMenu::handleMouseButtonUp(S32 x, S32 y, MASK mask) { // if this was the first click and no slice is highlighted (no borderless click), start the popup timer if (mFirstClick && !mSlice) { mFirstClick = false; #if PIE_POPUP_EFFECT mPopupTimer.start(); #endif } else { // default to invisible bool visible = false; // get the current selected slice and check if this is a regular click slice PieSlice* currentSlice = dynamic_cast(mSlice); if (currentSlice) { // if so, click it and make a sound make_ui_sound("UISndClickRelease"); currentSlice->onCommit(); } else { // check if this was a submenu PieMenu* currentSubmenu = dynamic_cast(mSlice); if (currentSubmenu) { // if so, remember we clicked the menu already at least once mFirstClick = false; // swap out the list of items for the ones in the submenu mSlices = ¤tSubmenu->mMySlices; // reset enable update checks for slices for (auto slice : *mSlices) { if (auto resetSlice = dynamic_cast(slice); resetSlice) { resetSlice->resetUpdateEnabledCheck(); } } // the menu stays visible visible = true; #if PIE_POPUP_EFFECT // restart the popup timer mPopupTimer.reset(); mPopupTimer.start(); #endif // make a sound make_ui_sound("UISndPieMenuAppear"); } } // show or hide the menu, as needed setVisible(visible); } // release mouse capture after the first click if we still have it grabbed if (hasMouseCapture()) { gFocusMgr.setMouseCapture(nullptr); } // give control back to the system return LLView::handleMouseUp(x, y, mask); } F32 PieMenu::getScaleFactor() { // initialize pie scale factor for popup effect F32 factor = 1.f; #if PIE_POPUP_EFFECT // set the popup size if this was the first click on the menu if (mFirstClick) { factor = PIE_POPUP_FACTOR; } // otherwise check if the popup timer is still running else if (mPopupTimer.getStarted()) { // if the timer ran past the popup time, stop the timer and set the size to 1.0 F32 elapsedTime = mPopupTimer.getElapsedTimeF32(); if (elapsedTime > PIE_POPUP_TIME) { factor = 1.f; mPopupTimer.stop(); } // otherwise calculate the size factor to make the menu shrink over time else { factor = PIE_POPUP_FACTOR - (PIE_POPUP_FACTOR - 1.f) * elapsedTime / PIE_POPUP_TIME; } } #endif return factor; }