diff --git a/indra/llui/llsliderctrl.cpp b/indra/llui/llsliderctrl.cpp index 66e97f093f..ca4e6eae48 100644 --- a/indra/llui/llsliderctrl.cpp +++ b/indra/llui/llsliderctrl.cpp @@ -302,7 +302,16 @@ void LLSliderCtrl::updateSliderRect() } if (mTextBox) { - right -= mTextBox->getRect().getWidth() + sliderctrl_spacing; + // Keep the value text pinned to the right edge, the same way the + // editable variant above is; without this a resized slider leaves + // the text floating at its construction-time offset mid-track. + LLRect text_rect = mTextBox->getRect(); + const S32 text_width = text_rect.getWidth(); + text_rect.mRight = right; + text_rect.mLeft = right - text_width; + mTextBox->setRect(text_rect); + + right -= text_width + sliderctrl_spacing; } if (mLabelBox) { diff --git a/indra/newview/CMakeLists.txt b/indra/newview/CMakeLists.txt index 3a81cdf207..d574689061 100644 --- a/indra/newview/CMakeLists.txt +++ b/indra/newview/CMakeLists.txt @@ -170,9 +170,13 @@ set(viewer_SOURCE_FILES alfloaterprofilelegacy.cpp alfloaterprogressview.cpp alfloaterregiontracker.cpp + alfloatersceneexplorer.cpp + alfloatersceneexplorerfilters.cpp alfloatertransactionlog.cpp + alsceneexplorerpredicate.cpp alfloaterwebprofile.cpp allegacynotificationwellwindow.cpp + alobjectproperties.cpp alpanelaomini.cpp alpanelaopulldown.cpp alpanelavatarlegacy.cpp @@ -195,6 +199,7 @@ set(viewer_SOURCE_FILES alpanelsearchweb.cpp alpanelstreaminfo.cpp alpickitem.cpp + alsceneexplorermodel.cpp alstreaminfo.cpp altoolalign.cpp alunzip.cpp @@ -932,9 +937,13 @@ set(viewer_HEADER_FILES alfloaterprofilelegacy.h alfloaterprogressview.h alfloaterregiontracker.h + alfloatersceneexplorer.h + alfloatersceneexplorerfilters.h alfloatertransactionlog.h + alsceneexplorerpredicate.h alfloaterwebprofile.h allegacynotificationwellwindow.h + alobjectproperties.h alpanelaomini.h alpanelaopulldown.h alpanelavatarlegacy.h @@ -957,6 +966,7 @@ set(viewer_HEADER_FILES alpanelsearchweb.h alpanelstreaminfo.h alpickitem.h + alsceneexplorermodel.h alstreaminfo.h altoolalign.h alunzip.h @@ -2351,6 +2361,7 @@ if (BUILD_TESTING) # This creates a separate test project per file listed. SET(viewer_TEST_SOURCE_FILES + alsceneexplorerpredicate.cpp llagentaccess.cpp lldateutil.cpp # llmediadataclient.cpp diff --git a/indra/newview/alderenderlist.h b/indra/newview/alderenderlist.h index 9ce823ee04..16f228335c 100644 --- a/indra/newview/alderenderlist.h +++ b/indra/newview/alderenderlist.h @@ -151,6 +151,7 @@ class ALDerenderList : public LLSingleton using entry_list_t = std::list>; const entry_list_t& getEntries() const { return m_Entries; } + bool isDerendered(const LLUUID& idObject) { return getObjectEntry(idObject) != nullptr; } void removeObject(ALDerenderEntry::EEntryType eType, const LLUUID& idObject); void removeObjects(ALDerenderEntry::EEntryType eType, const uuid_vec_t& idsObject); protected: @@ -170,7 +171,6 @@ class ALDerenderList : public LLSingleton protected: ALDerenderObject* getObjectEntry(const LLUUID& idObject) /*const*/; ALDerenderObject* getObjectEntry(U64 idRegion, const LLUUID& idObject, U32 idRootLocal) /*const*/; - bool isDerendered(const LLUUID& idObject) /*const*/ { return getObjectEntry(idObject) != nullptr; } bool isDerendered(U64 idRegion, const LLUUID& idObject, U32 idRootLocal) /*const*/ { return getObjectEntry(idRegion, idObject, idRootLocal) != nullptr; } /* diff --git a/indra/newview/alfloatersceneexplorer.cpp b/indra/newview/alfloatersceneexplorer.cpp new file mode 100644 index 0000000000..edc1e63f59 --- /dev/null +++ b/indra/newview/alfloatersceneexplorer.cpp @@ -0,0 +1,3399 @@ +/** + * @file alfloatersceneexplorer.cpp + * @brief Scene Explorer floater: a filterable scene-graph tree of region content + * + * Copyright (c) 2026, Rye Mutt + * + * The source code in this file is provided to you under the terms of the + * GNU Lesser General Public License, version 2.1, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. Terms of the LGPL can be found in doc/LGPL-licence.txt + * in this distribution, or online at http://www.gnu.org/licenses/lgpl-2.1.txt + * + */ + +#include "llviewerprecompiledheaders.h" + +#include "alfloatersceneexplorer.h" + +#include "message.h" + +#include "llcallbacklist.h" +#include "lltimer.h" + +#include "llbutton.h" +#include "llcheckboxctrl.h" +#include "llclipboard.h" +#include "llcombobox.h" +#include "llfiltereditor.h" +#include "llfolderview.h" +#include "llfolderviewitem.h" +#include "lllayoutstack.h" +#include "llmenubutton.h" +#include "llmenugl.h" +#include "llnotificationsutil.h" +#include "llscrollcontainer.h" +#include "llscrolllistctrl.h" +#include "lltextbox.h" +#include "lltoggleablemenu.h" +#include "lltrans.h" +#include "llui.h" +#include "lluicolortable.h" +#include "lluictrlfactory.h" + +#include "lldate.h" +#include "llinventoryfunctions.h" +#include "llinventorymodel.h" +#include "llmaterial.h" +#include "llpermissionsflags.h" +#include "lltextureentry.h" +#include "lltexturectrl.h" +#include "llviewerinventory.h" + +#include "alavataractions.h" +#include "alderenderlist.h" +#include "alfloatersceneexplorerfilters.h" +#include "alobjectproperties.h" +#include "llagent.h" +#include "llavataractions.h" +#include "llavatarname.h" +#include "llavatarnamecache.h" +#include "llcachename.h" +#include "llfloaterreg.h" +#include "llfloatertools.h" +#include "llselectmgr.h" +#include "llslurl.h" +#include "lltoolcomp.h" +#include "lltoolmgr.h" +#include "lltracker.h" +#include "llviewercontrol.h" +#include "llviewerjointattachment.h" +#include "llviewermenu.h" +#include "llviewerobjectlist.h" +#include "llviewerregion.h" +#include "llvoavatar.h" +#include "rlvactions.h" +#include "rlvcommon.h" + +namespace +{ + // Mirrors LLSelectMgr's per-packet object cap (llselectmgr.cpp). + constexpr S32 MAX_OBJECTS_PER_PACKET = 254; + + // Send a batched ObjectSelect / ObjectDeselect message by local id, mirroring + // the batching in LLSelectMgr (MAX_OBJECTS_PER_PACKET / isSendFullFast). + void sendObjectSelectionMessage(const char* message_name, const std::vector& local_ids, const LLHost& host) + { + LLMessageSystem* msg = gMessageSystem; + bool start_new_message = true; + S32 count = 0; + + for (U32 local_id : local_ids) + { + if (start_new_message) + { + msg->newMessageFast(message_name); + msg->nextBlockFast(_PREHASH_AgentData); + msg->addUUIDFast(_PREHASH_AgentID, gAgent.getID()); + msg->addUUIDFast(_PREHASH_SessionID, gAgent.getSessionID()); + start_new_message = false; + } + + msg->nextBlockFast(_PREHASH_ObjectData); + msg->addU32Fast(_PREHASH_ObjectLocalID, local_id); + ++count; + + if (msg->isSendFullFast(nullptr) || count >= MAX_OBJECTS_PER_PACKET) + { + msg->sendReliable(host); + start_new_message = true; + count = 0; + } + } + + if (!start_new_message) + { + msg->sendReliable(host); + } + } + + // Deterministic synthetic UUID for an avatar's attachment-point grouping + // folder. Name-based (MD5) so it is stable across reconciles and will not + // collide with real object ids. + LLUUID attachmentPointKey(const LLUUID& avatar_id, S32 point_index) + { + LLUUID key; + key.generate(avatar_id.asString() + ":ap:" + std::to_string(point_index)); + return key; + } + + // 1-based position of 'child' within 'parent's maintained child list, which + // is the viewer's canonical link order (LLViewerObject::addChild appends in + // arrival/link order). Returns 0 if not a direct child. + S32 linkOrderInParent(LLViewerObject* parent, const LLViewerObject* child) + { + if (!parent || !child) + return 0; + S32 idx = 1; + for (const auto& c : parent->getChildren()) + { + if (c.get() == child) + return idx; + ++idx; + } + return 0; + } + + // RLVa-aware avatar row label: anonymize when @shownames restricts this + // agent (self is never restricted). + std::string displayNameFor(const LLUUID& id, const LLAvatarName& av_name) + { + if (RlvActions::isRlvEnabled() && id != gAgentID + && !RlvActions::canShowName(RlvActions::SNC_DEFAULT, id)) + { + return RlvStrings::getAnonym(av_name); + } + return av_name.getCompleteName(); + } + + // Empty when the name cache hasn't resolved this avatar yet. + std::string avatarDisplayName(const LLUUID& id) + { + LLAvatarName av_name; + if (!LLAvatarNameCache::get(id, &av_name)) + return std::string(); + return displayNameFor(id, av_name); + } + + // Agent / group app-SLURLs: the URL machinery renders these as resolved, + // clickable names (profile/group inspect on click) and applies RLVa + // @shownames anonymization for us. + std::string agentSlurl(const LLUUID& id) + { + return "secondlife:///app/agent/" + id.asString() + "/inspect"; + } + std::string groupSlurl(const LLUUID& id) + { + return "secondlife:///app/group/" + id.asString() + "/inspect"; + } + + // Compact modify/copy/transfer triad, e.g. "MC-". + std::string permTriad(U32 mask) + { + std::string s; + s += (mask & PERM_MODIFY) ? 'M' : '-'; + s += (mask & PERM_COPY) ? 'C' : '-'; + s += (mask & PERM_TRANSFER) ? 'T' : '-'; + return s; + } + + // Mirrors the build floater's copy-texture / Copy-Asset-UUID gating + // (llpanelface.cpp): a texture/material asset id may be shown when the + // agent could legitimately obtain it — real god powers (no admin-menu + // fakery), a library/default asset, or a full-perm item referencing the + // asset in their inventory. The inventory search is expensive, so results + // are memoized per session (positives are stable; a stale negative just + // means reopening the floater after acquiring a full-perm copy). + bool canRevealAssetId(const LLUUID& asset_id) + { + if (asset_id.isNull()) + return false; + if (gAgent.isGodlikeWithoutAdminMenuFakery()) + return true; + + static boost::unordered_map s_reveal_cache; + auto cached = s_reveal_cache.find(asset_id); + if (cached != s_reveal_cache.end()) + return cached->second; + + bool reveal = get_is_predefined_texture(asset_id); + if (!reveal) + { + LLViewerInventoryCategory::cat_array_t cats; + LLViewerInventoryItem::item_array_t items; + LLAssetIDMatches asset_id_matches(asset_id); + gInventory.collectDescendentsIf(gInventory.getRootFolderID(), cats, items, + LLInventoryModel::EXCLUDE_TRASH, asset_id_matches); + for (const auto& item : items) + { + if (item && item->getIsFullPerm()) + { + reveal = true; + break; + } + } + if (!reveal) + { + // Library assets are free for everyone. + cats.clear(); + items.clear(); + gInventory.collectDescendentsIf(gInventory.getLibraryRootFolderID(), cats, items, + LLInventoryModel::EXCLUDE_TRASH, asset_id_matches); + reveal = !items.empty(); + } + } + s_reveal_cache[asset_id] = reveal; + return reveal; + } + + // Asset id for display, or a friendly note when the user lacks the + // permissions to see it. + std::string assetIdForDisplay(const LLUUID& asset_id) + { + return canRevealAssetId(asset_id) ? asset_id.asString() + : std::string("(needs a full-perm copy to view)"); + } + + // Rough heat indicator for cost numbers: normal / caution / red. + // (LabelTextColor, not TextFgColor — the latter is for editor backgrounds + // and reads near-black on dark skins.) + const LLColor4& heatColor(F32 value, F32 caution, F32 alert) + { + static const LLUIColor normal = LLUIColorTable::instance().getColor("LabelTextColor", LLColor4::white); + static const LLUIColor warn = LLUIColorTable::instance().getColor("AlertCautionTextColor", LLColor4::yellow); + static const LLUIColor danger = LLUIColorTable::instance().getColor("Red", LLColor4::red); + if (value >= alert) + return danger.get(); + if (value >= caution) + return warn.get(); + return normal.get(); + } + + // De-emphasis that is still readable on dark skins (LabelDisabledColor is + // grey-on-grey there). + const LLColor4& mutedColor() + { + static const LLUIColor muted = LLUIColorTable::instance().getColor("LtGray", LLColor4::grey); + return muted.get(); + } + + const LLColor4& labelColor() + { + static const LLUIColor label = LLUIColorTable::instance().getColor("LabelTextColor", LLColor4::white); + return label.get(); + } + + const LLColor4& cautionColor() + { + static const LLUIColor caution = LLUIColorTable::instance().getColor("AlertCautionTextColor", LLColor4::yellow); + return caution.get(); + } + + std::string placeholderName(ALSceneExplorerItem::EItemType type, const LLUUID& id) + { + switch (type) + { + case ALSceneExplorerItem::TYPE_AVATAR: + { + std::string name = avatarDisplayName(id); + if (!name.empty()) + return name; + return std::string("(loading avatar)"); + } + case ALSceneExplorerItem::TYPE_ATTACHMENT: + return std::string("(attachment)"); + default: + return std::string("(object)"); + } + } + + // ------------------------------------------------------------------ + // Context / gear menu helpers + // ------------------------------------------------------------------ + + // Evaluate a globally registered enable predicate (the same ones the pie + // and main menus use) against the current selection, so the explorer's + // permission gating can never diverge from the rest of the viewer. + bool registryEnabled(const std::string& name) + { + const LLUICtrl::enable_callback_t* cb = LLUICtrl::EnableCallbackRegistry::getValue(name); + return cb && (*cb)(nullptr, LLSD()); + } + + // Restore the all-visible/enabled baseline LLFolderView::updateMenuOptions + // establishes before buildContextMenu, so the gear button path starts from + // the same state as the right-click popup. + void resetMenuEntries(LLMenuGL& menu) + { + for (LLView* menu_item : *menu.getChildList()) + { + if (LLMenuItemBranchGL* branch = dynamic_cast(menu_item)) + { + if (branch->getBranch()) + resetMenuEntries(*branch->getBranch()); + } + menu_item->setVisible(false); + menu_item->pushVisible(true); + menu_item->setEnabled(true); + } + } + + // Show only the listed entries (the llinventorybridge hide_context_entries + // pattern, local so the explorer doesn't drag the inventory bridge in): + // recurses into submenus, drops leading/doubled separators, disables the + // entries named in @disabled. + void hideMenuEntries(LLMenuGL& menu, + const std::vector& show, + const std::vector& disabled) + { + bool prev_was_separator = true; + for (LLView* menu_item : *menu.getChildList()) + { + if (LLMenuItemBranchGL* branch = dynamic_cast(menu_item)) + { + if (branch->getBranch()) + hideMenuEntries(*branch->getBranch(), show, disabled); + } + + const std::string& name = menu_item->getName(); + bool found = std::find(show.begin(), show.end(), name) != show.end(); + if (found) + { + const bool is_separator = dynamic_cast(menu_item) != nullptr; + found = !(is_separator && prev_was_separator); + prev_was_separator = is_separator; + } + + if (!found) + { + // Multi-selection passes call this repeatedly; don't re-hide + // an entry an earlier selected item explicitly showed. + if (!menu_item->getLastVisible()) + menu_item->setVisible(false); + menu_item->setEnabled(false); + } + else + { + menu_item->setVisible(true); + menu_item->pushVisible(true); + menu_item->setEnabled( + std::find(disabled.begin(), disabled.end(), name) == disabled.end()); + } + } + } +} + +// ============================================================================ +ALFloaterSceneExplorer::ALFloaterSceneExplorer(const LLSD& key) +: LLFloater(key) +{ + // Registered in the constructor (not postBuild) so the gear / view menu + // buttons in the floater XML can resolve these while their menus build. + // The same names serve the folder view's right-click popup. Entries the + // superset menu reuses from the global registries (Object.Touch, + // Object.Return, PayObject, Tools.TakeCopy, ...) resolve through the + // default registrar and aren't repeated here. + mCommitCallbackRegistrar.add("SceneExplorer.Focus", boost::bind(&ALFloaterSceneExplorer::doFocus, this)); + mCommitCallbackRegistrar.add("SceneExplorer.Edit", boost::bind(&ALFloaterSceneExplorer::doEdit, this)); + mCommitCallbackRegistrar.add("SceneExplorer.Inspect", boost::bind(&ALFloaterSceneExplorer::doInspect, this)); + mCommitCallbackRegistrar.add("SceneExplorer.Teleport", boost::bind(&ALFloaterSceneExplorer::doTeleport, this)); + mCommitCallbackRegistrar.add("SceneExplorer.Sit", boost::bind(&ALFloaterSceneExplorer::doSit, this)); + mCommitCallbackRegistrar.add("SceneExplorer.Copy", boost::bind(&ALFloaterSceneExplorer::doCopy, this, _2)); + mCommitCallbackRegistrar.add("SceneExplorer.CopyResults", boost::bind(&ALFloaterSceneExplorer::doCopyResults, this)); + mCommitCallbackRegistrar.add("SceneExplorer.ShowOnMap", boost::bind(&ALFloaterSceneExplorer::doShowOnMap, this)); + mCommitCallbackRegistrar.add("SceneExplorer.Beacon", boost::bind(&ALFloaterSceneExplorer::doBeacon, this)); + mCommitCallbackRegistrar.add("SceneExplorer.BlockOwner", boost::bind(&ALFloaterSceneExplorer::doBlockOwner, this)); + mCommitCallbackRegistrar.add("SceneExplorer.AvatarAction", boost::bind(&ALFloaterSceneExplorer::doAvatarAction, this, _2)); + mCommitCallbackRegistrar.add("SceneExplorer.FilterByOwner",boost::bind(&ALFloaterSceneExplorer::doFilterByOwner, this)); + mCommitCallbackRegistrar.add("SceneExplorer.Derender", boost::bind(&ALFloaterSceneExplorer::doDerender, this, _2)); + mCommitCallbackRegistrar.add("SceneExplorer.Restore", boost::bind(&ALFloaterSceneExplorer::doRestore, this)); + mCommitCallbackRegistrar.add("SceneExplorer.Refresh", boost::bind(&ALFloaterSceneExplorer::doRefresh, this)); + mCommitCallbackRegistrar.add("SceneExplorer.SetSort", boost::bind(&ALFloaterSceneExplorer::setSortMode, this, _2)); + mCommitCallbackRegistrar.add("SceneExplorer.ToggleShow", boost::bind(&ALFloaterSceneExplorer::toggleShow, this, _2)); + mCommitCallbackRegistrar.add("SceneExplorer.ResetFilters", boost::bind(&ALFloaterSceneExplorer::doResetFilters, this)); + mCommitCallbackRegistrar.add("SceneExplorer.SelectAllResults", boost::bind(&ALFloaterSceneExplorer::doSelectAllResults, this)); + mCommitCallbackRegistrar.add("SceneExplorer.ShowFilters", boost::bind(&ALFloaterSceneExplorer::doShowFilters, this)); + mEnableCallbackRegistrar.add("SceneExplorer.CheckSort", boost::bind(&ALFloaterSceneExplorer::checkSortMode, this, _2)); + mEnableCallbackRegistrar.add("SceneExplorer.CheckShow", boost::bind(&ALFloaterSceneExplorer::checkShow, this, _2)); +} + +ALFloaterSceneExplorer::~ALFloaterSceneExplorer() +{ + gIdleCallbacks.deleteFunction(onIdle, this); + if (mPropsConn.connected()) + mPropsConn.disconnect(); + if (mDerenderConn.connected()) + mDerenderConn.disconnect(); + if (mWorldSelConn.connected()) + mWorldSelConn.disconnect(); +} + +bool ALFloaterSceneExplorer::postBuild() +{ + mTreePanel = getChild("scene_tree"); + buildTree(); + + // Shown by the folder view's status text when a filter matches nothing + // (without it, a zero-hit filter renders a blank pane). + mViewModel.getFilter().setEmptyLookupMessage(getString("no_matches")); + + mShowAvatars = gSavedSettings.getBOOL("ALSceneExplorerShowAvatars"); + mShowDerendered = gSavedSettings.getBOOL("ALSceneExplorerShowDerendered"); + // The 360 interest-list mode itself is applied in onOpen / released in + // onClose, so the simulator only streams the full region while the + // explorer is actually up. + mFullRegion = gSavedSettings.getBOOL("ALSceneExplorerFullRegion"); + mSelectionSync = gSavedSettings.getBOOL("ALSceneExplorerSelectionSync"); + + // Push persisted filter state into the controls before wiring the commit + // callbacks, so the UI, the saved settings, and the filter object all + // agree from the first frame. (Sort mode and the avatar/derendered + // toggles live in the view menu now and read their members directly.) + const U32 flag_mask = gSavedSettings.getU32("ALSceneExplorerFlagFilter"); + getChild("flag_scripted")->setValue((flag_mask & ALObjectProperties::FLAG_SCRIPTED) != 0); + getChild("flag_light")->setValue((flag_mask & ALObjectProperties::FLAG_LIGHT) != 0); + getChild("flag_particles")->setValue((flag_mask & ALObjectProperties::FLAG_PARTICLES) != 0); + getChild("limit_radius_check")->setValue( + gSavedSettings.getU32("ALSceneExplorerScope") == (U32)ALSceneExplorerFilter::SCOPE_RADIUS); + getChild("radius_slider")->setValue(gSavedSettings.getF32("ALSceneExplorerRadius")); + // The "Selected owner" mode is session-only (its target id isn't + // persisted), so never restore into it. + S32 owner_idx = (S32)gSavedSettings.getU32("ALSceneExplorerOwnerFilter"); + if (owner_idx >= (S32)ALSceneExplorerFilter::OWNER_SPECIFIC) + owner_idx = 0; + getChild("owner_combo")->setCurrentByIndex(owner_idx); + getChild("search_type_combo")->setCurrentByIndex( + (S32)gSavedSettings.getU32("ALSceneExplorerSearchType")); + + getChild("filter_input")->setCommitCallback(boost::bind(&ALFloaterSceneExplorer::onFilterChanged, this)); + getChild("search_type_combo")->setCommitCallback(boost::bind(&ALFloaterSceneExplorer::onFilterChanged, this)); + getChild("owner_combo")->setCommitCallback(boost::bind(&ALFloaterSceneExplorer::onFilterChanged, this)); + getChild("limit_radius_check")->setCommitCallback(boost::bind(&ALFloaterSceneExplorer::onFilterChanged, this)); + getChild("radius_slider")->setCommitCallback(boost::bind(&ALFloaterSceneExplorer::onFilterChanged, this)); + getChild("flag_scripted")->setCommitCallback(boost::bind(&ALFloaterSceneExplorer::onFilterChanged, this)); + getChild("flag_light")->setCommitCallback(boost::bind(&ALFloaterSceneExplorer::onFilterChanged, this)); + getChild("flag_particles")->setCommitCallback(boost::bind(&ALFloaterSceneExplorer::onFilterChanged, this)); + + getChild("focus_btn")->setClickedCallback(boost::bind(&ALFloaterSceneExplorer::doFocus, this)); + getChild("edit_btn")->setClickedCallback(boost::bind(&ALFloaterSceneExplorer::doEdit, this)); + getChild("inspect_btn")->setClickedCallback(boost::bind(&ALFloaterSceneExplorer::doInspect, this)); + getChild("beacon_btn")->setClickedCallback(boost::bind(&ALFloaterSceneExplorer::doBeacon, this)); + getChild("teleport_btn")->setClickedCallback(boost::bind(&ALFloaterSceneExplorer::doTeleport, this)); + // The gear menu is the same superset menu the tree's right-click shows; + // refresh its per-row state just before it opens (mouse-down fires ahead + // of LLMenuButton::toggleMenu). + getChild("gear_btn")->setMouseDownCallback(boost::bind(&ALFloaterSceneExplorer::onGearMouseDown, this)); + + // Detail pane, received-items style: one layout panel hosts the expander + // bar + content, and collapses down to just the bar (min_dim) — the bar + // stays visible and the panel-spacing gap above it is the drag area. + mDetailHost = getChild("detail_host_layout"); + mDetailsExpanded = gSavedSettings.getBOOL("ALSceneExplorerShowDetails"); + getChild("details_btn")->setToggleState(mDetailsExpanded); + getChild("details_btn")->setCommitCallback(boost::bind(&ALFloaterSceneExplorer::onToggleDetails, this)); + getChild("main_stack")->collapsePanel(mDetailHost, !mDetailsExpanded); + + // The perms / flag checkboxes are display-only: any click is reverted by + // refilling from the model (keeps them full-brightness, unlike disabling). + static const char* const READONLY_CHECKS[] = { + "check_modify", "check_copy", "check_transfer", + "check_scripted", "check_light", "check_physics", "check_phantom", "check_temp" + }; + for (const char* check_name : READONLY_CHECKS) + { + getChild(check_name)->setCommitCallback( + boost::bind(&ALFloaterSceneExplorer::refreshDetail, this)); + } + + mPropsConn = ALObjectPropertiesCache::instance().setChangeCallback( + boost::bind(&ALFloaterSceneExplorer::onPropsCacheChanged, this, _1)); + // Track derender changes from anywhere (explorer, build menu, Blocked + // floater) so rows move between the live tree and the Derendered category. + mDerenderConn = ALDerenderList::setChangeCallback( + boost::bind(&ALFloaterSceneExplorer::onDerenderListChanged, this)); + // In-world selection -> tree highlight. The signal can fire many times a + // frame during edits, so the handler only flags; idleUpdate processes. + mWorldSelConn = LLSelectMgr::getInstance()->mUpdateSignal.connect( + boost::bind(&ALFloaterSceneExplorer::onWorldSelectionChanged, this)); + + // Restore persisted sort order, and seed the filter object from the + // controls restored above. + mViewModel.getSorter().setMode((ALSceneExplorerSort::ESortMode)gSavedSettings.getU32("ALSceneExplorerSortOrder")); + onFilterChanged(); + + // Sentinel so the first idle pass (no selection) disables the action + // buttons rather than leaving them at their XML-default enabled state. + mLastButtonStateID.generate(); + + // Drive discovery / fetch / filtering / layout from the viewer idle loop, the + // way LLInventoryPanel does, rather than from draw(). This keeps tree mutation + // out of the render pass and lets the work time-slice across frames; idleUpdate + // itself no-ops while the floater isn't visible. + gIdleCallbacks.addFunction(onIdle, this); + + return true; +} + +void ALFloaterSceneExplorer::onOpen(const LLSD& key) +{ + mReconcileTimer.reset(); + if (mFullRegion) + applyFullRegionMode(true); + reconcile(); + // Inventory muscle memory: typing starts filtering right away. + getChild("filter_input")->setFocus(true); +} + +void ALFloaterSceneExplorer::onClose(bool app_quitting) +{ + // Drop a tracker beacon the explorer set; never clobber one the user set + // through the map or a landmark. + if (mBeaconTrackedID.notNull()) + { + LLTracker::stopTracking(false); + mBeaconTrackedID.setNull(); + } + // Release the full-region stream while the explorer is closed (the + // preference itself persists; reopening re-applies it). At quit the + // simulator cleans up the interest list with the circuit. + if (!app_quitting && mFullRegion) + applyFullRegionMode(false); +} + +void ALFloaterSceneExplorer::draw() +{ + // All discovery / fetch / filter / layout now happens in idleUpdate() (driven + // by gIdleCallbacks); draw() just renders the current tree state. + LLFloater::draw(); +} + +bool ALFloaterSceneExplorer::handleKeyHere(KEY key, MASK mask) +{ + if (key == 'F' && mask == MASK_CONTROL) + { + getChild("filter_input")->setFocus(true); + return true; + } + // Enter activates the selected row the way double-click does. The folder + // view itself leaves RETURN unhandled (it only consumes it mid-rename), + // and routing it through openItem() would be wrong — folder expansion + // calls openItem() too, which is why it stays inert for folder types. + if (key == KEY_RETURN && mask == MASK_NONE && mTree && mTree->hasFocus()) + { + ALSceneExplorerItem* item = getSelectedItem(); + if (item && !item->isContainer() && !item->isDerenderedType()) + { + item->activate(); + return true; + } + } + return LLFloater::handleKeyHere(key, mask); +} + +// static +void ALFloaterSceneExplorer::onIdle(void* user_data) +{ + static_cast(user_data)->idleUpdate(); +} + +void ALFloaterSceneExplorer::idleUpdate() +{ + // No work while hidden, minimised, or closed (single-instance floaters stay + // alive when closed). An occluded-but-open floater still counts as visible, + // which is what we want. + if (!mTree || !isInVisibleChain()) + return; + + if (mReconcileTimer.getElapsedTimeF32() > 1.5f) + { + mReconcileTimer.reset(); + reconcile(); + } + + // Build newly-discovered nodes a few milliseconds at a time so a dense region + // (up to ~60k objects) streams in instead of stalling the frame. + if (!mBuildQueue.empty()) + drainBuildQueue(0.006); + + // Explicit-Refresh local re-fills, same time-sliced treatment (the full + // fillFromObject walks every face). + if (!mRefillQueue.empty()) + drainRefillQueue(0.003); + + if (mRetryTimer.getElapsedTimeF32() > 8.f) + { + mRetryTimer.reset(); + retryUnresolved(); + } + // Adaptive drain cadence: snappy while the backlog is small (a normal + // region's queue clears in seconds), halved when it is huge so the densest + // regions don't see peak probe load for minutes on end. + const F32 drain_interval = (mFetchQueue.size() > 2048) ? 1.f : 0.5f; + if (mFetchTimer.getElapsedTimeF32() > drain_interval) + { + mFetchTimer.reset(); + drainPropsQueue(); + } + + // The folder view's own idle routine: runs (time-sliced) filtering, finalises + // it, then arranges/lays out the tree in one consistent pass. + mTree->update(); + + // On the reconcile cadence, after the arrange so widget rects are fresh: + // refresh on-screen suffixes, demand costs for visible rows, and bump + // their pending props requests to the front of the queue. + if (mScanVisible) + { + mScanVisible = false; + scanVisibleRows(); + } + updateStatusText(); + + syncSelectionToWorld(); + if (mWorldSelectionDirty) + { + mWorldSelectionDirty = false; + syncSelectionFromWorld(); + } + updateActionButtons(); + + // Detail pane follows the selection; also rebuilt when props arrive or a + // reconcile pass refreshes live metrics (mDetailDirty). Skipped while the + // host is collapsed to just the expander bar. + if (mDetailHost && mDetailsExpanded) + { + ALSceneExplorerItem* detail_item = getSelectedItem(); + const LLUUID detail_id = detail_item ? detail_item->getUUID() : LLUUID::null; + if (detail_id != mLastDetailID || mDetailDirty) + { + mLastDetailID = detail_id; + mDetailDirty = false; + refreshDetail(); + } + } +} + +void ALFloaterSceneExplorer::drainBuildQueue(F64 max_time) +{ + if (mBuildQueue.empty()) + return; + LLViewerRegion* region = gAgent.getRegion(); + if (!region) + return; + + // Always make progress on at least one node, then keep going until the time + // budget for this frame is spent. + const F64 end_time = LLTimer::getTotalSeconds() + max_time; + do + { + const LLUUID id = mBuildQueue.front(); + mBuildQueue.pop_front(); + mQueued.erase(id); + + LLViewerObject* obj = gObjectList.findObject(id); + if (obj && !obj->isDead() && obj->getRegion() == region) + getOrCreateNode(obj); // creates the widget (and any missing ancestors) + } + while (!mBuildQueue.empty() && LLTimer::getTotalSeconds() < end_time); +} + +void ALFloaterSceneExplorer::drainRefillQueue(F64 max_time) +{ + if (mRefillQueue.empty()) + return; + + const F64 end_time = LLTimer::getTotalSeconds() + max_time; + do + { + const LLUUID id = mRefillQueue.front(); + mRefillQueue.pop_front(); + + auto it = mItems.find(id); + if (it == mItems.end()) + continue; + ALSceneExplorerItem* item = it->second; + LLViewerObject* obj = gObjectList.findObject(id); + if (!obj || obj->isDead()) + continue; + + // Re-fill the locally-derived fields — per-face flags (glow, + // fullbright, alpha, PBR), light/media, geometry, prim and triangle + // counts — which are captured at node build and go stale as objects + // are edited. Costs are peeked (never trigger) and the async server + // fields are left untouched by fillFromObject(). + ALObjectProperties::Record rec = item->getRecord(); + ALObjectProperties::fillFromObject(rec, obj, /*fetch_costs=*/false); + item->updateRecord(rec); // dirties this row's filter state + } + while (!mRefillQueue.empty() && LLTimer::getTotalSeconds() < end_time); +} + +// ============================================================================ +// Tree construction +// ============================================================================ +void ALFloaterSceneExplorer::buildTree() +{ + LLViewerRegion* region = gAgent.getRegion(); + const std::string region_name = region ? region->getName() : LLStringUtil::null; + + mRootItem = new ALSceneExplorerItem(ALSceneExplorerItem::TYPE_REGION, LLUUID::null, + "Region: " + region_name, mViewModel, this); + + LLFolderView::Params p(LLUICtrlFactory::getDefaultParams()); + p.name = "scene_explorer_tree"; + p.title = mRootItem->getName(); + p.rect = LLRect(0, 0, mTreePanel->getRect().getWidth(), 0); + p.parent_panel = mTreePanel; + p.tool_tip = std::string("scene_explorer_tree"); + p.listener = mRootItem; + p.view_model = &mViewModel; + p.root = nullptr; + p.use_ellipses = true; + p.options_menu = "menu_scene_explorer.xml"; + mTree = LLUICtrlFactory::create(p); + mTree->setCallbackRegistrar(&mCommitCallbackRegistrar); + mTree->setEnableRegistrar(&mEnableCallbackRegistrar); + + LLRect scroller_rect = mTreePanel->getRect(); + scroller_rect.translate(-scroller_rect.mLeft, -scroller_rect.mBottom); + LLScrollContainer::Params sp(LLUICtrlFactory::getDefaultParams()); + sp.rect(scroller_rect); + LLScrollContainer* scroller = LLUICtrlFactory::create(sp); + scroller->setFollowsAll(); + mTreePanel->addChild(scroller); + scroller->addChild(mTree); + mTree->setScrollContainer(scroller); + mTree->setFollowsAll(); + mTree->addChild(mTree->mStatusTextBox); + + mViewModel.setFolderView(mTree); + + mObjectsCategory = new ALSceneExplorerItem(ALSceneExplorerItem::TYPE_CATEGORY_OBJECTS, + LLUUID::generateNewID(), "Objects", mViewModel, this); + mRootItem->addChild(mObjectsCategory); + mObjectsWidget = createWidget(mObjectsCategory, true, mTree); + + mAvatarsCategory = new ALSceneExplorerItem(ALSceneExplorerItem::TYPE_CATEGORY_AVATARS, + LLUUID::generateNewID(), "Avatars", mViewModel, this); + mRootItem->addChild(mAvatarsCategory); + mAvatarsWidget = createWidget(mAvatarsCategory, true, mTree); + + mTree->setOpen(true); + static_cast(mObjectsWidget)->setOpen(true); +} + +LLFolderViewItem* ALFloaterSceneExplorer::createWidget(ALSceneExplorerItem* item, bool is_folder, LLFolderViewItem* parent_widget) +{ + LLFolderViewItem::Params params(LLUICtrlFactory::getDefaultParams()); + params.name = item->getName(); + params.root = mTree; + params.listener = item; + params.tool_tip = item->getName(); + params.font_color = LLUIColorTable::instance().getColor("MenuItemEnabledColor", LLColor4::white); + params.font_highlight_color = LLUIColorTable::instance().getColor("MenuItemHighlightColor", LLColor4::white); + + LLFolderViewItem* widget; + if (is_folder) + { + // Folders are our custom subclass (double-click acts on the object; + // disclosure arrow expands). We populate children eagerly (unlike + // inventory's lazy fetch), so mark them inited immediately: + // LLFolderViewFolder::arrange() gates both sort(this) and the + // folder-complete computation on mAreChildrenInited; left false, our + // folders would never sort their children and would forever report + // "incomplete" (perpetual disclosure arrow). + ALSceneExplorerFolder* folder = LLUICtrlFactory::create(params); + folder->setChildrenInited(true); + widget = folder; + } + else + { + widget = LLUICtrlFactory::create(params); + } + + if (parent_widget) + widget->addToFolder(static_cast(parent_widget)); + + return widget; +} + +ALSceneExplorerItem* ALFloaterSceneExplorer::getOrCreateNode(LLViewerObject* obj) +{ + if (!obj) + return nullptr; + + const LLUUID& id = obj->getID(); + auto found = mItems.find(id); + if (found != mItems.end()) + return found->second; + + ALSceneExplorerItem::EItemType type; + ALSceneExplorerItem* parent_item = nullptr; + LLFolderViewItem* parent_widget = nullptr; + bool is_folder = false; + S32 link_order = 0; + + if (obj->asAvatar()) + { + type = ALSceneExplorerItem::TYPE_AVATAR; + parent_item = mAvatarsCategory; + parent_widget = mAvatarsWidget; + is_folder = true; + } + else if (obj->isRootEdit()) + { + // Root of a linkset: an attachment root nests under its avatar's + // attachment-point folder; everything else is a world linkset. + if (obj->isAttachment()) + { + // getAvatarAncestor() walks the parent chain to the real wearer. + // (getAvatar() would return an animesh object's own control avatar, + // which has no attachment point for it.) + ALSceneExplorerItem* point_node = getOrCreatePointFolder(obj->getAvatarAncestor(), obj); + if (!point_node) + return nullptr; + type = ALSceneExplorerItem::TYPE_ATTACHMENT; + parent_item = point_node; + auto wit = mWidgets.find(point_node->getUUID()); + parent_widget = (wit != mWidgets.end()) ? wit->second : nullptr; + } + else + { + type = ALSceneExplorerItem::TYPE_LINKSET; + parent_item = mObjectsCategory; + parent_widget = mObjectsWidget; + } + // Every root is a folder so single- and multi-prim objects sort together + // in one list; a childless folder simply draws no disclosure arrow. + is_folder = true; + } + else + { + // Child prim of a linkset (world or attachment). + LLViewerObject* root = obj->getRootEdit(); + ALSceneExplorerItem* root_node = (root && root != obj) ? getOrCreateNode(root) : nullptr; + if (!root_node) + return nullptr; + type = ALSceneExplorerItem::TYPE_PRIM; + parent_item = root_node; + auto wit = mWidgets.find(root->getID()); + parent_widget = (wit != mWidgets.end()) ? wit->second : nullptr; + link_order = linkOrderInParent(root, obj); + } + + if (!parent_item || !parent_widget) + return nullptr; + + ALSceneExplorerItem* item = new ALSceneExplorerItem(type, id, placeholderName(type, id), mViewModel, this); + item->setLinkOrder(link_order); + // Costs stay demand-driven (visible rows / LI sort / detail pane): a bulk + // build triggering a GetObjectCost for every object would ask the server + // to cost the whole region the moment it streams in under 360 mode. + ALObjectProperties::Record rec = ALObjectProperties::fromObject(obj, /*fetch_costs=*/false); + if (LLVOAvatar* av = obj->asAvatar()) + { + // Avatar rows repurpose these fields: complexity + worn attachments. + rec.mRenderCost = (F32)av->getVisualComplexity(); + rec.mPrimCount = av->getAttachmentCount(); + } + item->updateRecord(rec); + + parent_item->addChild(item); + LLFolderViewItem* widget = createWidget(item, is_folder, parent_widget); + + mItems[id] = item; + mWidgets[id] = widget; + + // Pick up already-cached server props, otherwise request them. Avatar + // display names resolve through the name cache instead; subscribe once and + // refresh when the name lands (handle-guarded in case we close first). + applyServerProps(item); + if (type != ALSceneExplorerItem::TYPE_AVATAR) + { + queueProps(id); + } + else + { + LLAvatarName av_name; + if (!LLAvatarNameCache::get(id, &av_name)) + { + LLHandle handle = getDerivedHandle(); + LLAvatarNameCache::get(id, + [handle](const LLUUID& av_id, const LLAvatarName& name) + { + if (ALFloaterSceneExplorer* self = handle.get()) + self->onAvatarNameLoaded(av_id, name); + }); + } + } + + widget->refresh(); // populate label/suffix/icon from the model + return item; +} + +ALSceneExplorerItem* ALFloaterSceneExplorer::getOrCreatePointFolder(LLViewerObject* avatar_obj, LLViewerObject* attachment) +{ + LLVOAvatar* avatar = avatar_obj ? avatar_obj->asAvatar() : nullptr; + if (!avatar || !attachment) + return nullptr; + + // Resolve the hosting attachment point from the object's attachment state + // (getTargetAttachmentPoint also handles the ATTACHMENT_ADD mask), then + // recover its index for the synthetic folder key. + LLViewerJointAttachment* point = avatar->getTargetAttachmentPoint(attachment); + S32 point_index = 0; + if (point) + { + for (const auto& [idx, jp] : avatar->mAttachmentPoints) + { + if (jp == point) + { + point_index = idx; + break; + } + } + } + if (!point || !point_index) + return nullptr; + + // The point folder hangs off the avatar node; ensure that exists first. + ALSceneExplorerItem* av_node = getOrCreateNode(avatar_obj); + if (!av_node) + return nullptr; + auto avwit = mWidgets.find(avatar_obj->getID()); + LLFolderViewItem* av_widget = (avwit != mWidgets.end()) ? avwit->second : nullptr; + if (!av_widget) + return nullptr; + + const LLUUID key = attachmentPointKey(avatar_obj->getID(), point_index); + auto found = mItems.find(key); + if (found != mItems.end()) + return found->second; + + // Localize the joint name the same way the attach menus do ("R Forearm", + // "Skull", ... are keys in strings.xml). + std::string point_name = point->getName(); + point_name = point_name.empty() ? std::string("Attachment point") + : LLTrans::getString(point_name); + + ALSceneExplorerItem* point_node = new ALSceneExplorerItem( + ALSceneExplorerItem::TYPE_ATTACHMENT_POINT, key, point_name, mViewModel, this); + + av_node->addChild(point_node); + LLFolderViewItem* point_widget = createWidget(point_node, true, av_widget); + + mItems[key] = point_node; + mWidgets[key] = point_widget; + + point_widget->refresh(); + return point_node; +} + +void ALFloaterSceneExplorer::eraseSubtreeMaps(ALSceneExplorerItem* item) +{ + for (auto it = item->getChildrenBegin(); it != item->getChildrenEnd(); ++it) + { + eraseSubtreeMaps(static_cast(it->get())); + } + const LLUUID& id = item->getUUID(); + mWidgets.erase(id); + mItems.erase(id); +} + +void ALFloaterSceneExplorer::removeNode(ALSceneExplorerItem* item) +{ + if (!item) + return; + + const LLUUID id = item->getUUID(); + auto wit = mWidgets.find(id); + LLFolderViewItem* widget = (wit != mWidgets.end()) ? wit->second : nullptr; + + eraseSubtreeMaps(item); + + // getParent() is public on the LLFolderViewModelItem interface but protected + // in the Common base, so reach it through a base-class pointer. + LLFolderViewModelItem* base_item = item; + if (LLFolderViewModelItem* parent = const_cast(base_item->getParent())) + parent->removeChild(item); + + if (widget) + widget->destroyView(); +} + +void ALFloaterSceneExplorer::clearTree() +{ + // Wholesale teardown (used on region change): drop every object/avatar + // subtree but keep the structural root and category nodes. Collect ids + // first — removing a root frees its descendants' model items, so holding + // raw pointers across removals would dangle. + std::vector roots; + auto collect_children = [&roots](ALSceneExplorerItem* category) + { + if (!category) + return; + for (auto it = category->getChildrenBegin(); it != category->getChildrenEnd(); ++it) + { + roots.push_back(static_cast(it->get())->getUUID()); + } + }; + collect_children(mObjectsCategory); + collect_children(mAvatarsCategory); + for (const LLUUID& id : roots) + { + auto it = mItems.find(id); + if (it != mItems.end()) + removeNode(it->second); + } + + mFetchQueue.clear(); + mQueuedProps.clear(); + mBuildQueue.clear(); + mQueued.clear(); + mRefillQueue.clear(); +} + +// ============================================================================ +// Derendered category +// ============================================================================ +void ALFloaterSceneExplorer::ensureDerenderedCategory() +{ + if (mDerenderedCategory) + return; + mDerenderedCategory = new ALSceneExplorerItem(ALSceneExplorerItem::TYPE_CATEGORY_DERENDERED, + LLUUID::generateNewID(), "Derendered", mViewModel, this); + mRootItem->addChild(mDerenderedCategory); + mDerenderedWidget = createWidget(mDerenderedCategory, true, mTree); + mDerenderedWidget->refresh(); +} + +void ALFloaterSceneExplorer::destroyDerenderedCategory() +{ + if (!mDerenderedCategory) + return; + + // Remove the entry rows first (they live in mItems/mWidgets), then the + // category node + widget themselves (member-held, not in the maps). + std::vector ids; + for (auto it = mDerenderedCategory->getChildrenBegin(); it != mDerenderedCategory->getChildrenEnd(); ++it) + { + ids.push_back(static_cast(it->get())->getUUID()); + } + for (const LLUUID& id : ids) + { + auto it = mItems.find(id); + if (it != mItems.end()) + removeNode(it->second); + } + + LLFolderViewModelItem* base_item = mDerenderedCategory; + if (LLFolderViewModelItem* parent = const_cast(base_item->getParent())) + parent->removeChild(mDerenderedCategory); + if (mDerenderedWidget) + mDerenderedWidget->destroyView(); + mDerenderedCategory = nullptr; + mDerenderedWidget = nullptr; +} + +void ALFloaterSceneExplorer::onDerenderListChanged() +{ + syncDerendered(); + updateCategoryCounts(); +} + +void ALFloaterSceneExplorer::syncDerendered() +{ + if (!mTree || !mRootItem) + return; + + LLViewerRegion* region = gAgent.getRegion(); + const U64 region_handle = region ? region->getHandle() : 0; + + // Desired set: derendered objects scoped to the current region (their + // local-id bookkeeping is region-specific); derendered avatars are + // region-less and always listed. + std::vector wanted; + if (mShowDerendered) + { + for (const auto& entry : ALDerenderList::instance().getEntries()) + { + if (!entry || !entry->isValid()) + continue; + if (entry->getType() == ALDerenderEntry::TYPE_OBJECT) + { + if (static_cast(entry.get())->idRegion != region_handle) + continue; + } + else if (entry->getType() != ALDerenderEntry::TYPE_AVATAR) + { + continue; + } + wanted.push_back(entry.get()); + } + } + + if (wanted.empty()) + { + destroyDerenderedCategory(); + return; + } + ensureDerenderedCategory(); + + boost::unordered_set present; + for (const ALDerenderEntry* entry : wanted) + { + const LLUUID& id = entry->getID(); + const bool is_avatar_entry = (entry->getType() == ALDerenderEntry::TYPE_AVATAR); + + // Same anonymization rule as live avatar rows. + std::string name = entry->getName(); + if (is_avatar_entry && RlvActions::isRlvEnabled() && id != gAgentID + && !RlvActions::canShowName(RlvActions::SNC_DEFAULT, id)) + { + name = RlvStrings::getAnonym(name); + } + + auto it = mItems.find(id); + ALSceneExplorerItem* node = (it != mItems.end()) ? it->second : nullptr; + if (node && !node->isDerenderedType()) + { + // A live node still holds this id (the kill is in flight); the next + // sync pass picks the entry up once reconcile has swept it. + continue; + } + present.insert(id); + + if (!node) + { + node = new ALSceneExplorerItem( + is_avatar_entry ? ALSceneExplorerItem::TYPE_DERENDERED_AVATAR + : ALSceneExplorerItem::TYPE_DERENDERED_OBJECT, + id, name, mViewModel, this); + if (is_avatar_entry) + node->getRecordRef().mGeom = ALObjectProperties::GEOM_AVATAR; + mDerenderedCategory->addChild(node); + LLFolderViewItem* widget = createWidget(node, false, mDerenderedWidget); + mItems[id] = node; + mWidgets[id] = widget; + widget->refresh(); + } + else if (node->getName() != name && !name.empty()) + { + node->setName(name); // covers RLVa @shownames flips + if (auto wit = mWidgets.find(id); wit != mWidgets.end()) + wit->second->refresh(); + } + + // Stored position drives the distance suffix / distance sort. + if (!is_avatar_entry && region) + { + const ALDerenderObject* obj_entry = static_cast(entry); + ALObjectProperties::Record& rec = node->getRecordRef(); + rec.mPosRegion = obj_entry->posRegion; + rec.mPosGlobal = region->getPosGlobalFromRegion(obj_entry->posRegion); + rec.mDistance = (F32)(rec.mPosGlobal - gAgent.getPositionGlobal()).magVec(); + } + } + + // Prune rows whose entries were restored/removed (or filtered out). + std::vector stale; + for (auto it = mDerenderedCategory->getChildrenBegin(); it != mDerenderedCategory->getChildrenEnd(); ++it) + { + const LLUUID& id = static_cast(it->get())->getUUID(); + if (!present.count(id)) + stale.push_back(id); + } + for (const LLUUID& id : stale) + { + auto it = mItems.find(id); + if (it != mItems.end()) + removeNode(it->second); + } + if (mDerenderedCategory->getChildrenCount() == 0) + destroyDerenderedCategory(); +} + +void ALFloaterSceneExplorer::reconcile() +{ + LLViewerRegion* region = gAgent.getRegion(); + if (!region || !mTree) + return; + + // Region crossing: tear the tree down wholesale and rebuild for the new + // region — cheaper and cleaner than discovering each stale node's absence + // one-by-one, and keeps the root label current. The props cache + // deliberately persists (UUID-keyed; see ALObjectPropertiesCache), so + // objects fetched on a previous visit skip re-probing entirely. + const U64 region_handle = region->getHandle(); + if (region_handle != mLastRegionHandle) + { + mLastRegionHandle = region_handle; + clearTree(); + if (mRootItem) + mRootItem->setName("Region: " + region->getName()); + } + + // Snapshot the derender list once per pass: isDerendered() is a linear + // list walk, far too slow to call per object on a dense region. + boost::unordered_set derendered; + for (const auto& entry : ALDerenderList::instance().getEntries()) + { + if (entry && entry->getType() == ALDerenderEntry::TYPE_OBJECT) + derendered.insert(entry->getID()); + } + + boost::unordered_set present; + const S32 num = gObjectList.getNumObjects(); + present.reserve(num); + for (S32 i = 0; i < num; ++i) + { + LLViewerObject* obj = gObjectList.getObject(i); + if (!obj || obj->isDead()) + continue; + if (obj->getRegion() != region) + continue; + if (obj->isHUDAttachment()) + continue; + // Terrain (surface patches) has no server-side object properties. + if (obj->getPCode() == LLViewerObject::LL_VO_SURFACE_PATCH) + continue; + + LLVOAvatar* avatarp = obj->asAvatar(); + // Control avatars (animesh) and UI/preview avatars have no associated + // user and aren't real scene avatars. The animesh object itself still + // appears via the normal object / attachment path below. + if (avatarp && (avatarp->isControlAvatar() || avatarp->isUIAvatar())) + continue; + + const bool is_avatar = (avatarp != nullptr); + const bool is_attachment = obj->isAttachment(); + if ((is_avatar || is_attachment) && !mShowAvatars) + continue; + // RLVa: when nearby-agent presence is restricted, other avatars (and + // their attachments) must not be listed at all; self always stays. + // Re-evaluated every pass, so imposing/lifting the restriction prunes + // or restores the rows within a reconcile tick. + if ((is_avatar || is_attachment) && RlvActions::isRlvEnabled() + && !RlvActions::canShowNearbyAgents()) + { + const LLVOAvatar* wearer = is_avatar ? avatarp : obj->getAvatarAncestor(); + if (wearer && !wearer->isSelf()) + continue; + } + // Unselectable objects (water/sky/ground, no-select prims) can neither be + // fetched from the server nor edited, so leave them out entirely. + if (!is_avatar && !obj->mbCanSelect) + continue; + if (!is_avatar && !is_attachment && derendered.count(obj->getID())) + continue; + + const LLUUID& id = obj->getID(); + present.insert(id); + + auto it = mItems.find(id); + if (it == mItems.end()) + { + // Not built yet: enqueue for time-sliced creation in drainBuildQueue() + // rather than building thousands of widgets inline this frame. + if (mQueued.insert(id).second) + mBuildQueue.push_back(id); + continue; + } + + // Cheap per-pass refresh. Distance changes as things move (drives the + // distance sort + "Nm" suffix); land-impact / object cost arrive + // asynchronously after the node-build fetch, so keep PEEKING the cached + // values until they populate — peek, never getObjectCost(): the getter + // re-queues a GetObjectCost request whenever the cost is stale, and a + // failed fetch stays stale, so a triggering read here would re-request + // failed/uncosted objects every pass forever. Flags, geometry and the + // ARC render cost (a profiled per-face hotspot) were captured at build + // time and rarely change, so the full fillFromObject() is skipped to + // keep the 1.5s reconcile O(N)-cheap even on a 60k-object region. The + // radius filter is re-armed below when the agent has moved, since this + // write deliberately doesn't dirty per-item filter state. + ALSceneExplorerItem* node = it->second; + ALObjectProperties::Record& rec = node->getRecordRef(); + const LLVector3 new_pos = obj->getPositionRegion(); + // Track whether anything actually moved this pass: the position-keyed + // sorts re-arm only then, instead of unconditionally every 1.5s. + if ((new_pos - rec.mPosRegion).magVecSquared() > 0.25f) // > 0.5 m + mObjectsMoved = true; + rec.mPosRegion = new_pos; + rec.mPosGlobal = obj->getPositionGlobal(); + rec.mDistance = (F32)(rec.mPosGlobal - gAgent.getPositionGlobal()).magVec(); + rec.mObjectCost = obj->peekObjectCost(); + rec.mLandImpact = obj->peekLinksetCost(); + + // Avatar row labels depend on RLVa name restrictions, which can change + // at any time — re-derive per pass (a handful of avatars, one cache + // lookup + string compare each) so @shownames anonymizes existing rows + // promptly and lifting it restores them. Complexity / attachment count + // shift as avatars change outfits, so refresh those here too. + if (is_avatar) + { + rec.mRenderCost = (F32)avatarp->getVisualComplexity(); + rec.mPrimCount = avatarp->getAttachmentCount(); + + const std::string name = avatarDisplayName(id); + if (!name.empty() && name != node->getName()) + { + node->setName(name); + if (auto wit = mWidgets.find(id); wit != mWidgets.end()) + wit->second->refresh(); + } + } + } + + // Collect ids, not pointers: removing a linkset root frees its descendant + // model items, so a raw descendant pointer collected earlier in the pass + // would dangle. Each id is re-looked-up immediately before removal. + std::vector to_remove; + for (const auto& entry : mItems) + { + // Synthetic nodes have no backing object in the present set: + // attachment-point folders are pruned below once empty, and derendered + // rows are managed entirely by syncDerendered(). + if (entry.second->getItemType() == ALSceneExplorerItem::TYPE_ATTACHMENT_POINT + || entry.second->isDerenderedType()) + { + continue; + } + if (!present.count(entry.first)) + to_remove.push_back(entry.first); + } + for (const LLUUID& id : to_remove) + { + auto it = mItems.find(id); + if (it != mItems.end()) + removeNode(it->second); + } + + // Drop attachment-point folders that no longer hold any attachments. + std::vector empty_points; + for (const auto& entry : mItems) + { + if (entry.second->getItemType() == ALSceneExplorerItem::TYPE_ATTACHMENT_POINT + && entry.second->getChildrenCount() == 0) + { + empty_points.push_back(entry.first); + } + } + for (const LLUUID& id : empty_points) + { + auto it = mItems.find(id); + if (it != mItems.end()) + removeNode(it->second); + } + + // Reflect derender-list state (also re-checked here so region changes and + // toggle flips stay correct even without a change signal). + syncDerendered(); + updateCategoryCounts(); + // React to RLVa @shownames flips: scrub or re-resolve owner names. + auditOwnerNames(); + mDetailDirty = true; // live metrics (distance/costs) were refreshed above + mScanVisible = true; // visible-row pass runs after the next arrange + + // Self-healing 360 mode: the capture floater restores the agent mode on + // its close without knowing about us, so while our toggle is on, re-claim + // it. changeInterestListMode() no-ops when the mode is already 360, and + // crossings re-apply it through LLAgent::capabilityReceivedCallback. + if (mFullRegion && gAgent.getInterestListMode() != IL_MODE_360) + { + gAgent.changeInterestListMode(IL_MODE_360); + } + + // The per-pass distance refresh above doesn't dirty per-item filter state, + // so while a spatial scope (radius or parcel) is active, re-arm the whole + // filter once the agent has actually moved — otherwise the scope would + // keep showing the object set from wherever the filter last ran. (A + // parcel change requires movement, so this re-arm covers parcel scope.) + ALSceneExplorerFilter& filter = mViewModel.getFilter(); + if (filter.isScopeActive()) + { + const LLVector3d agent_pos = gAgent.getPositionGlobal(); + if ((agent_pos - mLastFilterAgentPos).magVec() > 1.0) + { + mLastFilterAgentPos = agent_pos; + filter.setModified(LLFolderViewFilter::FILTER_RESTART); + } + } + + // Re-sort only for the position-driven keys, and only when positions + // actually changed: agent distance when the agent has moved, either key + // when an object moved this pass (std::list::sort over tens of thousands + // of roots every 1.5s is real cost on a 360-streamed region). Static keys + // (name/land-impact/triangles/type) re-sort per-parent when nodes are + // added or change. requestSortAll() only bumps the target sort version — + // sorting actually runs inside arrange(), which nothing else re-arms on a + // quiet tree — so force a re-arrange too, or the refreshed positions + // never reorder anything. While the cursor is over the tree or a context + // menu is up, the re-sort is deferred (not dropped — the trigger state + // stays armed) so rows never jump under the mouse mid-click. + const ALSceneExplorerSort::ESortMode sort_mode = mViewModel.getSorter().getMode(); + if (sort_mode == ALSceneExplorerSort::SORT_DISTANCE + || sort_mode == ALSceneExplorerSort::SORT_REGION_ORIGIN) + { + const LLVector3d agent_pos = gAgent.getPositionGlobal(); + const bool agent_moved = (sort_mode == ALSceneExplorerSort::SORT_DISTANCE) + && (agent_pos - mLastSortAgentPos).magVec() > 1.0; + + if (agent_moved || mObjectsMoved) + { + S32 mouse_x = 0, mouse_y = 0; + LLUI::getInstance()->getMousePositionScreen(&mouse_x, &mouse_y); + const bool pointer_over_tree = + mTreePanel && mTreePanel->calcScreenRect().pointInRect(mouse_x, mouse_y); + const bool menu_open = + LLMenuGL::sMenuContainer && LLMenuGL::sMenuContainer->hasVisibleMenu(); + if (!pointer_over_tree && !menu_open) + { + mLastSortAgentPos = agent_pos; + mObjectsMoved = false; + mViewModel.requestSortAll(); + mTree->arrangeAll(); + } + } + } +} + +// ============================================================================ +// Async property fetch +// ============================================================================ +void ALFloaterSceneExplorer::queueProps(const LLUUID& id) +{ + if (id.isNull() || mQueuedProps.count(id)) + return; + ALObjectPropertiesCache& cache = ALObjectPropertiesCache::instance(); + // Already sent and awaiting the reply. + if (cache.isPending(id)) + return; + // Skip if we already have full data (creator/date) for this object; a + // family-only entry (e.g. from a hover) still warrants a full fetch. + const ALObjectPropertiesCache::ServerProps* p = cache.get(id); + if (p && p->mHasFullData) + return; + mQueuedProps.insert(id); + mFetchQueue.push_back(id); +} + +void ALFloaterSceneExplorer::drainPropsQueue() +{ + LLViewerRegion* region = gAgent.getRegion(); + if (!region || (mFetchQueue.empty() && mPriorityFetch.empty())) + return; + + ALObjectPropertiesCache& cache = ALObjectPropertiesCache::instance(); + std::vector local_ids; + local_ids.reserve(MAX_OBJECTS_PER_PACKET); + + auto take = [&](const LLUUID& id) + { + mQueuedProps.erase(id); + + // Already in flight (covers the priority lane leaving its ids in the + // main queue too). Deliberately NOT gated on cached full data: an + // explicit Refresh re-queues resolved ids to pick up renames. + if (cache.isPending(id)) + return; + + LLViewerObject* obj = gObjectList.findObject(id); + // Local ids are a per-region namespace, so never address an object + // that died or crossed into a neighbour region. + if (!obj || obj->isDead() || obj->getRegion() != region) + return; + // Never disturb the user's live selection: a raw ObjectDeselect for an + // actively edited object desyncs the simulator's selection state (the + // sim halts physical objects and streams updates only while selected). + // retryUnresolved() re-queues it once it is no longer selected. + if (obj->isSelected()) + return; + + local_ids.push_back(obj->getLocalID()); + cache.markPending(id); + }; + + // On-screen rows first — a 360-streamed region can hold a multi-minute + // backlog, and what the user is looking at shouldn't wait behind it. + while (!mPriorityFetch.empty() && local_ids.size() < (size_t)MAX_OBJECTS_PER_PACKET) + { + const LLUUID id = mPriorityFetch.front(); + mPriorityFetch.pop_front(); + mPriorityQueued.erase(id); + take(id); + } + while (!mFetchQueue.empty() && local_ids.size() < (size_t)MAX_OBJECTS_PER_PACKET) + { + const LLUUID id = mFetchQueue.front(); + mFetchQueue.pop_front(); + take(id); + } + if (local_ids.empty()) + return; + + // Bulk-select to provoke full ObjectProperties replies (which, unlike the + // family variant, include creator and creation date), then immediately + // deselect so nothing stays selected on the simulator. The replies are + // captured by ALObjectPropertiesCache, whose in-flight tracking also tells + // LLSelectMgr these node-less replies are expected (no warning). + const LLHost& host = region->getHost(); + sendObjectSelectionMessage(_PREHASH_ObjectSelect, local_ids, host); + sendObjectSelectionMessage(_PREHASH_ObjectDeselect, local_ids, host); +} + +void ALFloaterSceneExplorer::retryUnresolved() +{ + // Property replies arrive over unreliable transport and can be dropped, so + // periodically re-request objects whose full data never came back. Capped + // per object so something the sim never answers for can't generate retry + // traffic forever. + constexpr S32 MAX_PROPS_RETRIES = 8; + ALObjectPropertiesCache& cache = ALObjectPropertiesCache::instance(); + for (const auto& entry : mItems) + { + ALSceneExplorerItem* item = entry.second; + const ALSceneExplorerItem::EItemType type = item->getItemType(); + // Avatars resolve via the name cache; attachment-point folders and + // derendered rows are synthetic — none has server props to fetch. + if (type == ALSceneExplorerItem::TYPE_AVATAR + || type == ALSceneExplorerItem::TYPE_ATTACHMENT_POINT + || item->isDerenderedType() + || item->isContainer()) + { + continue; + } + // Gate on the cache's full-data flag, not the record's mPropsValid: a + // family-only entry (hover) marks the record valid but the full fetch + // (creator / creation date) still needs its retry. + const ALObjectPropertiesCache::ServerProps* p = cache.get(entry.first); + if (p && p->mHasFullData) + continue; + if (mQueuedProps.count(entry.first)) + continue; // still waiting in the queue, not lost + if (item->getPropsRetries() >= MAX_PROPS_RETRIES) + continue; + item->notePropsRetry(); + cache.clearPending(entry.first); // assume the in-flight reply was lost + queueProps(entry.first); + } +} + +void ALFloaterSceneExplorer::applyServerProps(ALSceneExplorerItem* item) +{ + // Derendered rows keep their stored entry name; cached props (from before + // the derender) must not overwrite it. + if (!item || item->isDerenderedType()) + return; + const ALObjectPropertiesCache::ServerProps* p = + ALObjectPropertiesCache::instance().get(item->getUUID()); + if (!p) + return; + + ALObjectProperties::Record rec = item->getRecord(); + rec.mName = p->mName; + rec.mDescription = p->mDescription; + rec.mOwnerId = p->mOwnerId; + rec.mGroupId = p->mGroupId; + rec.mCreatorId = p->mCreatorId; + rec.mGroupOwned = p->mGroupOwned; + rec.mCreationDate = p->mCreationDate; + rec.mSaleType = p->mSaleType; + rec.mSalePrice = p->mSalePrice; + if (p->mSaleType != 0) // LLSaleInfo::FS_NOT + rec.mFlags |= ALObjectProperties::FLAG_FOR_SALE; + else + rec.mFlags &= ~ALObjectProperties::FLAG_FOR_SALE; + rec.mPropsValid = true; + + // Adopt the server name in the same pass (avatars keep their display name + // from the name cache instead). + const std::string display_name = + (!p->mName.empty() && item->getItemType() != ALSceneExplorerItem::TYPE_AVATAR) + ? p->mName : LLStringUtil::null; + item->updateRecord(rec, display_name); + + // Fold the owner's display name into the searchable text (resolved once + // per unique owner; batched into the rows when the lookup lands). + noteOwnerFor(item); +} + +void ALFloaterSceneExplorer::noteOwnerFor(ALSceneExplorerItem* item) +{ + const ALObjectProperties::Record& rec = item->getRecord(); + if (!rec.mPropsValid) + return; + const LLUUID& owner = rec.mGroupOwned ? rec.mGroupId : rec.mOwnerId; + if (owner.isNull()) + return; + + auto found = mOwnerNames.find(owner); + if (found != mOwnerNames.end()) + { + if (!found->second.mName.empty()) + item->setOwnerName(found->second.mName); + return; + } + resolveOwnerName(owner, rec.mGroupOwned); +} + +void ALFloaterSceneExplorer::resolveOwnerName(const LLUUID& owner_id, bool group_owned) +{ + if (!mOwnerNamesPending.insert(owner_id).second) + return; // lookup already in flight + + // RLVa @shownames: a hidden agent name must not become searchable text + // (an anonym would be useless to search anyway). Recorded as empty; + // auditOwnerNames() re-resolves it if the restriction lifts. + if (!group_owned && RlvActions::isRlvEnabled() && owner_id != gAgentID + && !RlvActions::canShowName(RlvActions::SNC_DEFAULT, owner_id)) + { + mOwnerNamesPending.erase(owner_id); + mOwnerNames[owner_id] = ResolvedOwner(); + return; + } + + LLHandle handle = getDerivedHandle(); + if (group_owned) + { + gCacheName->getGroup(owner_id, + [handle](const LLUUID& id, const std::string& name, bool is_group) + { + if (ALFloaterSceneExplorer* self = handle.get()) + self->onOwnerNameResolved(id, name, true); + }); + } + else + { + LLAvatarNameCache::get(owner_id, + [handle](const LLUUID& id, const LLAvatarName& av_name) + { + if (ALFloaterSceneExplorer* self = handle.get()) + self->onOwnerNameResolved(id, av_name.getCompleteName(), false); + }); + } +} + +void ALFloaterSceneExplorer::onOwnerNameResolved(const LLUUID& owner_id, const std::string& name, bool is_group) +{ + mOwnerNamesPending.erase(owner_id); + + // The @shownames restriction may have been imposed while the lookup was + // in flight; record hidden rather than leaking the name. + std::string stored = name; + if (!is_group && RlvActions::isRlvEnabled() && owner_id != gAgentID + && !RlvActions::canShowName(RlvActions::SNC_DEFAULT, owner_id)) + { + stored.clear(); + } + ResolvedOwner& entry = mOwnerNames[owner_id]; + entry.mName = stored; + entry.mIsGroup = is_group; + if (stored.empty()) + return; + + // One batched pass folds the name into every row this owner has (search + // text + suffix); each setOwnerName dirties that row's filter state, and + // the visible-row scan refreshes on-screen suffixes within a tick. + for (const auto& item_entry : mItems) + { + ALSceneExplorerItem* item = item_entry.second; + const ALObjectProperties::Record& rec = item->getRecord(); + if (rec.mPropsValid + && (rec.mGroupOwned ? rec.mGroupId : rec.mOwnerId) == owner_id) + { + item->setOwnerName(stored); + } + } + mScanVisible = true; + + // The owner-filter combo may have been waiting on this very name. + if (owner_id == mFilterOwnerId) + updateOwnerFilterLabel(); +} + +void ALFloaterSceneExplorer::auditOwnerNames() +{ + // RLVa @shownames can flip at any time. Imposing it must SCRUB resolved + // owner names back out of the rows (suffix and searchable text both); + // lifting it re-resolves owners recorded as hidden. Group names are never + // restricted. Runs per reconcile pass — the unique-owner map is small and + // canShowName is a lookup. + for (auto& owner_entry : mOwnerNames) + { + if (owner_entry.second.mIsGroup) + continue; + const LLUUID& id = owner_entry.first; + const bool can_show = !RlvActions::isRlvEnabled() || id == gAgentID + || RlvActions::canShowName(RlvActions::SNC_DEFAULT, id); + + if (!can_show && !owner_entry.second.mName.empty()) + { + owner_entry.second.mName.clear(); + for (const auto& item_entry : mItems) + { + ALSceneExplorerItem* item = item_entry.second; + const ALObjectProperties::Record& rec = item->getRecord(); + if (rec.mPropsValid && !rec.mGroupOwned && rec.mOwnerId == id) + item->setOwnerName(LLStringUtil::null); + } + mScanVisible = true; + if (id == mFilterOwnerId) + updateOwnerFilterLabel(); + } + else if (can_show && owner_entry.second.mName.empty() + && !mOwnerNamesPending.count(id)) + { + // Hidden when first seen (or imposed mid-flight): resolve now. + resolveOwnerName(id, false); + } + } +} + +void ALFloaterSceneExplorer::onAvatarNameLoaded(const LLUUID& id, const LLAvatarName& av_name) +{ + auto it = mItems.find(id); + if (it == mItems.end()) + return; + it->second->setName(displayNameFor(id, av_name)); // RLVa-aware + auto wit = mWidgets.find(id); + if (wit != mWidgets.end()) + wit->second->refresh(); +} + +void ALFloaterSceneExplorer::onPropsCacheChanged(const LLUUID& id) +{ + auto it = mItems.find(id); + if (it == mItems.end()) + return; + applyServerProps(it->second); + + // The folder-view widget caches its label, so make it re-read the model. + // applyServerProps() already dirtied the filter via updateRecord()/setName(), + // so the next idle pass re-filters this node. + auto wit = mWidgets.find(id); + if (wit != mWidgets.end()) + wit->second->refresh(); + + if (id == mLastDetailID) + mDetailDirty = true; +} + +// ============================================================================ +// Filters / sort +// ============================================================================ +namespace +{ + // The flag bits the quick-bar checkboxes own; everything else in the + // mask belongs to the companion filters floater. + constexpr U32 QUICK_FLAG_BITS = ALObjectProperties::FLAG_SCRIPTED + | ALObjectProperties::FLAG_LIGHT | ALObjectProperties::FLAG_PARTICLES; +} + +void ALFloaterSceneExplorer::onFilterChanged() +{ + ALSceneExplorerFilter& f = mViewModel.getFilter(); + f.setFilterSubString(getChild("filter_input")->getText()); + + const S32 search_idx = llmax(0, getChild("search_type_combo")->getCurrentIndex()); + f.setSearchType((ALSceneExplorerFilter::ESearchType)search_idx); + gSavedSettings.setU32("ALSceneExplorerSearchType", (U32)search_idx); + + // "Selected owner" is only meaningful with a target id (set by the + // context menu's Filter by This Owner); picked manually it means Any. + S32 owner_idx = llmax(0, getChild("owner_combo")->getCurrentIndex()); + if (owner_idx == (S32)ALSceneExplorerFilter::OWNER_SPECIFIC && mFilterOwnerId.isNull()) + owner_idx = (S32)ALSceneExplorerFilter::OWNER_ANY; + f.setOwnerId(mFilterOwnerId); + f.setOwnerMode((ALSceneExplorerFilter::EOwnerMode)owner_idx); + + // The quick boxes own their three bits of the persisted mask; the rest + // belongs to the companion filters floater and must survive a bar commit. + U32 flags = gSavedSettings.getU32("ALSceneExplorerFlagFilter") & ~QUICK_FLAG_BITS; + if (getChild("flag_scripted")->getValue().asBoolean()) flags |= ALObjectProperties::FLAG_SCRIPTED; + if (getChild("flag_light")->getValue().asBoolean()) flags |= ALObjectProperties::FLAG_LIGHT; + if (getChild("flag_particles")->getValue().asBoolean()) flags |= ALObjectProperties::FLAG_PARTICLES; + f.setFlagMask(flags); + f.setGeomMask(gSavedSettings.getU32("ALSceneExplorerTypeFilter")); + + // The bar's Within checkbox toggles the radius scope; the parcel scope is + // only reachable from the companion floater and survives until the user + // explicitly switches away. + U32 scope = gSavedSettings.getU32("ALSceneExplorerScope"); + const bool within = getChild("limit_radius_check")->getValue().asBoolean(); + if (within) + scope = (U32)ALSceneExplorerFilter::SCOPE_RADIUS; + else if (scope == (U32)ALSceneExplorerFilter::SCOPE_RADIUS) + scope = (U32)ALSceneExplorerFilter::SCOPE_REGION; + const F32 radius = (F32)getChild("radius_slider")->getValue().asReal(); + f.setScope((ALSceneExplorerFilter::EScope)scope, radius); + + f.setMinLandImpact(gSavedSettings.getF32("ALSceneExplorerMinLandImpact")); + f.setMinTriangles(gSavedSettings.getU32("ALSceneExplorerMinTriangles")); + + // Persist the filter set (the text predicate is deliberately session-only). + gSavedSettings.setU32("ALSceneExplorerOwnerFilter", (U32)owner_idx); + gSavedSettings.setU32("ALSceneExplorerFlagFilter", flags); + gSavedSettings.setU32("ALSceneExplorerScope", scope); + gSavedSettings.setF32("ALSceneExplorerRadius", radius); + + updateOwnerFilterLabel(); + // The filter setters above bumped the filter generation, so the next idle + // pass (mTree->update()) re-filters and re-arranges. +} + +void ALFloaterSceneExplorer::refreshFilters() +{ + // Settings -> quick-bar controls, then the normal commit path (which + // re-derives the same settings — stable). The companion floater calls + // this after writing settings so both surfaces stay in agreement. + const U32 flags = gSavedSettings.getU32("ALSceneExplorerFlagFilter"); + getChild("flag_scripted")->setValue((flags & ALObjectProperties::FLAG_SCRIPTED) != 0); + getChild("flag_light")->setValue((flags & ALObjectProperties::FLAG_LIGHT) != 0); + getChild("flag_particles")->setValue((flags & ALObjectProperties::FLAG_PARTICLES) != 0); + getChild("limit_radius_check")->setValue( + gSavedSettings.getU32("ALSceneExplorerScope") == (U32)ALSceneExplorerFilter::SCOPE_RADIUS); + getChild("radius_slider")->setValue(gSavedSettings.getF32("ALSceneExplorerRadius")); + onFilterChanged(); +} + +void ALFloaterSceneExplorer::doResetFilters() +{ + getChild("filter_input")->setText(LLStringUtil::null); + getChild("owner_combo")->setCurrentByIndex(0); + mFilterOwnerId.setNull(); + gSavedSettings.setU32("ALSceneExplorerFlagFilter", 0); + gSavedSettings.setU32("ALSceneExplorerTypeFilter", 0); + gSavedSettings.setU32("ALSceneExplorerScope", 0); + gSavedSettings.getControl("ALSceneExplorerRadius")->resetToDefault(); + gSavedSettings.setF32("ALSceneExplorerMinLandImpact", 0.f); + gSavedSettings.setU32("ALSceneExplorerMinTriangles", 0); + refreshFilters(); + + // Push the cleared state into the companion floater if it is open. + if (LLFloater* filters = LLFloaterReg::findInstance("scene_explorer_filters")) + { + static_cast(filters)->refreshFromSettings(); + } +} + +void ALFloaterSceneExplorer::doShowFilters() +{ + LLFloater* filters = LLFloaterReg::showInstance("scene_explorer_filters"); + if (filters) + { + // Parent it like inventory's filter finder: follows and closes with + // the explorer. + addDependentFloater(filters); + } +} + +void ALFloaterSceneExplorer::updateOwnerFilterLabel() +{ + // Show WHO the "Selected owner" filter targets on the combo button. The + // list entry itself stays generic; the button text carries the name. + LLComboBox* combo = getChild("owner_combo"); + if (combo->getCurrentIndex() != (S32)ALSceneExplorerFilter::OWNER_SPECIFIC + || mFilterOwnerId.isNull()) + { + return; + } + std::string name; + auto found = mOwnerNames.find(mFilterOwnerId); + if (found != mOwnerNames.end() && !found->second.mName.empty()) + name = found->second.mName; + else if (mItems.count(mFilterOwnerId)) + name = mItems[mFilterOwnerId]->getName(); // avatar row labels are RLVa-anonymized + combo->setLabel(name.empty() ? std::string("Owner: (loading)") : "Owner: " + name); +} + +void ALFloaterSceneExplorer::doFilterByOwner() +{ + ALSceneExplorerItem* item = getSelectedItem(); + if (!item || item->isContainer()) + return; + + // An avatar row is its own owner; object rows use their fetched owner + // (the deeding group for group-owned objects). + LLUUID owner; + bool group_owned = false; + if (item->getItemType() == ALSceneExplorerItem::TYPE_AVATAR) + { + owner = item->getUUID(); + } + else + { + const ALObjectProperties::Record& rec = item->getRecord(); + if (rec.mPropsValid) + { + owner = rec.mGroupOwned ? rec.mGroupId : rec.mOwnerId; + group_owned = rec.mGroupOwned; + } + } + if (owner.isNull()) + return; + + mFilterOwnerId = owner; + // Make sure the combo can label itself with the owner's name. + if (!mOwnerNames.count(owner)) + resolveOwnerName(owner, group_owned); + getChild("owner_combo")->setCurrentByIndex((S32)ALSceneExplorerFilter::OWNER_SPECIFIC); + onFilterChanged(); +} + +void ALFloaterSceneExplorer::updateCategoryCounts() +{ + auto update = [](ALSceneExplorerItem* category, LLFolderViewItem* widget, const char* base) + { + if (!category) + return; + const std::string name = llformat("%s (%d)", base, (S32)category->getChildrenCount()); + if (name != category->getName()) + { + category->setName(name); + if (widget) + widget->refresh(); + } + }; + update(mObjectsCategory, mObjectsWidget, "Objects"); + update(mAvatarsCategory, mAvatarsWidget, "Avatars"); + update(mDerenderedCategory, mDerenderedWidget, "Derendered"); +} + +void ALFloaterSceneExplorer::updateActionButtons() +{ + ALSceneExplorerItem* item = getSelectedItem(); + const LLUUID id = item ? item->getUUID() : LLUUID::null; + if (id == mLastButtonStateID) + return; + mLastButtonStateID = id; + + const bool is_row = item && !item->isContainer(); + LLViewerObject* obj = is_row ? gObjectList.findObject(id) : nullptr; + const bool derendered_obj = item && item->getItemType() == ALSceneExplorerItem::TYPE_DERENDERED_OBJECT; + + getChild("focus_btn")->setEnabled(obj != nullptr); + getChild("edit_btn")->setEnabled( + obj && !obj->isAvatar() + && (!RlvActions::isRlvEnabled() || RlvActions::canEdit(obj))); + getChild("inspect_btn")->setEnabled(obj != nullptr); + // Beacon needs a position: live rows have one, derendered objects keep + // their stored one, derendered avatars don't. + getChild("beacon_btn")->setEnabled( + is_row && !item->getRecord().mPosGlobal.isExactlyZero()); + // Teleport works for live rows and for derendered objects via their + // stored position. + getChild("teleport_btn")->setEnabled(obj != nullptr || derendered_obj); + // Everything else lives in the gear / context menus. +} + +// ============================================================================ +// Detail pane +// ============================================================================ +void ALFloaterSceneExplorer::onToggleDetails() +{ + mDetailsExpanded = getChild("details_btn")->getToggleState(); + gSavedSettings.setBOOL("ALSceneExplorerShowDetails", mDetailsExpanded); + if (mDetailHost) + getChild("main_stack")->collapsePanel(mDetailHost, !mDetailsExpanded); + mDetailDirty = true; +} + +void ALFloaterSceneExplorer::refreshDetail() +{ + ALSceneExplorerItem* item = getSelectedItem(); + const bool avatar_row = item + && (item->getItemType() == ALSceneExplorerItem::TYPE_AVATAR + || item->getItemType() == ALSceneExplorerItem::TYPE_DERENDERED_AVATAR); + + // Type-specific panels: swap which one shows for a cleaner display. + getChild("detail_panel_avatar")->setVisible(avatar_row); + getChild("detail_panel_object")->setVisible(!avatar_row); + + if (avatar_row) + fillAvatarDetail(item); + else + fillObjectDetail(item); +} + +void ALFloaterSceneExplorer::fillAvatarDetail(ALSceneExplorerItem* item) +{ + getChild("detail_faces_layout")->setVisible(false); + getChild("detail_faces")->deleteAllItems(); + mLastFacesID.setNull(); + + const ALObjectProperties::Record& rec = item->getRecord(); + getChild("avatar_name")->setText(agentSlurl(item->getUUID())); + getChild("avatar_distance")->setText(llformat("%.1f m", rec.mDistance)); + + LLTextBox* complexity = getChild("avatar_complexity"); + if (rec.mRenderCost > 0.f) + { + complexity->setText(llformat("%.0f", rec.mRenderCost)); + complexity->setColor(heatColor(rec.mRenderCost, 100000.f, 250000.f)); + } + else + { + complexity->setText(std::string("-")); + complexity->setColor(mutedColor()); + } + getChild("avatar_attachments")->setText( + rec.mPrimCount > 0 ? llformat("%d worn", rec.mPrimCount) : std::string("-")); + + LLTextBox* note = getChild("avatar_note"); + if (item->isDerenderedType()) + { + note->setText(std::string("Derendered - right-click the row to Restore.")); + note->setColor(cautionColor()); + } + else + { + note->setText(std::string("Click the name to open their profile.")); + note->setColor(mutedColor()); + } +} + +void ALFloaterSceneExplorer::fillObjectDetail(ALSceneExplorerItem* item) +{ + auto show = [this](const char* name, bool visible) + { + getChild(name)->setVisible(visible); + }; + auto set_text = [this](const char* name, const std::string& value) + { + getChild(name)->setText(value); + }; + auto show_row = [&](const char* label, const char* value, bool visible) + { + show(label, visible); + show(value, visible); + }; + + LLScrollListCtrl* faces = getChild("detail_faces"); + + const bool is_row = item && !item->isContainer(); + const bool derendered = is_row && item->isDerenderedType(); + LLViewerObject* obj = is_row ? gObjectList.findObject(item->getUUID()) : nullptr; + if (obj && obj->isDead()) + obj = nullptr; + + LLTextBox* name_box = getChild("detail_name"); + LLTextBox* desc_box = getChild("detail_desc"); + + if (!is_row) + { + name_box->setText(std::string("Select an object or avatar to see its details.")); + desc_box->setVisible(false); + show_row("label_owner", "detail_owner", false); + show_row("label_creator", "detail_creator", false); + show("label_perms", false); + show("check_modify", false); + show("check_copy", false); + show("check_transfer", false); + show("detail_perms_next", false); + show_row("label_pos", "detail_where", false); + show_row("label_size", "detail_size", false); + show_row("label_geom", "detail_build", false); + show("label_costs", false); + show("label_li", false); + show("detail_li", false); + show("label_render", false); + show("detail_render", false); + show("label_phys", false); + show("detail_phys", false); + show("label_stream", false); + show("detail_stream", false); + show("label_flags", false); + show("check_scripted", false); + show("check_light", false); + show("check_physics", false); + show("check_phantom", false); + show("check_temp", false); + show("detail_flags_extra", false); + getChild("detail_faces_layout")->setVisible(false); + faces->deleteAllItems(); + mLastFacesID.setNull(); + return; + } + + const ALObjectProperties::Record& rec = item->getRecord(); + const LLUUID& id = item->getUUID(); + + name_box->setText(item->getName()); + + // Description doubles as the derendered notice. + if (derendered) + { + desc_box->setVisible(true); + desc_box->setText(std::string("Derendered - right-click the row to Restore.")); + desc_box->setColor(cautionColor()); + } + else + { + desc_box->setVisible(!rec.mDescription.empty()); + desc_box->setText(rec.mDescription); + desc_box->setColor(labelColor()); + } + + // Value + colour in one step so loading placeholders read muted and real + // values reset to full brightness. + auto set_value = [this](const char* name, const std::string& value, bool muted) + { + LLTextBox* box = getChild(name); + box->setText(value); + box->setColor(muted ? mutedColor() : labelColor()); + }; + + // Who. Rows stay visible while data is in flight (muted "loading...") + // so the form doesn't reflow as replies arrive. Hidden only for + // derendered entries, whose props are stale. + const ALObjectPropertiesCache::ServerProps* props = + derendered ? nullptr : ALObjectPropertiesCache::instance().get(id); + const bool props_full = props && props->mHasFullData; + + show_row("label_owner", "detail_owner", !derendered); + if (!derendered) + { + if (props) + set_value("detail_owner", props->mGroupOwned ? groupSlurl(props->mGroupId) + : agentSlurl(props->mOwnerId), false); + else + set_value("detail_owner", std::string("loading..."), true); + } + + show_row("label_creator", "detail_creator", !derendered); + if (!derendered) + { + if (!props_full) + { + set_value("detail_creator", std::string("loading..."), true); + } + else if (props->mCreatorId.isNull()) + { + set_value("detail_creator", std::string("-"), true); + } + else + { + std::string creator = agentSlurl(props->mCreatorId); + if (props->mCreationDate) + creator += " on " + LLDate((F64)props->mCreationDate).asString().substr(0, 10); + set_value("detail_creator", creator, false); + } + } + + show("label_perms", !derendered); + show("check_modify", !derendered); + show("check_copy", !derendered); + show("check_transfer", !derendered); + show("detail_perms_next", !derendered); + if (!derendered) + { + getChild("check_modify")->setValue(props_full && (props->mOwnerMask & PERM_MODIFY) != 0); + getChild("check_copy")->setValue(props_full && (props->mOwnerMask & PERM_COPY) != 0); + getChild("check_transfer")->setValue(props_full && (props->mOwnerMask & PERM_TRANSFER) != 0); + set_text("detail_perms_next", props_full ? "next owner " + permTriad(props->mNextOwnerMask) + : std::string("loading...")); + } + + // Where + show_row("label_pos", "detail_where", true); + if (derendered) + { + set_text("detail_where", rec.mPosRegion.isExactlyZero() + ? std::string("unknown") + : llformat("<%.0f, %.0f, %.0f> (%.0f m away)", + rec.mPosRegion.mV[VX], rec.mPosRegion.mV[VY], rec.mPosRegion.mV[VZ], + rec.mDistance)); + } + else if (obj) + { + const LLVector3 pos = obj->getPositionRegion(); + set_text("detail_where", llformat("<%.1f, %.1f, %.1f> (%.1f m away)", + pos.mV[VX], pos.mV[VY], pos.mV[VZ], rec.mDistance)); + } + else + { + set_text("detail_where", llformat("%.1f m away", rec.mDistance)); + } + + show_row("label_size", "detail_size", !derendered); + if (!derendered) + { + if (obj) + { + const LLVector3 scale = obj->getScale(); + F32 roll, pitch, yaw; + obj->getRotationRegion().getEulerAngles(&roll, &pitch, &yaw); + set_value("detail_size", llformat("%.2f x %.2f x %.2f m rot <%.0f, %.0f, %.0f>", + scale.mV[VX], scale.mV[VY], scale.mV[VZ], + roll * RAD_TO_DEG, pitch * RAD_TO_DEG, yaw * RAD_TO_DEG), + false); + } + else + { + set_value("detail_size", std::string("loading..."), true); + } + } + + std::string geom; + if (rec.mPrimCount > 1) + geom += llformat("%d prims ", rec.mPrimCount); + if (rec.mNumTriangles > 0) + geom += llformat("%u tris %u verts %d faces", rec.mNumTriangles, rec.mNumVertices, rec.mNumFaces); + show_row("label_geom", "detail_build", !derendered); + if (!derendered) + set_value("detail_build", geom.empty() ? std::string("-") : geom, geom.empty()); + + // Costs: grey metric labels with aligned, heat-colored values; a missing + // metric shows a muted dash so cells never shift. Showing the detail pane + // is explicit demand for this object's costs. + requestCostsFor(item); + const bool show_costs = !derendered; + show("label_costs", show_costs); + show("label_li", show_costs); + show("detail_li", show_costs); + show("label_render", show_costs); + show("detail_render", show_costs); + show("label_phys", show_costs); + show("detail_phys", show_costs); + show("label_stream", show_costs); + show("detail_stream", show_costs); + if (show_costs) + { + auto set_cost = [this](const char* name, const std::string& value, const LLColor4& color) + { + LLTextBox* box = getChild(name); + box->setText(value); + box->setColor(color); + }; + set_cost("detail_li", + rec.mLandImpact > 0.f ? llformat("%.0f", rec.mLandImpact) : std::string("-"), + rec.mLandImpact > 0.f ? heatColor(rec.mLandImpact, 50.f, 200.f) : mutedColor()); + set_cost("detail_render", + rec.mRenderCost > 0.f ? llformat("%.0f", rec.mRenderCost) : std::string("-"), + rec.mRenderCost > 0.f ? heatColor(rec.mRenderCost, 30000.f, 100000.f) : mutedColor()); + set_cost("detail_phys", + rec.mPhysicsCost > 0.f ? llformat("%.1f", rec.mPhysicsCost) : std::string("-"), + rec.mPhysicsCost > 0.f ? labelColor() : mutedColor()); + set_cost("detail_stream", + rec.mStreamingCost > 0.f ? llformat("%.1f", rec.mStreamingCost) : std::string("-"), + rec.mStreamingCost > 0.f ? labelColor() : mutedColor()); + } + + // Flags: the common ones as read-only checkboxes, the rest as muted text. + const bool show_flags = !derendered; + show("label_flags", show_flags); + show("check_scripted", show_flags); + show("check_light", show_flags); + show("check_physics", show_flags); + show("check_phantom", show_flags); + show("check_temp", show_flags); + if (show_flags) + { + getChild("check_scripted")->setValue(rec.hasFlag(ALObjectProperties::FLAG_SCRIPTED)); + getChild("check_light")->setValue(rec.hasFlag(ALObjectProperties::FLAG_LIGHT)); + getChild("check_physics")->setValue(rec.hasFlag(ALObjectProperties::FLAG_PHYSICS)); + getChild("check_phantom")->setValue(rec.hasFlag(ALObjectProperties::FLAG_PHANTOM)); + getChild("check_temp")->setValue(rec.hasFlag(ALObjectProperties::FLAG_TEMPORARY)); + } + // Suppress everything already conveyed elsewhere in the pane: the five + // checkboxes above, and the per-face columns (glow / fullbright / alpha / + // PBR) in the materials list below. + constexpr U32 SHOWN_ELSEWHERE = ALObjectProperties::FLAG_SCRIPTED | ALObjectProperties::FLAG_LIGHT + | ALObjectProperties::FLAG_PHYSICS | ALObjectProperties::FLAG_PHANTOM + | ALObjectProperties::FLAG_TEMPORARY + | ALObjectProperties::FLAG_GLOW | ALObjectProperties::FLAG_FULLBRIGHT + | ALObjectProperties::FLAG_ALPHA | ALObjectProperties::FLAG_PBR_MATERIAL + | ALObjectProperties::FLAG_AVATAR; + const std::string extra_flags = ALObjectProperties::flagsToString(rec.mFlags & ~SHOWN_ELSEWHERE); + show("detail_flags_extra", show_flags && !extra_flags.empty()); + if (!extra_flags.empty()) + set_text("detail_flags_extra", "Also: " + extra_flags); + + // Per-face material/texture list (live objects only); asset ids are + // perm-gated like the build floater's copy checks. Only rebuilt when the + // selection changes — the periodic detail refresh must not reset the + // user's scroll position mid-read. + getChild("detail_faces_layout")->setVisible(obj != nullptr); + if (!obj) + { + faces->deleteAllItems(); + mLastFacesID.setNull(); + } + else if (id != mLastFacesID) + { + mLastFacesID = id; + faces->deleteAllItems(); + const U8 num_tes = obj->getNumTEs(); + for (U8 i = 0; i < num_tes; ++i) + { + const LLTextureEntry* te = obj->getTE(i); + if (!te) + continue; + + std::string material; + const LLUUID& mat_id = obj->getRenderMaterialID(i); + if (mat_id.notNull()) + { + material = "PBR " + assetIdForDisplay(mat_id); + } + else + { + material = "tex " + assetIdForDisplay(te->getID()); + if (const LLMaterialPtr mat = te->getMaterialParams()) + { + if (mat->getNormalID().notNull()) + material += " +norm"; + if (mat->getSpecularID().notNull()) + material += " +spec"; + } + } + + const F32 alpha = te->getColor().mV[VALPHA]; + LLSD row; + row["columns"][0]["column"] = "face"; + row["columns"][0]["value"] = (S32)i; + row["columns"][1]["column"] = "material"; + row["columns"][1]["value"] = material; + row["columns"][2]["column"] = "alpha"; + row["columns"][2]["value"] = (alpha < 0.999f) ? llformat("%.2f", alpha) : std::string(); + row["columns"][3]["column"] = "glow"; + row["columns"][3]["value"] = (te->getGlow() > 0.f) ? llformat("%.2f", te->getGlow()) : std::string(); + row["columns"][4]["column"] = "bright"; + row["columns"][4]["value"] = te->getFullbright() ? std::string("Y") : std::string(); + faces->addElement(row); + } + } +} + +namespace +{ + ALSceneExplorerSort::ESortMode sortModeFromParam(const LLSD& param) + { + const std::string key = param.asString(); + if (key == "name") return ALSceneExplorerSort::SORT_NAME; + if (key == "land_impact") return ALSceneExplorerSort::SORT_LAND_IMPACT; + if (key == "triangles") return ALSceneExplorerSort::SORT_TRIANGLES; + if (key == "type") return ALSceneExplorerSort::SORT_TYPE; + if (key == "region_origin") return ALSceneExplorerSort::SORT_REGION_ORIGIN; + return ALSceneExplorerSort::SORT_DISTANCE; + } +} + +void ALFloaterSceneExplorer::setSortMode(const LLSD& param) +{ + const ALSceneExplorerSort::ESortMode mode = sortModeFromParam(param); + mViewModel.getSorter().setMode(mode); + gSavedSettings.setU32("ALSceneExplorerSortOrder", (U32)mode); + + // Choosing the land-impact key is explicit demand for every row's cost + // (the order is meaningless while they read 0). The requests run through + // the existing GetObjectCost pipeline, chunked and deduplicated there. + if (mode == ALSceneExplorerSort::SORT_LAND_IMPACT) + { + for (const auto& entry : mItems) + { + requestCostsFor(entry.second); + } + } + + mViewModel.requestSortAll(); + if (mTree) + mTree->arrangeAll(); +} + +bool ALFloaterSceneExplorer::checkSortMode(const LLSD& param) const +{ + return mViewModel.getSorter().getMode() == sortModeFromParam(param); +} + +void ALFloaterSceneExplorer::toggleShow(const LLSD& param) +{ + const std::string key = param.asString(); + if (key == "avatars") + { + mShowAvatars = !mShowAvatars; + gSavedSettings.setBOOL("ALSceneExplorerShowAvatars", mShowAvatars); + reconcile(); // prunes / restores the avatar rows immediately + } + else if (key == "derendered") + { + mShowDerendered = !mShowDerendered; + gSavedSettings.setBOOL("ALSceneExplorerShowDerendered", mShowDerendered); + syncDerendered(); + updateCategoryCounts(); + } + else if (key == "full_region") + { + mFullRegion = !mFullRegion; + gSavedSettings.setBOOL("ALSceneExplorerFullRegion", mFullRegion); + if (mFullRegion) + { + // The load is the feature, but the user should know they are + // asking the simulator for it ("don't show again" supported). + LLNotificationsUtil::add("SceneExplorerFullRegion"); + } + applyFullRegionMode(mFullRegion); + } + else if (key == "selection_sync") + { + mSelectionSync = !mSelectionSync; + gSavedSettings.setBOOL("ALSceneExplorerSelectionSync", mSelectionSync); + } + else if (key == "owners") + { + gSavedSettings.setBOOL("ALSceneExplorerOwnerSuffix", + !gSavedSettings.getBOOL("ALSceneExplorerOwnerSuffix")); + // Refresh what's on screen now; scrolled-in rows pick the change up + // from the periodic visible-row scan. + forEachVisibleRow([](ALSceneExplorerItem*, LLFolderViewItem* widget) + { + widget->refresh(); + }); + if (mTree) + mTree->arrangeAll(); + } +} + +bool ALFloaterSceneExplorer::checkShow(const LLSD& param) const +{ + const std::string key = param.asString(); + if (key == "avatars") + return mShowAvatars; + if (key == "derendered") + return mShowDerendered; + if (key == "full_region") + return mFullRegion; + if (key == "selection_sync") + return mSelectionSync; + if (key == "owners") + return gSavedSettings.getBOOL("ALSceneExplorerOwnerSuffix"); + return false; +} + +// ============================================================================ +// Full-region (360) coverage + scalability +// ============================================================================ +void ALFloaterSceneExplorer::applyFullRegionMode(bool active) +{ + if (active) + { + // Remember what to restore; LLAgent re-applies the agent-level mode + // to new regions after crossings, and reconcile() re-claims it if the + // 360 capture floater restores the mode out from under us. + mPrevILMode = gAgent.getInterestListMode(); + if (gAgent.getInterestListMode() != IL_MODE_360) + { + gAgent.changeInterestListMode(IL_MODE_360); + } + return; + } + + // Releasing: never clobber another driver of the mode. If the 360 capture + // floater is up it owns 360 for as long as it lives; if someone already + // switched the mode away, there is nothing of ours to restore. + if (gAgent.getInterestListMode() != IL_MODE_360) + return; + if (LLFloaterReg::findInstance("360capture")) + return; + // A saved mode of 360 means something else had it on when we enabled — + // restoring "360" would strand the stream on, so fall back to default. + const std::string restore_mode = + (!mPrevILMode.empty() && mPrevILMode != IL_MODE_360) ? mPrevILMode : IL_MODE_DEFAULT; + gAgent.changeInterestListMode(restore_mode); +} + +void ALFloaterSceneExplorer::requestCostsFor(ALSceneExplorerItem* item) +{ + // One-shot demand trigger: a failed fetch leaves the cost stale forever, + // so without the guard every read would re-request it (the Stage-0 loop). + if (!item || item->wasCostRequested() + || item->isContainer() || item->isDerenderedType() + || item->getItemType() == ALSceneExplorerItem::TYPE_AVATAR) + { + return; + } + LLViewerObject* obj = gObjectList.findObject(item->getUUID()); + if (!obj || obj->isDead()) + return; + if (obj->peekLinksetCost() > 0.f) + return; // already costed (the reconcile peek keeps the record fresh) + item->noteCostRequested(); + obj->getObjectCost(); + obj->getLinksetCost(); +} + +void ALFloaterSceneExplorer::forEachVisibleRow( + const std::function& fn) +{ + if (!mTree || !mTreePanel || mItems.empty()) + return; + + const LLRect viewport = mTreePanel->calcScreenRect(); + for (const auto& entry : mItems) + { + ALSceneExplorerItem* item = entry.second; + if (item->isContainer()) + continue; + auto wit = mWidgets.find(entry.first); + if (wit == mWidgets.end()) + continue; + LLFolderViewItem* widget = wit->second; + if (!widget->getVisible() || !widget->isInVisibleChain()) + continue; + if (!viewport.overlaps(widget->calcScreenRect())) + continue; + fn(item, widget); + } +} + +void ALFloaterSceneExplorer::scanVisibleRows() +{ + ALObjectPropertiesCache& cache = ALObjectPropertiesCache::instance(); + forEachVisibleRow([this, &cache](ALSceneExplorerItem* item, LLFolderViewItem* widget) + { + // Keep on-screen label suffixes (distance, complexity, LI) current — + // they are captured at widget refresh and would otherwise go stale as + // the agent moves. + widget->refresh(); + + if (item->isDerenderedType()) + return; + + // Viewport-bounded cost demand: what the user is looking at gets its + // LI without costing the whole region. + requestCostsFor(item); + + // Bump this row's queued props request ahead of the off-screen + // backlog (no-op unless it is still waiting in the main queue). + const LLUUID& id = item->getUUID(); + const ALObjectPropertiesCache::ServerProps* p = cache.get(id); + if (mQueuedProps.count(id) && !(p && p->mHasFullData) + && mPriorityQueued.insert(id).second) + { + mPriorityFetch.push_back(id); + } + }); +} + +void ALFloaterSceneExplorer::updateStatusText() +{ + // Transient pipeline feedback; quiet when there is nothing in flight. + std::string status; + if (const size_t building = mBuildQueue.size()) + { + status = llformat("Loading %d objects...", (S32)building); + } + else if (const size_t refilling = mRefillQueue.size()) + { + status = llformat("Refreshing %d objects...", (S32)refilling); + } + else if (const size_t fetching = mFetchQueue.size() + mPriorityFetch.size()) + { + status = llformat("Fetching details: %d...", (S32)fetching); + } + else if (mItems.size() > 30000) + { + status = llformat("%d rows - large region", (S32)mItems.size()); + } + + if (status != mLastStatus) + { + mLastStatus = status; + getChild("status_text")->setText(status); + } +} + +// ============================================================================ +// Actions +// ============================================================================ +ALSceneExplorerItem* ALFloaterSceneExplorer::getSelectedItem() const +{ + if (!mTree) + return nullptr; + LLFolderViewItem* sel = mTree->getCurSelectedItem(); + return sel ? static_cast(sel->getViewModelItem()) : nullptr; +} + +// Every selected scene row (containers excluded), in tree selection order. +std::vector ALFloaterSceneExplorer::getSelectedSceneItems() const +{ + std::vector items; + if (!mTree) + return items; + for (LLFolderViewItem* widget : mTree->getSelectedItems()) + { + ALSceneExplorerItem* item = + widget ? static_cast(widget->getViewModelItem()) : nullptr; + if (item && !item->isContainer()) + items.push_back(item); + } + return items; +} + +LLViewerObject* ALFloaterSceneExplorer::getSelectedObject() const +{ + ALSceneExplorerItem* item = getSelectedItem(); + return item ? gObjectList.findObject(item->getUUID()) : nullptr; +} + +void ALFloaterSceneExplorer::selectInWorld(const uuid_vec_t& ids) +{ + std::vector objs; + objs.reserve(ids.size()); + for (const LLUUID& id : ids) + { + if (LLViewerObject* o = gObjectList.findObject(id)) + objs.push_back(o); + } + + LLSelectMgr* sm = LLSelectMgr::getInstance(); + mSyncingSelection = true; + sm->deselectAll(); + + // Mirror LLToolSelect's EditLinkedParts split exactly: with "Edit linked" + // on, select the individual prim; otherwise always the whole linkset. + // Selecting a lone child while the build tools are in whole-object mode + // (or vice versa) desyncs the manipulators from what the user expects. + // Also mirror its RLVa gate: while the build tools are up, objects the + // user may not edit are refused, exactly as clicking them in-world is. + static LLCachedControl edit_linked(gSavedSettings, "EditLinkedParts", false); + const bool rlv_gate_edit = RlvActions::isRlvEnabled() && LLFloaterReg::instanceVisible("build"); + for (LLViewerObject* obj : objs) + { + if (rlv_gate_edit && !obj->isAvatar() && !RlvActions::canEdit(obj)) + continue; + if (edit_linked && !obj->isAvatar()) + { + sm->selectObjectOnly(obj, SELECT_ALL_TES); + } + else + { + sm->selectObjectAndFamily(obj, /*add_to_end=*/true); + } + } + mSyncingSelection = false; +} + +void ALFloaterSceneExplorer::syncSelectionToWorld() +{ + if (mSyncingSelection || !mSelectionSync) + return; + // Drive the in-world selection only while an editing surface is up. Plain + // browsing must never grab the user's selection — pushing one has side + // effects (avatar look-at/point-at targeting, edit-mode behaviour) that + // read as the camera/avatar reacting to every tree click. The sync state + // deliberately doesn't advance while gated, so opening Build/Inspect + // adopts the currently selected rows once. + if (!LLFloaterReg::instanceVisible("build") && !LLFloaterReg::instanceVisible("inspect")) + return; + + uuid_vec_t ids; + for (ALSceneExplorerItem* item : getSelectedSceneItems()) + { + if (item->isDerenderedType()) + continue; + if (gObjectList.findObject(item->getUUID())) + ids.push_back(item->getUUID()); + } + std::sort(ids.begin(), ids.end()); + if (ids == mLastPushedSelection) + return; + mLastPushedSelection = ids; + if (!ids.empty()) + selectInWorld(ids); +} + +void ALFloaterSceneExplorer::onWorldSelectionChanged() +{ + // Fires for every selection update (including per-frame property updates + // while editing) — just flag; idleUpdate processes once per pass. + mWorldSelectionDirty = true; +} + +void ALFloaterSceneExplorer::syncSelectionFromWorld() +{ + // The sync toggle silences the passive mirroring in both directions; + // explicit actions (Edit / Inspect / menu staging) still select. + if (mSyncingSelection || !mSelectionSync || !mTree) + return; + // Mirror only while an editing surface is up, matching the tree->world + // direction. While plain browsing, transient world selections (pie menus, + // the gear menu's own staging — which selects the whole family and would + // bounce the tree from a child row to its root) must not yank the tree's + // selection or scroll position. + if (!LLFloaterReg::instanceVisible("build") && !LLFloaterReg::instanceVisible("inspect")) + return; + + // Mirror the in-world selection onto the tree at the granularity it was + // made: selection roots (an individually selected child prim is its own + // root node). Only ids the tree actually holds participate. + uuid_vec_t ids; + LLObjectSelectionHandle selection = LLSelectMgr::getInstance()->getSelection(); + for (LLObjectSelection::valid_root_iterator it = selection->valid_root_begin(); + it != selection->valid_root_end(); ++it) + { + LLViewerObject* obj = (*it)->getObject(); + if (obj && mWidgets.count(obj->getID())) + ids.push_back(obj->getID()); + } + // Never clear the tree's selection because the world's emptied (closing + // the build floater deselects, but the user's place in the tree remains). + if (ids.empty()) + return; + std::sort(ids.begin(), ids.end()); + if (ids == mLastPushedSelection) + return; // our own push echoing back through mUpdateSignal + mLastPushedSelection = ids; + + mSyncingSelection = true; + mTree->clearSelection(); + bool first = true; + for (const LLUUID& id : ids) + { + auto wit = mWidgets.find(id); + if (wit == mWidgets.end()) + continue; + if (first) + { + // Open ancestors and scroll so the row is actually seen; don't + // steal keyboard focus from the world the user is clicking in. + mTree->setSelection(wit->second, /*openitem=*/true, /*take_keyboard_focus=*/false); + first = false; + } + else + { + mTree->changeSelection(wit->second, true); + } + } + mTree->scrollToShowSelection(); + mSyncingSelection = false; +} + +void ALFloaterSceneExplorer::doSelectAllResults() +{ + if (!mTree) + return; + // Select every root-level row that passes the current filter (linkset + // granularity — child prims follow their roots through selectInWorld), + // feeding the batch menu actions (Take Copy / Return / Derender). + mSyncingSelection = true; + mTree->clearSelection(); + bool first = true; + auto select_category = [&](ALSceneExplorerItem* category) + { + if (!category) + return; + for (auto it = category->getChildrenBegin(); it != category->getChildrenEnd(); ++it) + { + ALSceneExplorerItem* child = static_cast(it->get()); + if (!child->passedFilter()) + continue; + auto wit = mWidgets.find(child->getUUID()); + if (wit == mWidgets.end()) + continue; + if (first) + { + mTree->setSelection(wit->second, false, true); + first = false; + } + else + { + mTree->changeSelection(wit->second, true); + } + } + }; + select_category(mObjectsCategory); + select_category(mAvatarsCategory); + mSyncingSelection = false; + // The next idle pass pushes the new multi-selection to the world if an + // editing surface is up (and the gear menu pushes it at open regardless). +} + +void ALFloaterSceneExplorer::doRefresh() +{ + ALObjectPropertiesCache& cache = ALObjectPropertiesCache::instance(); + mRefillQueue.clear(); + for (const auto& entry : mItems) + { + ALSceneExplorerItem* item = entry.second; + const ALSceneExplorerItem::EItemType type = item->getItemType(); + if (type == ALSceneExplorerItem::TYPE_AVATAR + || type == ALSceneExplorerItem::TYPE_ATTACHMENT_POINT + || item->isDerenderedType() + || item->isContainer()) + { + continue; + } + // Re-arm the bounded retry and the one-shot cost demand, and + // re-request anything that never resolved (assume the reply is lost). + item->resetFetchState(); + const ALObjectPropertiesCache::ServerProps* p = cache.get(entry.first); + if (!(p && p->mHasFullData)) + { + cache.clearPending(entry.first); + queueProps(entry.first); + } + // Local fields (per-face flags, geometry, prim counts) go stale after + // node build; re-fill them time-sliced so filters see current state. + mRefillQueue.push_back(entry.first); + } + + // Rows on screen get a forced re-request even when data is cached, so a + // rename or permission change made while standing here shows up — without + // re-probing the whole (possibly 360-streamed) region. + forEachVisibleRow([this, &cache](ALSceneExplorerItem* item, LLFolderViewItem*) + { + if (item->isDerenderedType() + || item->getItemType() == ALSceneExplorerItem::TYPE_AVATAR + || item->getItemType() == ALSceneExplorerItem::TYPE_ATTACHMENT_POINT) + { + return; + } + const LLUUID& id = item->getUUID(); + if (cache.isPending(id) || mQueuedProps.count(id)) + return; + if (mPriorityQueued.insert(id).second) + mPriorityFetch.push_back(id); + }); + + reconcile(); +} + +void ALFloaterSceneExplorer::activateItem(const LLUUID& id) +{ + // Act on the activated item itself, not on whatever the tree currently has + // selected — double-click paths can fire without a selection change. + LLViewerObject* obj = gObjectList.findObject(id); + if (!obj) + return; + uuid_vec_t ids; + ids.push_back(id); + mLastPushedSelection = ids; // keep the per-frame selection sync from re-selecting + selectInWorld(ids); + + switch (gSavedSettings.getU32("ALSceneExplorerActivateAction")) + { + case 1: + // Same RLVa edit gate as the in-world edit paths. + if (!RlvActions::isRlvEnabled() || RlvActions::canEdit(obj)) + { + openBuildTools(); + } + break; + case 2: LLFloaterReg::showInstance("inspect"); break; + case 3: break; // select only + case 0: + default: + // Focus zooms the camera only while browsing freely. With Build or + // Inspect up the user is editing — double-click means "retarget the + // selection", and yanking the camera mid-edit is jarring (the explicit + // Focus button / context entry still zooms regardless). + if (!LLFloaterReg::instanceVisible("build") && !LLFloaterReg::instanceVisible("inspect")) + { + handle_zoom_to_object(id); + } + break; + } +} + +void ALFloaterSceneExplorer::openBuildTools() +{ + LLFloaterReg::showInstance("build"); + LLToolMgr::getInstance()->setCurrentToolset(gBasicToolset); + if (gFloaterTools) + gFloaterTools->setEditTool(LLToolCompTranslate::getInstance()); +} + +void ALFloaterSceneExplorer::doFocus() +{ + if (LLViewerObject* obj = getSelectedObject()) + { + // Frames the object from the bounding box / FOV like the standard + // "Zoom To" everywhere else, instead of a fixed-offset camera jump. + handle_zoom_to_object(obj->getID()); + } +} + +void ALFloaterSceneExplorer::doEdit() +{ + LLViewerObject* obj = getSelectedObject(); + if (!obj) + return; + // The menu's edit path enforces RLVa @edit; entering build mode from the + // explorer must not bypass it. + if (RlvActions::isRlvEnabled() && !RlvActions::canEdit(obj)) + return; + uuid_vec_t ids; + ids.push_back(obj->getID()); + selectInWorld(ids); + openBuildTools(); +} + +void ALFloaterSceneExplorer::doInspect() +{ + LLViewerObject* obj = getSelectedObject(); + if (!obj) + return; + uuid_vec_t ids; + ids.push_back(obj->getID()); + selectInWorld(ids); + LLFloaterReg::showInstance("inspect"); +} + +void ALFloaterSceneExplorer::doTeleport() +{ + ALSceneExplorerItem* item = getSelectedItem(); + if (!item) + return; + if (LLViewerObject* obj = gObjectList.findObject(item->getUUID())) + { + gAgent.teleportViaLocation(obj->getPositionGlobal()); + return; + } + // Derendered objects keep their stored position. + if (item->getItemType() == ALSceneExplorerItem::TYPE_DERENDERED_OBJECT) + { + const LLVector3d& pos = item->getRecord().mPosGlobal; + if (!pos.isExactlyZero()) + gAgent.teleportViaLocation(pos); + } +} + +void ALFloaterSceneExplorer::doSit() +{ + LLViewerObject* obj = getSelectedObject(); + if (!obj || obj->isAvatar()) + return; + // The shared sit handler carries the RLVa / already-sitting handling. + handle_object_sit(obj->getID()); +} + +void ALFloaterSceneExplorer::doCopy(const LLSD& param) +{ + ALSceneExplorerItem* item = getSelectedItem(); + if (!item || item->isContainer()) + return; + + const ALObjectProperties::Record& rec = item->getRecord(); + const std::string what = param.asString(); + std::string out; + if (what == "name") + { + out = item->getName(); // avatar rows are already RLVa-anonymized + } + else if (what == "uuid") + { + out = item->getUUID().asString(); + } + else if (what == "pos") + { + out = llformat("<%.1f, %.1f, %.1f>", + rec.mPosRegion.mV[VX], rec.mPosRegion.mV[VY], rec.mPosRegion.mV[VZ]); + } + else if (what == "slurl") + { + // The menu disables this under RLVa @showloc; double-check anyway. + if (RlvActions::isRlvEnabled() && !RlvActions::canShowLocation()) + return; + LLViewerRegion* region = gAgent.getRegion(); + if (!region || rec.mPosGlobal.isExactlyZero()) + return; + out = LLSLURL(region->getName(), rec.mPosGlobal).getSLURLString(); + } + if (out.empty()) + return; + + const LLWString wout = utf8str_to_wstring(out); + LLClipboard::instance().copyToClipboard(wout, 0, (S32)wout.size()); +} + +void ALFloaterSceneExplorer::doCopyResults() +{ + // Export the rows that pass the current filter (linkset granularity) as + // CSV for offline auditing. Owners resolve to a name only when the name + // cache already has it; otherwise the UUID still identifies them. + std::string out = "Name,Owner,Land Impact,Triangles,Distance (m),UUID\n"; + auto csv_escape = [](std::string s) + { + LLStringUtil::replaceString(s, "\"", "\"\""); + return "\"" + s + "\""; + }; + + S32 rows = 0; + auto append_category = [&](ALSceneExplorerItem* category) + { + if (!category) + return; + for (auto it = category->getChildrenBegin(); it != category->getChildrenEnd(); ++it) + { + ALSceneExplorerItem* child = static_cast(it->get()); + if (!child->passedFilter()) + continue; + const ALObjectProperties::Record& rec = child->getRecord(); + + std::string owner; + if (child->getItemType() == ALSceneExplorerItem::TYPE_AVATAR) + { + owner = child->getName(); + } + else if (rec.mPropsValid) + { + // The resolved-owner map covers groups and avatars alike; + // RLVa-hidden avatar owners fall through to the anonymized + // display name, anything still unresolved to the bare id. + const LLUUID& owner_id = rec.mGroupOwned ? rec.mGroupId : rec.mOwnerId; + auto found = mOwnerNames.find(owner_id); + if (found != mOwnerNames.end() && !found->second.mName.empty()) + owner = found->second.mName; + else if (!rec.mGroupOwned) + owner = avatarDisplayName(rec.mOwnerId); + if (owner.empty()) + owner = owner_id.asString(); + } + + out += csv_escape(child->getName()) + "," + csv_escape(owner) + "," + + (rec.mLandImpact > 0.f ? llformat("%.0f", rec.mLandImpact) : std::string()) + + "," + (rec.mNumTriangles > 0 ? llformat("%u", rec.mNumTriangles) : std::string()) + + "," + llformat("%.1f", rec.mDistance) + + "," + child->getUUID().asString() + "\n"; + ++rows; + } + }; + append_category(mObjectsCategory); + append_category(mAvatarsCategory); + append_category(mDerenderedCategory); + if (!rows) + return; + + const LLWString wout = utf8str_to_wstring(out); + LLClipboard::instance().copyToClipboard(wout, 0, (S32)wout.size()); +} + +void ALFloaterSceneExplorer::doShowOnMap() +{ + ALSceneExplorerItem* item = getSelectedItem(); + if (!item || item->isContainer()) + return; + if (RlvActions::isRlvEnabled() && !RlvActions::canShowLocation()) + return; + + if (item->getItemType() == ALSceneExplorerItem::TYPE_AVATAR) + { + LLAvatarActions::showOnMap(item->getUUID()); + return; + } + const LLVector3d& pos = item->getRecord().mPosGlobal; + if (pos.isExactlyZero()) + return; + LLTracker::trackLocation(pos, item->getName(), LLStringUtil::null, LLTracker::LOCATION_ITEM); + mBeaconTrackedID = item->getUUID(); + LLFloaterReg::showInstance("world_map", "center"); +} + +void ALFloaterSceneExplorer::doBeacon() +{ + ALSceneExplorerItem* item = getSelectedItem(); + if (!item || item->isContainer()) + return; + + // Toggle: a second use on the same row clears its beacon. + if (mBeaconTrackedID == item->getUUID()) + { + LLTracker::stopTracking(false); + mBeaconTrackedID.setNull(); + return; + } + const LLVector3d& pos = item->getRecord().mPosGlobal; + if (pos.isExactlyZero()) + return; + LLTracker::trackLocation(pos, item->getName(), LLStringUtil::null, LLTracker::LOCATION_ITEM); + mBeaconTrackedID = item->getUUID(); +} + +void ALFloaterSceneExplorer::doBlockOwner() +{ + ALSceneExplorerItem* item = getSelectedItem(); + if (!item || item->isContainer()) + return; + const ALObjectProperties::Record& rec = item->getRecord(); + if (!rec.mPropsValid || rec.mGroupOwned + || rec.mOwnerId.isNull() || rec.mOwnerId == gAgentID) + { + return; + } + LLAvatarActions::toggleBlock(rec.mOwnerId); +} + +void ALFloaterSceneExplorer::doAvatarAction(const LLSD& param) +{ + ALSceneExplorerItem* item = getSelectedItem(); + if (!item) + return; + const LLUUID id = item->getUUID(); + if (id.isNull()) + return; + + // All of these carry their own permission / RLVa gating and confirmation + // dialogs; the menu only decides visibility. + const std::string action = param.asString(); + if (action == "profile") LLAvatarActions::showProfile(id); + else if (action == "im") LLAvatarActions::startIM(id); + else if (action == "offer_tp") LLAvatarActions::offerTeleport(id); + else if (action == "request_tp") LLAvatarActions::teleportRequest(id); + else if (action == "zoom") ALAvatarActions::zoomIn(id); + else if (action == "teleport_to") ALAvatarActions::teleportTo(id); + else if (action == "freeze") ALAvatarActions::parcelFreeze(id); + else if (action == "eject") ALAvatarActions::parcelEject(id); + else if (action == "estate_tp_home") ALAvatarActions::estateTeleportHome(id); + else if (action == "estate_kick") ALAvatarActions::estateKick(id); + else if (action == "estate_ban") ALAvatarActions::estateBan(id); + else if (action == "block") LLAvatarActions::toggleBlock(id); + else if (action == "report") ALAvatarActions::reportAbuse(id); +} + +// ============================================================================ +// Context / gear menu +// ============================================================================ +void ALFloaterSceneExplorer::onGearMouseDown() +{ + // Mouse-down fires before LLMenuButton::toggleMenu shows the menu, so + // this is the per-open hook to mirror the right-click popup's state. + LLToggleableMenu* menu = getChild("gear_btn")->getMenu(); + if (!menu) + return; + resetMenuEntries(*menu); + buildRowContextMenu(*menu, 0); +} + +void ALFloaterSceneExplorer::buildRowContextMenu(LLMenuGL& menu, U32 flags) +{ + ALSceneExplorerItem* item = getSelectedItem(); + + std::vector show; + std::vector disabled; + + // Structural rows (and no selection at all): tree-level utilities only. + if (!item || item->isContainer()) + { + show = { "refresh", "copy_results" }; + hideMenuEntries(menu, show, disabled); + return; + } + + // Multi-selection: only the naturally batching entries. Everything is + // staged into the world selection so the reused handlers (Take Copy, + // Return) and their enable predicates act on the full set; Derender and + // Restore batch through their own handlers. Single-target entries + // (Touch / Pay / Profile / ...) don't appear at all. + const std::vector selected = getSelectedSceneItems(); + if (selected.size() > 1) + { + bool all_derendered = true; + uuid_vec_t ids; + for (ALSceneExplorerItem* sel_item : selected) + { + all_derendered &= sel_item->isDerenderedType(); + if (!sel_item->isDerenderedType() + && sel_item->getItemType() != ALSceneExplorerItem::TYPE_AVATAR + && gObjectList.findObject(sel_item->getUUID())) + { + ids.push_back(sel_item->getUUID()); + } + } + + if (all_derendered) + { + show = { "restore_derendered" }; + hideMenuEntries(menu, show, disabled); + return; + } + + if (!ids.empty()) + { + uuid_vec_t sorted = ids; + std::sort(sorted.begin(), sorted.end()); + mLastPushedSelection = sorted; + selectInWorld(ids); + + show = { "take_copy", "sep_derender", "derender_temp", "derender_perm" }; + if (!registryEnabled("Tools.EnableTakeCopy")) + disabled.push_back("take_copy"); + if (registryEnabled("Object.EnableReturn")) + show.insert(show.begin() + 1, { "sep_admin", "return" }); + } + hideMenuEntries(menu, show, disabled); + return; + } + + const LLUUID id = item->getUUID(); + const ALObjectProperties::Record& rec = item->getRecord(); + const bool rlv = RlvActions::isRlvEnabled(); + const bool can_show_location = !rlv || RlvActions::canShowLocation(); + + if (item->isDerenderedType()) + { + show = { "restore_derendered", "sep_derender", "sep_copy", "copy_menu", "copy_name", "copy_uuid" }; + if (item->getItemType() == ALSceneExplorerItem::TYPE_DERENDERED_OBJECT) + { + show.insert(show.end(), { "teleport", "show_map", "beacon", "copy_pos", "copy_slurl" }); + if (!can_show_location) + disabled.insert(disabled.end(), { "show_map", "copy_slurl" }); + } + hideMenuEntries(menu, show, disabled); + return; + } + + if (item->getItemType() == ALSceneExplorerItem::TYPE_AVATAR) + { + const bool is_self = (id == gAgentID); + show = { "focus", "av_zoom", "av_teleport", "show_map", "beacon", + "sep_copy", "copy_menu", "copy_name", "copy_uuid", "copy_pos", "copy_slurl", + "filter_by_owner" }; + if (!ALAvatarActions::canZoomIn(id)) + disabled.push_back("av_zoom"); + if (!ALAvatarActions::canTeleportTo(id)) + disabled.push_back("av_teleport"); + if (!can_show_location) + disabled.insert(disabled.end(), { "show_map", "copy_slurl" }); + + if (is_self) + { + // Social / moderation actions make no sense on yourself. + show.insert(show.begin(), { "av_profile", "sep_avatar_actions" }); + } + else + { + show.insert(show.begin(), { "av_profile", "av_im", "av_offer_tp", "av_request_tp", + "sep_avatar_actions" }); + if (!LLAvatarActions::canOfferTeleport(id)) + disabled.push_back("av_offer_tp"); + + // Admin entries are hidden outright without the matching power. + if (ALAvatarActions::canFreezeEject(id)) + show.insert(show.end(), { "sep_admin", "freeze", "eject" }); + if (ALAvatarActions::canManageAvatarsEstate(id)) + show.insert(show.end(), { "sep_admin", "estate_home", "estate_kick", "estate_ban" }); + + show.insert(show.end(), { "sep_moderation", "av_block", "av_report" }); + if (!LLAvatarActions::canBlock(id)) + disabled.push_back("av_block"); + } + hideMenuEntries(menu, show, disabled); + return; + } + + // Live object rows (world linkset / child prim / attachment root). + LLViewerObject* obj = gObjectList.findObject(id); + if (!obj || obj->isDead()) + { + // Row went stale between reconciles; offer the inert copies only. + show = { "copy_menu", "copy_name", "copy_uuid", "copy_pos" }; + hideMenuEntries(menu, show, disabled); + return; + } + + // Stage the row as the live selection so the reused viewer handlers + // (Object.Touch / Object.Return / PayObject / ...) and their registered + // enable predicates act on it — the same model as the in-world + // right-click, which also selects what it targets. + uuid_vec_t ids; + ids.push_back(id); + mLastPushedSelection = ids; + selectInWorld(ids); + + const bool is_attachment = obj->isAttachment(); + + show = { "touch", "open", "buy", "pay", "take_copy", "sep_object_actions", + "focus", "edit", "inspect", "teleport", "show_map", "beacon", + "sep_copy", "copy_menu", "copy_name", "copy_uuid", "copy_pos", "copy_slurl", + "filter_by_owner" }; + if (!is_attachment) + { + show.insert(show.begin() + 1, "sit"); + if (registryEnabled("Object.EnableReturn")) + show.insert(show.end(), { "sep_admin", "return" }); + show.insert(show.end(), { "sep_derender", "derender_temp", "derender_perm" }); + } + show.insert(show.end(), { "sep_moderation", "block_object", "block_owner", "report" }); + + if (!(obj->flagHandleTouch() && (!rlv || RlvActions::canTouch(obj)))) + disabled.push_back("touch"); + if (rlv && !RlvActions::canSit(obj)) + disabled.push_back("sit"); + if (!registryEnabled("Object.EnableOpen")) + disabled.push_back("open"); + if (!registryEnabled("Object.EnableBuy")) + disabled.push_back("buy"); + if (!registryEnabled("EnablePayObject")) + disabled.push_back("pay"); + if (!registryEnabled("Tools.EnableTakeCopy")) + disabled.push_back("take_copy"); + if (rlv && !RlvActions::canEdit(obj)) + disabled.push_back("edit"); + if (!registryEnabled("Object.EnableMute")) + disabled.push_back("block_object"); + if (!(rec.mPropsValid && !rec.mGroupOwned + && rec.mOwnerId.notNull() && rec.mOwnerId != gAgentID)) + disabled.push_back("block_owner"); + if (!registryEnabled("Object.EnableReportAbuse")) + disabled.push_back("report"); + if (!(rec.mPropsValid && (rec.mOwnerId.notNull() || rec.mGroupId.notNull()))) + disabled.push_back("filter_by_owner"); + if (!ALDerenderList::canAdd(obj)) + disabled.insert(disabled.end(), { "derender_temp", "derender_perm" }); + if (!can_show_location) + disabled.insert(disabled.end(), { "show_map", "copy_slurl" }); + + hideMenuEntries(menu, show, disabled); +} + +void ALFloaterSceneExplorer::doDerender(const LLSD& param) +{ + // Every selected derenderable object row (multi-select batches). + std::vector objs; + for (ALSceneExplorerItem* item : getSelectedSceneItems()) + { + if (item->isDerenderedType()) + continue; + LLViewerObject* obj = gObjectList.findObject(item->getUUID()); + if (!obj || obj->isAvatar() || obj->isAttachment()) + continue; + if (!ALDerenderList::canAdd(obj)) + continue; + objs.push_back(obj); + } + if (objs.empty()) + return; + + // ALDerenderList::addSelection() consumes the live selection; always feed + // it the whole families regardless of the EditLinkedParts split so each + // entry's root/child local-id bookkeeping is complete. + LLSelectMgr* sm = LLSelectMgr::getInstance(); + mSyncingSelection = true; + sm->deselectAll(); + for (LLViewerObject* obj : objs) + { + sm->selectObjectAndFamily(obj, /*add_to_end=*/true); + } + mSyncingSelection = false; + + if (ALDerenderList::canAddSelection()) + { + ALDerenderList::instance().addSelection(param.asString() == "permanent"); + } + sm->deselectAll(); // whatever the kill left behind + // The change signal already ran syncDerendered(); the live nodes fall out + // on the next reconcile sweep. +} + +void ALFloaterSceneExplorer::doRestore() +{ + // Every selected derendered row (multi-select batches), split by entry + // type for ALDerenderList's two namespaces. + uuid_vec_t object_ids; + uuid_vec_t avatar_ids; + for (ALSceneExplorerItem* item : getSelectedSceneItems()) + { + if (item->getItemType() == ALSceneExplorerItem::TYPE_DERENDERED_OBJECT) + object_ids.push_back(item->getUUID()); + else if (item->getItemType() == ALSceneExplorerItem::TYPE_DERENDERED_AVATAR) + avatar_ids.push_back(item->getUUID()); + } + if (!object_ids.empty()) + ALDerenderList::instance().removeObjects(ALDerenderEntry::TYPE_OBJECT, object_ids); + if (!avatar_ids.empty()) + ALDerenderList::instance().removeObjects(ALDerenderEntry::TYPE_AVATAR, avatar_ids); + // removeObjects() requests the objects back from the sim (cache-miss + // refetch) and fires the change signal, which prunes the rows. +} diff --git a/indra/newview/alfloatersceneexplorer.h b/indra/newview/alfloatersceneexplorer.h new file mode 100644 index 0000000000..c517921dff --- /dev/null +++ b/indra/newview/alfloatersceneexplorer.h @@ -0,0 +1,245 @@ +/** + * @file alfloatersceneexplorer.h + * @brief Scene Explorer floater: a filterable scene-graph tree of region content + * + * Copyright (c) 2026, Rye Mutt + * + * The source code in this file is provided to you under the terms of the + * GNU Lesser General Public License, version 2.1, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. Terms of the LGPL can be found in doc/LGPL-licence.txt + * in this distribution, or online at http://www.gnu.org/licenses/lgpl-2.1.txt + * + */ +#ifndef AL_FLOATERSCENEEXPLORER_H +#define AL_FLOATERSCENEEXPLORER_H + +#include +#include + +#include +#include +#include + +#include "llfloater.h" +#include "llframetimer.h" +#include "lluuid.h" + +#include "alsceneexplorermodel.h" + +class LLAvatarName; +class LLFolderView; +class LLFolderViewItem; +class LLLayoutPanel; +class LLMenuGL; +class LLPanel; +class LLViewerObject; + +class ALFloaterSceneExplorer final : public LLFloater +{ + friend class LLFloaterReg; +public: + bool postBuild() override; + void onOpen(const LLSD& key) override; + void onClose(bool app_quitting) override; + void draw() override; + bool handleKeyHere(KEY key, MASK mask) override; + + // Invoked from ALSceneExplorerItem::openItem (double-click / Enter). + void activateItem(const LLUUID& id); + + // Shows/hides the shared superset menu's entries for the selected row's + // type and stages live object rows as the in-world selection so reused + // viewer handlers (touch/buy/return/...) and their enable predicates act + // on them. Driven by the folder view's right-click popup (via + // ALSceneExplorerItem::buildContextMenu) and by the gear button. + void buildRowContextMenu(LLMenuGL& menu, U32 flags); + + // Re-read the persisted filter settings into the quick-bar controls and + // the filter object. The companion filters floater calls this after + // writing settings, keeping both surfaces views of the same state. + void refreshFilters(); + + // Canonical "reset all": clears the quick-bar search/owner state and every + // persisted filter (flags, types, scope, radius, thresholds) back to its + // default. The companion filters floater's reset button delegates here. + void doResetFilters(); + +private: + ALFloaterSceneExplorer(const LLSD& key); + ~ALFloaterSceneExplorer() override; + + // --- idle update (driven by gIdleCallbacks, not draw) ----------------- + static void onIdle(void* user_data); + void idleUpdate(); + void drainBuildQueue(F64 max_time); + void drainRefillQueue(F64 max_time); + + // --- tree construction ------------------------------------------------ + void buildTree(); + void reconcile(); + ALSceneExplorerItem* getOrCreateNode(LLViewerObject* obj); + ALSceneExplorerItem* getOrCreatePointFolder(LLViewerObject* avatar, LLViewerObject* attachment); + LLFolderViewItem* createWidget(ALSceneExplorerItem* item, bool is_folder, LLFolderViewItem* parent_widget); + void eraseSubtreeMaps(ALSceneExplorerItem* item); + void removeNode(ALSceneExplorerItem* item); + void clearTree(); + + // --- derendered category (synthetic rows from ALDerenderList) ---------- + void syncDerendered(); + void ensureDerenderedCategory(); + void destroyDerenderedCategory(); + void onDerenderListChanged(); + + // --- async property fetch -------------------------------------------- + void queueProps(const LLUUID& id); + void drainPropsQueue(); + void retryUnresolved(); + void onPropsCacheChanged(const LLUUID& id); + void applyServerProps(ALSceneExplorerItem* item); + void onAvatarNameLoaded(const LLUUID& id, const LLAvatarName& av_name); + + // --- full-region (360) coverage + scalability --------------------------- + void applyFullRegionMode(bool active); + void forEachVisibleRow(const std::function& fn); + void scanVisibleRows(); + void updateStatusText(); + void requestCostsFor(ALSceneExplorerItem* item); + + // --- owner name resolution (search + suffix) ---------------------------- + void noteOwnerFor(ALSceneExplorerItem* item); + void resolveOwnerName(const LLUUID& owner_id, bool group_owned); + void onOwnerNameResolved(const LLUUID& owner_id, const std::string& name, bool is_group); + void auditOwnerNames(); + + // --- filters / sort --------------------------------------------------- + void onFilterChanged(); + void setSortMode(const LLSD& param); + bool checkSortMode(const LLSD& param) const; + void toggleShow(const LLSD& param); + bool checkShow(const LLSD& param) const; + void doFilterByOwner(); + void doShowFilters(); + void updateOwnerFilterLabel(); + void updateCategoryCounts(); + void updateActionButtons(); + + // --- detail pane -------------------------------------------------------- + void onToggleDetails(); + void refreshDetail(); + void fillObjectDetail(ALSceneExplorerItem* item); + void fillAvatarDetail(ALSceneExplorerItem* item); + + // --- actions ---------------------------------------------------------- + void syncSelectionToWorld(); + void onWorldSelectionChanged(); + void syncSelectionFromWorld(); + void doSelectAllResults(); + void doRefresh(); + ALSceneExplorerItem* getSelectedItem() const; + std::vector getSelectedSceneItems() const; + LLViewerObject* getSelectedObject() const; + void selectInWorld(const uuid_vec_t& ids); + void openBuildTools(); + void onGearMouseDown(); + void doFocus(); + void doEdit(); + void doInspect(); + void doTeleport(); + void doSit(); + void doCopy(const LLSD& param); + void doCopyResults(); + void doShowOnMap(); + void doBeacon(); + void doBlockOwner(); + void doAvatarAction(const LLSD& param); + void doDerender(const LLSD& param); + void doRestore(); + + // --- members ---------------------------------------------------------- + ALSceneExplorerViewModel mViewModel; + LLFolderView* mTree = nullptr; + LLPanel* mTreePanel = nullptr; + // Received-items-style host: holds the expander bar + detail content and + // collapses down to the bar via LLLayoutStack::collapsePanel(). + LLLayoutPanel* mDetailHost = nullptr; + + ALSceneExplorerItem* mRootItem = nullptr; + ALSceneExplorerItem* mObjectsCategory = nullptr; + ALSceneExplorerItem* mAvatarsCategory = nullptr; + LLFolderViewItem* mObjectsWidget = nullptr; + LLFolderViewItem* mAvatarsWidget = nullptr; + // Created lazily while derendered entries exist and the toggle is on, so + // an empty category never clutters the tree. + ALSceneExplorerItem* mDerenderedCategory = nullptr; + LLFolderViewItem* mDerenderedWidget = nullptr; + + boost::unordered_map mItems; + boost::unordered_map mWidgets; + + // Property-fetch pipeline: mFetchQueue holds ids waiting to be sent + // (mQueuedProps mirrors its membership for dedup); once sent, an id is + // tracked as in-flight by ALObjectPropertiesCache until its reply lands. + // On-screen rows jump the line through mPriorityFetch (mPriorityQueued + // mirrors it), drained ahead of the main queue. + std::deque mFetchQueue; + boost::unordered_set mQueuedProps; + std::deque mPriorityFetch; + boost::unordered_set mPriorityQueued; + + // New objects discovered by reconcile() awaiting time-sliced widget creation + // in drainBuildQueue(), so a 60k-object region populates without a stall. + std::deque mBuildQueue; + boost::unordered_set mQueued; + + // Explicit Refresh: rows awaiting a time-sliced local Record re-fill + // (per-face flags, geometry, prim counts go stale after node build). + std::deque mRefillQueue; + + boost::signals2::connection mPropsConn; + boost::signals2::connection mDerenderConn; + boost::signals2::connection mWorldSelConn; + + // Owner display names for the searchable text and row suffixes, resolved + // once per unique owner/group id. An empty name records "unresolved or + // RLVa-hidden"; auditOwnerNames() re-resolves/scrubs entries when the + // @shownames restriction flips mid-session (groups are never hidden). + struct ResolvedOwner + { + std::string mName; + bool mIsGroup = false; + }; + boost::unordered_map mOwnerNames; + boost::unordered_set mOwnerNamesPending; + + LLFrameTimer mReconcileTimer; + LLFrameTimer mFetchTimer; + LLFrameTimer mRetryTimer; + + U64 mLastRegionHandle = 0; + // Last id set synced between the tree and the world selection, kept + // sorted — used for change detection in both directions, which is also + // what breaks the world<->tree feedback loop. + uuid_vec_t mLastPushedSelection; + LLUUID mLastButtonStateID; // selection the action buttons were last gated for + LLUUID mLastDetailID; // selection the detail pane was last built for + LLUUID mLastFacesID; // selection the faces list was last built for + LLUUID mBeaconTrackedID; // row the location tracker beacon was set for + LLUUID mFilterOwnerId; // "Filter by this owner" target (session-only) + LLVector3d mLastFilterAgentPos; // last agent position the radius filter ran at + LLVector3d mLastSortAgentPos; // last agent position the distance sort ran at + std::string mPrevILMode; // interest-list mode before 360 was applied + std::string mLastStatus; // last status-line text (avoid re-set churn) + bool mShowAvatars = true; + bool mShowDerendered = false; + bool mFullRegion = false; + bool mSelectionSync = true; // passive tree<->world selection mirroring + bool mObjectsMoved = false; // any object moved since the last re-sort + bool mScanVisible = false; // run scanVisibleRows after the next arrange + bool mWorldSelectionDirty = false; // LLSelectMgr signalled a change + bool mDetailsExpanded = false; + bool mDetailDirty = false; + bool mSyncingSelection = false; +}; + +#endif // AL_FLOATERSCENEEXPLORER_H diff --git a/indra/newview/alfloatersceneexplorerfilters.cpp b/indra/newview/alfloatersceneexplorerfilters.cpp new file mode 100644 index 0000000000..bdcaa90fda --- /dev/null +++ b/indra/newview/alfloatersceneexplorerfilters.cpp @@ -0,0 +1,177 @@ +/** + * @file alfloatersceneexplorerfilters.cpp + * @brief Companion filters floater for the Scene Explorer + * + * Copyright (c) 2026, Rye Mutt + * + * The source code in this file is provided to you under the terms of the + * GNU Lesser General Public License, version 2.1, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. Terms of the LGPL can be found in doc/LGPL-licence.txt + * in this distribution, or online at http://www.gnu.org/licenses/lgpl-2.1.txt + * + */ + +#include "llviewerprecompiledheaders.h" + +#include "alfloatersceneexplorerfilters.h" + +#include "llcheckboxctrl.h" +#include "llfloaterreg.h" +#include "llradiogroup.h" +#include "llsliderctrl.h" +#include "llspinctrl.h" + +#include "alfloatersceneexplorer.h" +#include "alobjectproperties.h" +#include "alsceneexplorermodel.h" +#include "llviewercontrol.h" + +namespace +{ + // Checkbox name -> EFlag bit (mask semantics: all checked bits required). + constexpr std::pair FLAG_CHECKS[] = { + { "check_scripted", ALObjectProperties::FLAG_SCRIPTED }, + { "check_physics", ALObjectProperties::FLAG_PHYSICS }, + { "check_phantom", ALObjectProperties::FLAG_PHANTOM }, + { "check_temporary", ALObjectProperties::FLAG_TEMPORARY }, + { "check_flexible", ALObjectProperties::FLAG_FLEXIBLE }, + { "check_light", ALObjectProperties::FLAG_LIGHT }, + { "check_spotlight", ALObjectProperties::FLAG_SPOTLIGHT }, + { "check_projector", ALObjectProperties::FLAG_PROJECTOR }, + { "check_glow", ALObjectProperties::FLAG_GLOW }, + { "check_fullbright", ALObjectProperties::FLAG_FULLBRIGHT }, + { "check_media", ALObjectProperties::FLAG_MEDIA }, + { "check_particles", ALObjectProperties::FLAG_PARTICLES }, + { "check_probe", ALObjectProperties::FLAG_REFLECTION_PROBE }, + { "check_animated", ALObjectProperties::FLAG_ANIMATED }, + { "check_pbr", ALObjectProperties::FLAG_PBR_MATERIAL }, + { "check_alpha", ALObjectProperties::FLAG_ALPHA }, + { "check_attachment", ALObjectProperties::FLAG_ATTACHMENT }, + { "check_for_sale", ALObjectProperties::FLAG_FOR_SALE }, + { "check_payable", ALObjectProperties::FLAG_PAYABLE }, + }; + + constexpr std::pair TYPE_CHECKS[] = { + { "check_type_prim", 1u << ALObjectProperties::GEOM_PRIM }, + { "check_type_mesh", 1u << ALObjectProperties::GEOM_MESH }, + { "check_type_sculpt", 1u << ALObjectProperties::GEOM_SCULPT }, + }; +} + +ALFloaterSceneExplorerFilters::ALFloaterSceneExplorerFilters(const LLSD& key) +: LLFloater(key) +{ +} + +bool ALFloaterSceneExplorerFilters::postBuild() +{ + for (const auto& [name, bit] : FLAG_CHECKS) + { + getChild(name)->setCommitCallback( + boost::bind(&ALFloaterSceneExplorerFilters::onCommitAny, this)); + } + for (const auto& [name, bit] : TYPE_CHECKS) + { + getChild(name)->setCommitCallback( + boost::bind(&ALFloaterSceneExplorerFilters::onCommitAny, this)); + } + getChild("scope_radio")->setCommitCallback( + boost::bind(&ALFloaterSceneExplorerFilters::onCommitAny, this)); + getChild("radius_slider")->setCommitCallback( + boost::bind(&ALFloaterSceneExplorerFilters::onCommitAny, this)); + getChild("min_li_spinner")->setCommitCallback( + boost::bind(&ALFloaterSceneExplorerFilters::onCommitAny, this)); + getChild("min_tris_spinner")->setCommitCallback( + boost::bind(&ALFloaterSceneExplorerFilters::onCommitAny, this)); + getChild("reset_btn")->setCommitCallback( + boost::bind(&ALFloaterSceneExplorerFilters::onClickReset, this)); + + refreshFromSettings(); + return true; +} + +void ALFloaterSceneExplorerFilters::onOpen(const LLSD& key) +{ + // The quick bar may have edited the shared settings since we were last + // shown. + refreshFromSettings(); +} + +void ALFloaterSceneExplorerFilters::refreshFromSettings() +{ + const U32 flags = gSavedSettings.getU32("ALSceneExplorerFlagFilter"); + for (const auto& [name, bit] : FLAG_CHECKS) + { + getChild(name)->setValue((flags & bit) != 0); + } + const U32 types = gSavedSettings.getU32("ALSceneExplorerTypeFilter"); + for (const auto& [name, bit] : TYPE_CHECKS) + { + getChild(name)->setValue((types & bit) != 0); + } + getChild("scope_radio")->setSelectedIndex( + (S32)gSavedSettings.getU32("ALSceneExplorerScope")); + getChild("radius_slider")->setValue(gSavedSettings.getF32("ALSceneExplorerRadius")); + getChild("min_li_spinner")->setValue(gSavedSettings.getF32("ALSceneExplorerMinLandImpact")); + getChild("min_tris_spinner")->setValue( + (S32)gSavedSettings.getU32("ALSceneExplorerMinTriangles")); +} + +void ALFloaterSceneExplorerFilters::onCommitAny() +{ + U32 flags = 0; + for (const auto& [name, bit] : FLAG_CHECKS) + { + if (getChild(name)->getValue().asBoolean()) + flags |= bit; + } + U32 types = 0; + for (const auto& [name, bit] : TYPE_CHECKS) + { + if (getChild(name)->getValue().asBoolean()) + types |= bit; + } + gSavedSettings.setU32("ALSceneExplorerFlagFilter", flags); + gSavedSettings.setU32("ALSceneExplorerTypeFilter", types); + gSavedSettings.setU32("ALSceneExplorerScope", + (U32)llmax(0, getChild("scope_radio")->getSelectedIndex())); + gSavedSettings.setF32("ALSceneExplorerRadius", + (F32)getChild("radius_slider")->getValue().asReal()); + gSavedSettings.setF32("ALSceneExplorerMinLandImpact", + (F32)getChild("min_li_spinner")->getValue().asReal()); + gSavedSettings.setU32("ALSceneExplorerMinTriangles", + (U32)llmax(0, getChild("min_tris_spinner")->getValue().asInteger())); + + // Settings are shared state; the explorer re-reads them into its quick + // bar and filter object. + if (ALFloaterSceneExplorer* explorer = + LLFloaterReg::findTypedInstance("scene_explorer")) + { + explorer->refreshFilters(); + } +} + +void ALFloaterSceneExplorerFilters::onClickReset() +{ + // The button promises a full reset (see tool_tip). The explorer owns the + // canonical "reset all" -- it also clears the quick-bar search/owner state + // -- so delegate when it is open. When it is not, fall back to resetting + // the shared settings ourselves so the persisted filter set still returns + // to defaults. + if (ALFloaterSceneExplorer* explorer = + LLFloaterReg::findTypedInstance("scene_explorer")) + { + explorer->doResetFilters(); // refreshes our controls via refreshFromSettings() + } + else + { + gSavedSettings.setU32("ALSceneExplorerFlagFilter", 0); + gSavedSettings.setU32("ALSceneExplorerTypeFilter", 0); + gSavedSettings.setU32("ALSceneExplorerScope", (U32)ALSceneExplorerFilter::SCOPE_REGION); + gSavedSettings.getControl("ALSceneExplorerRadius")->resetToDefault(); + gSavedSettings.setF32("ALSceneExplorerMinLandImpact", 0.f); + gSavedSettings.setU32("ALSceneExplorerMinTriangles", 0); + refreshFromSettings(); + } +} diff --git a/indra/newview/alfloatersceneexplorerfilters.h b/indra/newview/alfloatersceneexplorerfilters.h new file mode 100644 index 0000000000..e54ca1df2d --- /dev/null +++ b/indra/newview/alfloatersceneexplorerfilters.h @@ -0,0 +1,41 @@ +/** + * @file alfloatersceneexplorerfilters.h + * @brief Companion filters floater for the Scene Explorer (the inventory + * filter-finder pattern): the full predicate set, too large for the + * explorer's quick bar. + * + * Copyright (c) 2026, Rye Mutt + * + * The source code in this file is provided to you under the terms of the + * GNU Lesser General Public License, version 2.1, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. Terms of the LGPL can be found in doc/LGPL-licence.txt + * in this distribution, or online at http://www.gnu.org/licenses/lgpl-2.1.txt + * + */ +#ifndef AL_FLOATERSCENEEXPLORERFILTERS_H +#define AL_FLOATERSCENEEXPLORERFILTERS_H + +#include "llfloater.h" + +class ALFloaterSceneExplorerFilters final : public LLFloater +{ + friend class LLFloaterReg; +public: + bool postBuild() override; + void onOpen(const LLSD& key) override; + + // Persisted settings -> controls (also called by the explorer's Reset). + void refreshFromSettings(); + +private: + ALFloaterSceneExplorerFilters(const LLSD& key); + ~ALFloaterSceneExplorerFilters() override = default; + + // Controls -> persisted settings -> explorer filter refresh. The settings + // are the single source of truth shared with the explorer's quick bar. + void onCommitAny(); + void onClickReset(); +}; + +#endif // AL_FLOATERSCENEEXPLORERFILTERS_H diff --git a/indra/newview/alobjectproperties.cpp b/indra/newview/alobjectproperties.cpp new file mode 100644 index 0000000000..ceee3f61f9 --- /dev/null +++ b/indra/newview/alobjectproperties.cpp @@ -0,0 +1,317 @@ +/** + * @file alobjectproperties.cpp + * @brief Shared per-object metric record for the Scene Explorer, Inspect and detail views + * + * Copyright (c) 2026, Rye Mutt + * + * The source code in this file is provided to you under the terms of the + * GNU Lesser General Public License, version 2.1, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. Terms of the LGPL can be found in doc/LGPL-licence.txt + * in this distribution, or online at http://www.gnu.org/licenses/lgpl-2.1.txt + * + */ + +#include "llviewerprecompiledheaders.h" + +#include "alobjectproperties.h" + +#include "message.h" + +#include "llagent.h" +#include "llprimitive.h" +#include "llselectmgr.h" +#include "lltextureentry.h" +#include "llviewerobject.h" +#include "llviewerregion.h" +#include "llvovolume.h" + +namespace ALObjectProperties +{ + +void fillFromObject(Record& rec, LLViewerObject* obj, bool fetch_costs) +{ + if (!obj) + return; + + rec.mLocalId = obj->getLocalID(); + if (LLViewerRegion* regionp = obj->getRegion()) + { + rec.mRegionHandle = regionp->getHandle(); + } + + // Spatial + rec.mPosRegion = obj->getPositionRegion(); + rec.mPosGlobal = obj->getPositionGlobal(); + rec.mScale = obj->getScale(); + rec.mDistance = (F32)(rec.mPosGlobal - gAgent.getPositionGlobal()).magVec(); + + // Structure / geometry + rec.mPrimCount = obj->numChildren() + 1; + rec.mNumFaces = obj->getNumFaces(); + rec.mNumVertices = obj->getNumVertices(); + rec.mNumTriangles = obj->getNumIndices() / 3; + + U32 flags = FLAG_NONE; + + if (obj->asAvatar()) + { + rec.mGeom = GEOM_AVATAR; + flags |= FLAG_AVATAR; + } + else if (obj->isMesh()) + { + rec.mGeom = GEOM_MESH; + } + else if (obj->isSculpted()) + { + rec.mGeom = GEOM_SCULPT; + } + else + { + rec.mGeom = GEOM_PRIM; + } + + if (obj->flagScripted()) flags |= FLAG_SCRIPTED; + if (obj->flagUsePhysics()) flags |= FLAG_PHYSICS; + if (obj->flagPhantom()) flags |= FLAG_PHANTOM; + if (obj->flagTemporaryOnRez()) flags |= FLAG_TEMPORARY; + if (obj->isFlexible()) flags |= FLAG_FLEXIBLE; + if (obj->hasLightTexture()) flags |= FLAG_PROJECTOR; + if (obj->isReflectionProbe()) flags |= FLAG_REFLECTION_PROBE; + if (obj->isParticleSource()) flags |= FLAG_PARTICLES; + if (obj->isAttachment()) flags |= FLAG_ATTACHMENT; + if (obj->isAnimatedObject()) flags |= FLAG_ANIMATED; + if (obj->flagTakesMoney()) flags |= FLAG_PAYABLE; + if (obj->hasRenderMaterialParams()) flags |= FLAG_PBR_MATERIAL; + + // Volume-only features. + if (obj->getPCode() == LL_PCODE_VOLUME) + { + LLVOVolume* vobj = static_cast(obj); + + if (vobj->getIsLight()) + { + flags |= FLAG_LIGHT; + if (vobj->isLightSpotlight()) + flags |= FLAG_SPOTLIGHT; + } + if (vobj->hasMedia()) + flags |= FLAG_MEDIA; + + LLVOVolume::texture_cost_t textures; + rec.mRenderCost = (F32)vobj->getRenderCost(textures); + } + + // Per-face appearance flags. + const U8 num_tes = obj->getNumTEs(); + for (U8 i = 0; i < num_tes; ++i) + { + const LLTextureEntry* te = obj->getTE(i); + if (!te) + continue; + if (te->getFullbright()) + flags |= FLAG_FULLBRIGHT; + if (te->getGlow() > 0.f) + flags |= FLAG_GLOW; + if (te->getColor().mV[VALPHA] < 0.999f) + flags |= FLAG_ALPHA; + } + + // FLAG_FOR_SALE comes from the async server props (sale info), so carry + // the previous merge over local rebuilds. + flags |= (rec.mFlags & FLAG_FOR_SALE); + rec.mFlags = flags; + + // Cost: cached values either way; the triggering getters additionally + // queue an async refresh when the cached value is stale. + if (fetch_costs) + { + rec.mObjectCost = obj->getObjectCost(); + rec.mLandImpact = obj->getLinksetCost(); + rec.mPhysicsCost = obj->getPhysicsCost(); + } + else + { + rec.mObjectCost = obj->peekObjectCost(); + rec.mLandImpact = obj->peekLinksetCost(); + rec.mPhysicsCost = obj->peekPhysicsCost(); + } + rec.mStreamingCost = obj->getStreamingCost(); +} + +Record fromObject(LLViewerObject* obj, bool fetch_costs) +{ + Record rec; + if (obj) + { + rec.mId = obj->getID(); + fillFromObject(rec, obj, fetch_costs); + } + return rec; +} + +std::string flagsToString(U32 flags) +{ + static const std::pair labels[] = { + { FLAG_SCRIPTED, "scripted" }, + { FLAG_PHYSICS, "physics" }, + { FLAG_PHANTOM, "phantom" }, + { FLAG_TEMPORARY, "temporary" }, + { FLAG_FLEXIBLE, "flexi" }, + { FLAG_LIGHT, "light" }, + { FLAG_SPOTLIGHT, "spotlight" }, + { FLAG_PROJECTOR, "projector" }, + { FLAG_GLOW, "glow" }, + { FLAG_FULLBRIGHT, "fullbright" }, + { FLAG_MEDIA, "media" }, + { FLAG_PARTICLES, "particles" }, + { FLAG_REFLECTION_PROBE, "probe" }, + { FLAG_ANIMATED, "animated" }, + { FLAG_ATTACHMENT, "attachment" }, + { FLAG_PBR_MATERIAL, "PBR" }, + { FLAG_ALPHA, "alpha" }, + { FLAG_PAYABLE, "payable" }, + { FLAG_FOR_SALE, "for sale" }, + }; + + std::string out; + for (const auto& [bit, name] : labels) + { + if (flags & bit) + { + if (!out.empty()) + out += ", "; + out += name; + } + } + return out; +} + +const std::string& iconName(const Record& rec) +{ + static const std::string s_object("Inv_Object"); + static const std::string s_multi("Inv_Object_Multi"); + static const std::string s_mesh("Inv_Mesh"); + static const std::string s_avatar("Generic_Person"); + + if (rec.mGeom == GEOM_AVATAR) + return s_avatar; + // Multi-prim linksets read as "multi" regardless of geometry; single prims + // split mesh vs everything else. + if (rec.mPrimCount > 1) + return s_multi; + if (rec.mGeom == GEOM_MESH) + return s_mesh; + return s_object; +} + +} // namespace ALObjectProperties + +// ============================================================================ +// ALObjectPropertiesCache +// ============================================================================ +ALObjectPropertiesCache::ALObjectPropertiesCache() +{ +} + +ALObjectPropertiesCache::~ALObjectPropertiesCache() +{ +} + +const ALObjectPropertiesCache::ServerProps* ALObjectPropertiesCache::get(const LLUUID& id) const +{ + auto it = mCache.find(id); + return (it != mCache.end()) ? &it->second : nullptr; +} + +ALObjectPropertiesCache::ServerProps& ALObjectPropertiesCache::lookupOrCreate(const LLUUID& id) +{ + auto found = mCache.find(id); + if (found != mCache.end()) + return found->second; + + ServerProps& p = mCache[id]; + mCacheOrder.push_back(id); + trimCache(); // evicts from the front, never the entry just added + return p; +} + +void ALObjectPropertiesCache::trimCache() +{ + while (mCache.size() > MAX_CACHE_ENTRIES && !mCacheOrder.empty()) + { + mCache.erase(mCacheOrder.front()); + mCacheOrder.pop_front(); + } +} + +// static +void ALObjectPropertiesCache::processObjectPropertiesFamily(LLMessageSystem* msg, void** user_data) +{ + LLUUID id; + msg->getUUIDFast(_PREHASH_ObjectData, _PREHASH_ObjectID, id); + if (id.notNull()) + { + ALObjectPropertiesCache& self = instance(); + ServerProps& p = self.lookupOrCreate(id); + msg->getUUIDFast(_PREHASH_ObjectData, _PREHASH_OwnerID, p.mOwnerId); + msg->getUUIDFast(_PREHASH_ObjectData, _PREHASH_GroupID, p.mGroupId); + msg->getU8Fast(_PREHASH_ObjectData, _PREHASH_SaleType, p.mSaleType); + msg->getS32Fast(_PREHASH_ObjectData, _PREHASH_SalePrice, p.mSalePrice); + msg->getStringFast(_PREHASH_ObjectData, _PREHASH_Name, p.mName); + msg->getStringFast(_PREHASH_ObjectData, _PREHASH_Description, p.mDescription); + p.mGroupOwned = p.mOwnerId.isNull() && p.mGroupId.notNull(); + self.mChangeSignal(id); + } + + // Preserve normal hover/selection behaviour. + LLSelectMgr::processObjectPropertiesFamily(msg, user_data); +} + +// static +void ALObjectPropertiesCache::processObjectProperties(LLMessageSystem* msg, void** user_data) +{ + ALObjectPropertiesCache& self = instance(); + const S32 count = msg->getNumberOfBlocksFast(_PREHASH_ObjectData); + uuid_vec_t ids; + ids.reserve(count); + for (S32 i = 0; i < count; ++i) + { + LLUUID id; + msg->getUUIDFast(_PREHASH_ObjectData, _PREHASH_ObjectID, id, i); + if (id.isNull()) + continue; + + ServerProps& p = self.lookupOrCreate(id); + msg->getUUIDFast(_PREHASH_ObjectData, _PREHASH_CreatorID, p.mCreatorId, i); + msg->getUUIDFast(_PREHASH_ObjectData, _PREHASH_OwnerID, p.mOwnerId, i); + msg->getUUIDFast(_PREHASH_ObjectData, _PREHASH_GroupID, p.mGroupId, i); + U64 creation_date = 0; + msg->getU64Fast(_PREHASH_ObjectData, _PREHASH_CreationDate, creation_date, i); + p.mCreationDate = (time_t)(creation_date / 1000000); + msg->getU32Fast(_PREHASH_ObjectData, _PREHASH_BaseMask, p.mBaseMask, i); + msg->getU32Fast(_PREHASH_ObjectData, _PREHASH_OwnerMask, p.mOwnerMask, i); + msg->getU32Fast(_PREHASH_ObjectData, _PREHASH_GroupMask, p.mGroupMask, i); + msg->getU32Fast(_PREHASH_ObjectData, _PREHASH_EveryoneMask, p.mEveryoneMask, i); + msg->getU32Fast(_PREHASH_ObjectData, _PREHASH_NextOwnerMask, p.mNextOwnerMask, i); + msg->getU8Fast(_PREHASH_ObjectData, _PREHASH_SaleType, p.mSaleType, i); + msg->getS32Fast(_PREHASH_ObjectData, _PREHASH_SalePrice, p.mSalePrice, i); + msg->getStringFast(_PREHASH_ObjectData, _PREHASH_Name, p.mName, i); + msg->getStringFast(_PREHASH_ObjectData, _PREHASH_Description, p.mDescription, i); + p.mGroupOwned = p.mOwnerId.isNull() && p.mGroupId.notNull(); + p.mHasFullData = true; + ids.push_back(id); + self.mChangeSignal(id); + } + + // Preserve normal selection / inspect / build behaviour. Forward before + // retiring the in-flight markers: LLSelectMgr consults isExpectedReply() + // to decide whether a reply block with no select node warrants a warning. + LLSelectMgr::processObjectProperties(msg, user_data); + + for (const LLUUID& id : ids) + { + self.mPendingRequests.erase(id); + } +} diff --git a/indra/newview/alobjectproperties.h b/indra/newview/alobjectproperties.h new file mode 100644 index 0000000000..cbd3fe5206 --- /dev/null +++ b/indra/newview/alobjectproperties.h @@ -0,0 +1,235 @@ +/** + * @file alobjectproperties.h + * @brief Shared per-object metric record for the Scene Explorer, Inspect and detail views + * + * Copyright (c) 2026, Rye Mutt + * + * The source code in this file is provided to you under the terms of the + * GNU Lesser General Public License, version 2.1, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. Terms of the LGPL can be found in doc/LGPL-licence.txt + * in this distribution, or online at http://www.gnu.org/licenses/lgpl-2.1.txt + * + */ +#ifndef AL_OBJECTPROPERTIES_H +#define AL_OBJECTPROPERTIES_H + +#include + +#include +#include +#include + +#include "llsingleton.h" +#include "lluuid.h" +#include "v3math.h" +#include "v3dmath.h" + +class LLMessageSystem; +class LLViewerObject; + +// ============================================================================ +// ALObjectProperties - a flat, copyable snapshot of an object's properties. +// +// Local (geometry / flag / cost) fields are filled synchronously from a live +// LLViewerObject via fillFromObject(). Server fields (name/owner/creator/desc/ +// dates) arrive asynchronously and are merged by the owning view. +// ============================================================================ + +namespace ALObjectProperties +{ + // Geometry kind, used to pick the row icon. + enum EGeom : U8 + { + GEOM_PRIM = 0, + GEOM_MESH, + GEOM_SCULPT, + GEOM_AVATAR, + GEOM_UNKNOWN + }; + + // Feature flags. Independent of EGeom (an object can be e.g. a scripted, + // glowing, light-emitting mesh). Used by the Scene Explorer filter. + enum EFlag : U32 + { + FLAG_NONE = 0, + FLAG_SCRIPTED = 1 << 0, + FLAG_PHYSICS = 1 << 1, + FLAG_PHANTOM = 1 << 2, + FLAG_TEMPORARY = 1 << 3, + FLAG_FLEXIBLE = 1 << 4, + FLAG_LIGHT = 1 << 5, + FLAG_SPOTLIGHT = 1 << 6, // light with a projector cone + FLAG_PROJECTOR = 1 << 7, // has a projection texture + FLAG_GLOW = 1 << 8, + FLAG_FULLBRIGHT = 1 << 9, + FLAG_MEDIA = 1 << 10, + FLAG_PARTICLES = 1 << 11, + FLAG_REFLECTION_PROBE = 1 << 12, + FLAG_ANIMATED = 1 << 13, // animated (animesh) object + FLAG_ATTACHMENT = 1 << 14, + FLAG_PBR_MATERIAL = 1 << 15, // has a GLTF render material on any face + FLAG_ALPHA = 1 << 16, // any face is alpha-blended / transparent + FLAG_AVATAR = 1 << 17, + FLAG_PAYABLE = 1 << 18, // pays a script on payment (takes money) + FLAG_FOR_SALE = 1 << 19 // server sale info (set when props arrive) + }; + + struct Record + { + // Identity + LLUUID mId; + U32 mLocalId = 0; + U64 mRegionHandle = 0; + + // Spatial (local, live) + LLVector3 mPosRegion; + LLVector3d mPosGlobal; + LLVector3 mScale; + F32 mDistance = 0.f; + + // Structure / geometry (local, live) + EGeom mGeom = GEOM_UNKNOWN; + U32 mFlags = FLAG_NONE; + S32 mPrimCount = 1; // linkset prim count for a root + S32 mNumFaces = 0; + U32 mNumVertices = 0; + U32 mNumTriangles = 0; + + // Cost (local cached; populates over time) + F32 mLandImpact = 0.f; // linkset cost for a root object + F32 mObjectCost = 0.f; + F32 mPhysicsCost = 0.f; + F32 mRenderCost = 0.f; // "ARC" + F32 mStreamingCost = 0.f; + + // Server props (async; merged by the view) + bool mPropsValid = false; + std::string mName; + std::string mDescription; + LLUUID mOwnerId; + LLUUID mGroupId; + LLUUID mCreatorId; + bool mGroupOwned = false; + time_t mCreationDate = 0; + U8 mSaleType = 0; // LLSaleInfo::EForSale; 0 = not for sale + S32 mSalePrice = 0; + + bool hasFlag(EFlag f) const { return (mFlags & f) != 0; } + }; + + // Fill the local (non-server) fields of @rec from a live object. The + // identity and server fields are left untouched so a cached record keeps + // its async data across refreshes. With @fetch_costs the cost reads go + // through the triggering getters (queueing an async GetObjectCost refresh + // when stale) — pass false anywhere costs must stay demand-driven, e.g. + // bulk node builds; the cached values are still peeked either way. + void fillFromObject(Record& rec, LLViewerObject* obj, bool fetch_costs = true); + + // Convenience: build a fresh record (identity + local fields) for @obj. + Record fromObject(LLViewerObject* obj, bool fetch_costs = true); + + // A compact human-readable flag list for a row suffix / tooltip, e.g. + // "scripted, light, glow". Empty when no notable flags set. + std::string flagsToString(U32 flags); + + // UI image name for a record's geometry kind. + const std::string& iconName(const Record& rec); +} + +// ============================================================================ +// ALObjectPropertiesCache +// +// Session-lifetime cache of server-provided object properties (name, owner, +// group, creator, description, creation date), keyed by object UUID. Populated +// by the ObjectProperties / ObjectPropertiesFamily message handlers, which are +// registered in llstartup.cpp ahead of LLSelectMgr's and forward to it so all +// existing selection behaviour is preserved. Consumers (Scene Explorer, detail +// pane, ...) read from here and subscribe for change notifications instead of +// having to be in a selection. +// +// The cache deliberately PERSISTS across region crossings: entries are keyed +// by globally-unique object UUID, so crossing a border doesn't stale them — +// and wiping would make every border hop re-probe the whole region. Growth is +// bounded by oldest-first eviction instead (insertion order roughly equals +// region visit order, so stale regions age out first). +// ============================================================================ +class ALObjectPropertiesCache : public LLSingleton +{ + LLSINGLETON(ALObjectPropertiesCache); + ~ALObjectPropertiesCache(); +public: + struct ServerProps + { + std::string mName; + std::string mDescription; + LLUUID mOwnerId; + LLUUID mGroupId; + LLUUID mCreatorId; + bool mGroupOwned = false; + time_t mCreationDate = 0; + // Sale info (both reply flavours carry it). + U8 mSaleType = 0; // LLSaleInfo::EForSale; 0 = not for sale + S32 mSalePrice = 0; + // Permission masks (full ObjectProperties only). + U32 mBaseMask = 0; + U32 mOwnerMask = 0; + U32 mGroupMask = 0; + U32 mEveryoneMask = 0; + U32 mNextOwnerMask = 0; + bool mHasFullData = false; // creator/date/perms came from full ObjectProperties + }; + + // Message handlers (registered in llstartup.cpp). Each caches the relevant + // fields, notifies listeners, then forwards to the matching LLSelectMgr + // handler so normal selection / inspect / build behaviour is unaffected. + static void processObjectProperties(LLMessageSystem* msg, void** user_data); + static void processObjectPropertiesFamily(LLMessageSystem* msg, void** user_data); + + // Returns the cached entry for @id, or nullptr if nothing received yet. + const ServerProps* get(const LLUUID& id) const; + + typedef boost::signals2::signal change_signal_t; + boost::signals2::connection setChangeCallback(const change_signal_t::slot_type& cb) + { + return mChangeSignal.connect(cb); + } + + // In-flight request tracking. The Scene Explorer marks each object id it + // probes via bulk select/deselect; the id is retired when the full + // ObjectProperties reply lands (or explicitly, when the requester decides + // the reply was lost and re-requests). LLSelectMgr consults this to avoid + // warning about reply blocks that legitimately match no select node — + // including replies that arrive after the requesting floater has closed. + void markPending(const LLUUID& id) { mPendingRequests.insert(id); } + void clearPending(const LLUUID& id) { mPendingRequests.erase(id); } + bool isPending(const LLUUID& id) const { return mPendingRequests.count(id) != 0; } + static bool isExpectedReply(const LLUUID& id) + { + return instanceExists() && instance().isPending(id); + } + + // Drop everything (explicit refresh only — see the class comment for why + // region crossings must NOT clear). + void clear() + { + mCache.clear(); + mCacheOrder.clear(); + mPendingRequests.clear(); + } + +private: + // Bounded growth: once past the cap, evict oldest insertions first. The + // cap comfortably exceeds a maximal (~60k object) region so a single + // region never churns against it. + static constexpr size_t MAX_CACHE_ENTRIES = 100000; + ServerProps& lookupOrCreate(const LLUUID& id); + void trimCache(); + + boost::unordered_map mCache; + std::deque mCacheOrder; // insertion order, for eviction + boost::unordered_set mPendingRequests; + change_signal_t mChangeSignal; +}; + +#endif // AL_OBJECTPROPERTIES_H diff --git a/indra/newview/alsceneexplorermodel.cpp b/indra/newview/alsceneexplorermodel.cpp new file mode 100644 index 0000000000..06f4e0f9b6 --- /dev/null +++ b/indra/newview/alsceneexplorermodel.cpp @@ -0,0 +1,693 @@ +/** + * @file alsceneexplorermodel.cpp + * @brief Folder-view model for the Scene Explorer tree + * + * Copyright (c) 2026, Rye Mutt + * + * The source code in this file is provided to you under the terms of the + * GNU Lesser General Public License, version 2.1, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. Terms of the LGPL can be found in doc/LGPL-licence.txt + * in this distribution, or online at http://www.gnu.org/licenses/lgpl-2.1.txt + * + */ + +#include "llviewerprecompiledheaders.h" + +#include "alsceneexplorermodel.h" + +#include "alfloatersceneexplorer.h" +#include "llagent.h" +#include "lltooltip.h" +#include "llui.h" +#include "llviewercontrol.h" +#include "llviewerparcelmgr.h" + +// ============================================================================ +// ALSceneExplorerSort +// ============================================================================ +bool ALSceneExplorerSort::operator()(const ALSceneExplorerItem* a, const ALSceneExplorerItem* b) const +{ + if (!a || !b) + return false; + + // Container nodes (region / categories / attachment points) keep a fixed + // structural order. Sibling attachment-point folders share a type, so order + // those alphabetically by point name ("Chest", "Right Hand", ...). + if (a->isContainer() || b->isContainer()) + { + if (a->getItemType() == ALSceneExplorerItem::TYPE_ATTACHMENT_POINT + && b->getItemType() == ALSceneExplorerItem::TYPE_ATTACHMENT_POINT) + { + return LLStringUtil::compareInsensitive(a->getName(), b->getName()) < 0; + } + return a->getItemType() < b->getItemType(); + } + + // Child prims of a linkset keep their native link order so a linkset's + // contents read top-down as they're linked, not scattered by distance/name. + // Every other node here is a root-level object (world linkset, single prim, + // avatar, or attachment root) and honours the active sort key. Siblings are + // always the same kind (the comparator only sees one folder's children). + if (a->getItemType() == ALSceneExplorerItem::TYPE_PRIM) + return a->getLinkOrder() < b->getLinkOrder(); + + const ALObjectProperties::Record& ra = a->getRecord(); + const ALObjectProperties::Record& rb = b->getRecord(); + + switch (mMode) + { + case SORT_NAME: + break; + case SORT_LAND_IMPACT: + if (ra.mLandImpact != rb.mLandImpact) + return ra.mLandImpact > rb.mLandImpact; + break; + case SORT_TRIANGLES: + if (ra.mNumTriangles != rb.mNumTriangles) + return ra.mNumTriangles > rb.mNumTriangles; + break; + case SORT_TYPE: + if (ra.mGeom != rb.mGeom) + return ra.mGeom < rb.mGeom; + break; + case SORT_REGION_ORIGIN: + { + // Agent-independent ordering for builders: distance from the region's + // <0,0,0>, computed from the live region position. + const F32 da = ra.mPosRegion.magVecSquared(); + const F32 db = rb.mPosRegion.magVecSquared(); + if (da != db) + return da < db; + break; + } + case SORT_DISTANCE: + default: + if (ra.mDistance != rb.mDistance) + return ra.mDistance < rb.mDistance; + break; + } + + return LLStringUtil::compareInsensitive(a->getName(), b->getName()) < 0; +} + +// ============================================================================ +// ALSceneExplorerFilter +// ============================================================================ + +// The UI-facing enums must mirror the pure predicate's numeric twins; the +// floater persists and casts the raw values. +static_assert((U32)ALSceneExplorerFilter::SEARCH_NAME == (U32)ALSceneExplorerPredicate::SEARCH_NAME); +static_assert((U32)ALSceneExplorerFilter::SEARCH_DESCRIPTION == (U32)ALSceneExplorerPredicate::SEARCH_DESCRIPTION); +static_assert((U32)ALSceneExplorerFilter::SEARCH_OWNER == (U32)ALSceneExplorerPredicate::SEARCH_OWNER); +static_assert((U32)ALSceneExplorerFilter::SEARCH_UUID == (U32)ALSceneExplorerPredicate::SEARCH_UUID); +static_assert((U32)ALSceneExplorerFilter::SEARCH_ALL == (U32)ALSceneExplorerPredicate::SEARCH_ALL); +static_assert((U32)ALSceneExplorerFilter::OWNER_ANY == (U32)ALSceneExplorerPredicate::OWNER_ANY); +static_assert((U32)ALSceneExplorerFilter::OWNER_MINE == (U32)ALSceneExplorerPredicate::OWNER_MINE); +static_assert((U32)ALSceneExplorerFilter::OWNER_GROUP == (U32)ALSceneExplorerPredicate::OWNER_GROUP); +static_assert((U32)ALSceneExplorerFilter::OWNER_OTHERS == (U32)ALSceneExplorerPredicate::OWNER_OTHERS); +static_assert((U32)ALSceneExplorerFilter::OWNER_SPECIFIC == (U32)ALSceneExplorerPredicate::OWNER_SPECIFIC); +static_assert((U32)ALSceneExplorerFilter::SCOPE_REGION == (U32)ALSceneExplorerPredicate::SCOPE_REGION); +static_assert((U32)ALSceneExplorerFilter::SCOPE_PARCEL == (U32)ALSceneExplorerPredicate::SCOPE_PARCEL); +static_assert((U32)ALSceneExplorerFilter::SCOPE_RADIUS == (U32)ALSceneExplorerPredicate::SCOPE_RADIUS); + +ALSceneExplorerFilter::ALSceneExplorerFilter() +: mName("scene_explorer") +{ +} + +void ALSceneExplorerFilter::applyChange(ALSceneExplorerPredicate::EFilterChange change) +{ + switch (change) + { + case ALSceneExplorerPredicate::CHANGE_NONE: + return; + case ALSceneExplorerPredicate::CHANGE_MORE_RESTRICTIVE: + setModified(FILTER_MORE_RESTRICTIVE); + return; + case ALSceneExplorerPredicate::CHANGE_LESS_RESTRICTIVE: + setModified(FILTER_LESS_RESTRICTIVE); + return; + case ALSceneExplorerPredicate::CHANGE_RESTART: + default: + setModified(FILTER_RESTART); + return; + } +} + +void ALSceneExplorerFilter::setFilterSubString(const std::string& string) +{ + std::string lower(string); + LLStringUtil::toLower(lower); + if (lower != mConstraints.mFilterSubString) + { + const ALSceneExplorerPredicate::EFilterChange change = + ALSceneExplorerPredicate::classifySubstringChange(mConstraints.mFilterSubString, lower); + mConstraints.mFilterSubString = lower; + applyChange(change); + } +} + +void ALSceneExplorerFilter::setSearchType(ESearchType type) +{ + if ((U32)type != mConstraints.mSearchType) + { + mConstraints.mSearchType = (U32)type; + // A different field is an unrelated predicate; only matters while a + // text predicate is set. + if (!mConstraints.mFilterSubString.empty()) + setModified(FILTER_RESTART); + } +} + +void ALSceneExplorerFilter::setOwnerMode(EOwnerMode mode) +{ + if ((U32)mode != mConstraints.mOwnerMode) + { + // ANY passes everything, so entering a mode only narrows and leaving + // one only widens; switching between two real modes is unrelated. + const bool was_any = (mConstraints.mOwnerMode == (U32)OWNER_ANY); + mConstraints.mOwnerMode = (U32)mode; + if (was_any) + setModified(FILTER_MORE_RESTRICTIVE); + else if (mode == OWNER_ANY) + setModified(FILTER_LESS_RESTRICTIVE); + else + setModified(FILTER_RESTART); + } +} + +void ALSceneExplorerFilter::setOwnerId(const LLUUID& id) +{ + if (id != mConstraints.mOwnerId) + { + mConstraints.mOwnerId = id; + // A different target invalidates passes and fails alike. + if (mConstraints.mOwnerMode == (U32)OWNER_SPECIFIC) + setModified(FILTER_RESTART); + } +} + +void ALSceneExplorerFilter::setGeomMask(U32 mask) +{ + if (mask != mConstraints.mGeomMask) + { + const ALSceneExplorerPredicate::EFilterChange change = + ALSceneExplorerPredicate::classifyAllowedMaskChange(mConstraints.mGeomMask, mask); + mConstraints.mGeomMask = mask; + applyChange(change); + } +} + +void ALSceneExplorerFilter::setFlagMask(U32 mask) +{ + if (mask != mConstraints.mFlagMask) + { + const ALSceneExplorerPredicate::EFilterChange change = + ALSceneExplorerPredicate::classifyRequireAllMaskChange(mConstraints.mFlagMask, mask); + mConstraints.mFlagMask = mask; + applyChange(change); + } +} + +void ALSceneExplorerFilter::setScope(EScope scope, F32 radius) +{ + const EScope old_scope = (EScope)mConstraints.mScope; + const F32 old_radius = mConstraints.mRadius; + mConstraints.mScope = (U32)scope; + mConstraints.mRadius = radius; // kept current even while the scope is off + + if (scope == old_scope) + { + // Same scope: only a radius change while the radius scope is active + // affects results, and it is monotonic. + if (scope == SCOPE_RADIUS && radius != old_radius) + { + setModified(radius < old_radius ? FILTER_MORE_RESTRICTIVE + : FILTER_LESS_RESTRICTIVE); + } + return; + } + // REGION passes everything spatial, so entering a spatial scope narrows + // and leaving one widens; switching between parcel and radius is + // unrelated. + if (old_scope == SCOPE_REGION) + setModified(FILTER_MORE_RESTRICTIVE); + else if (scope == SCOPE_REGION) + setModified(FILTER_LESS_RESTRICTIVE); + else + setModified(FILTER_RESTART); +} + +void ALSceneExplorerFilter::setMinLandImpact(F32 min_li) +{ + if (min_li != mConstraints.mMinLandImpact) + { + const ALSceneExplorerPredicate::EFilterChange change = + ALSceneExplorerPredicate::classifyMinThresholdChange(mConstraints.mMinLandImpact, min_li); + mConstraints.mMinLandImpact = min_li; + applyChange(change); + } +} + +void ALSceneExplorerFilter::setMinTriangles(U32 min_tris) +{ + if (min_tris != mConstraints.mMinTriangles) + { + const ALSceneExplorerPredicate::EFilterChange change = + ALSceneExplorerPredicate::classifyMinThresholdChange( + (F32)mConstraints.mMinTriangles, (F32)min_tris); + mConstraints.mMinTriangles = min_tris; + applyChange(change); + } +} + +bool ALSceneExplorerFilter::isActive() const +{ + return !mConstraints.mFilterSubString.empty() + || mConstraints.mOwnerMode != (U32)OWNER_ANY + || mConstraints.mGeomMask != 0 + || mConstraints.mFlagMask != 0 + || mConstraints.mScope != (U32)SCOPE_REGION + || mConstraints.mMinLandImpact > 0.f + || mConstraints.mMinTriangles > 0; +} + +void ALSceneExplorerFilter::setModified(EFilterModified behavior) +{ + mModified = true; + ++mCurrentGeneration; + + // Merge with whatever kind of change is already pending this refilter: + // two different kinds in one batch degrade to a restart (inventory's + // rule). clearModified() resets the batch once the refilter completes. + if (mFilterModified == FILTER_NONE) + mFilterModified = behavior; + else if (mFilterModified != behavior) + mFilterModified = FILTER_RESTART; + + switch (mFilterModified) + { + case FILTER_MORE_RESTRICTIVE: + // Old passes need revalidating; old fails certainly still fail, so + // first-required stays put and they can be re-stamped cheaply. + mFirstSuccessGeneration = mCurrentGeneration; + break; + case FILTER_LESS_RESTRICTIVE: + // Old fails need revalidating; old passes certainly still pass, so + // first-success stays put and the visible set never blanks out. + mFirstRequiredGeneration = mCurrentGeneration; + break; + case FILTER_RESTART: + default: + mFirstRequiredGeneration = mCurrentGeneration; + mFirstSuccessGeneration = mCurrentGeneration; + break; + } +} + +std::string::size_type ALSceneExplorerFilter::getStringMatchOffset(LLFolderViewModelItem* item) const +{ + if (mConstraints.mFilterSubString.empty()) + return std::string::npos; + // Only a name match can be highlighted: the widget draws the highlight at + // this offset within its displayed label, and the name is the only + // searched field the label shows (description/UUID/owner matches return + // npos). The lowercased search copy is byte-length-identical to the + // display name, so the offset maps straight onto the label — matching + // LLInventoryFilter's behaviour, including its non-ASCII imprecision. + if (mConstraints.mSearchType != (U32)SEARCH_NAME + && mConstraints.mSearchType != (U32)SEARCH_ALL) + { + return std::string::npos; + } + return static_cast(item)->searchName().find(mConstraints.mFilterSubString); +} + +bool ALSceneExplorerFilter::check(const LLFolderViewModelItem* item) +{ + if (!isActive()) + return true; + + const ALSceneExplorerItem* sit = static_cast(item); + if (!sit || sit->isContainer()) + return false; // containers are only shown through matching descendants + + // Gather the per-item facts (including the impure ones the pure predicate + // can't derive) and delegate to the unit-tested core. + const ALObjectProperties::Record& rec = sit->getRecord(); + ALSceneExplorerPredicate::ItemFacts facts; + facts.mRecord = &rec; + facts.mSearchName = &sit->searchName(); + facts.mSearchDesc = &sit->searchDesc(); + facts.mSearchUUID = &sit->searchUUID(); + facts.mSearchOwner = &sit->searchOwner(); + facts.mItemId = sit->getUUID(); + facts.mIsAvatar = (sit->getItemType() == ALSceneExplorerItem::TYPE_AVATAR); + facts.mIsChildPrim = (sit->getItemType() == ALSceneExplorerItem::TYPE_PRIM); + facts.mInAgentParcel = mConstraints.mScope != (U32)SCOPE_PARCEL + || LLViewerParcelMgr::getInstance()->inAgentParcel(rec.mPosGlobal); + mConstraints.mAgentId = gAgentID; + + return ALSceneExplorerPredicate::matches(facts, mConstraints); +} + +// ============================================================================ +// ALSceneExplorerItem +// ============================================================================ +ALSceneExplorerItem::ALSceneExplorerItem(EItemType type, const LLUUID& id, const std::string& name, + LLFolderViewModelInterface& root_view_model, ALFloaterSceneExplorer* floater) +: LLFolderViewModelItemCommon(root_view_model), + mItemType(type), + mUUID(id), + mName(name), + mSearchUUID(id.asString()), // never changes; asString() is lowercase hex + mFloater(floater) +{ + mRecord.mId = id; + rebuildSearchable(); +} + +void ALSceneExplorerItem::setName(const std::string& name) +{ + if (name != mName) + { + mName = name; + rebuildSearchable(); + dirtyFilter(); + } +} + +void ALSceneExplorerItem::updateRecord(const ALObjectProperties::Record& rec) +{ + updateRecord(rec, LLStringUtil::null); +} + +void ALSceneExplorerItem::updateRecord(const ALObjectProperties::Record& rec, const std::string& display_name) +{ + mRecord = rec; + mRecord.mId = mUUID; + if (!display_name.empty()) + { + mName = display_name; + } + rebuildSearchable(); + dirtyFilter(); +} + +void ALSceneExplorerItem::setOwnerName(const std::string& name) +{ + if (name == mOwnerDisplay) + return; + mOwnerDisplay = name; + mOwnerSearch = name; + LLStringUtil::toLower(mOwnerSearch); + rebuildSearchable(); + dirtyFilter(); +} + +void ALSceneExplorerItem::rebuildSearchable() +{ + mSearchName = mName; + LLStringUtil::toLower(mSearchName); + mSearchDesc = mRecord.mDescription; + LLStringUtil::toLower(mSearchDesc); + // mSearchUUID is fixed at construction; mOwnerSearch is set by the + // floater's owner-name resolution. +} + +LLPointer ALSceneExplorerItem::getIcon() const +{ + return LLUI::getUIImage(ALObjectProperties::iconName(mRecord)); +} + +LLFontGL::StyleFlags ALSceneExplorerItem::getLabelStyle() const +{ + return LLFontGL::NORMAL; +} + +std::string ALSceneExplorerItem::getLabelSuffix() const +{ + if (isContainer()) + return LLStringUtil::null; + + // Derendered entries have no live metrics; objects keep their stored + // position (distance), region-less avatar entries show nothing. + if (mItemType == TYPE_DERENDERED_AVATAR) + return LLStringUtil::null; + if (mItemType == TYPE_DERENDERED_OBJECT) + return llformat("%.0fm", mRecord.mDistance); + + // Avatar rows show render complexity + worn attachment count (stored in + // mRenderCost / mPrimCount by the reconcile pass) instead of object costs. + if (mItemType == TYPE_AVATAR) + { + std::string suffix = llformat("%.0fm", mRecord.mDistance); + if (mRecord.mRenderCost > 0.f) + suffix += llformat(" cmplx %.0fk", mRecord.mRenderCost / 1000.f); + if (mRecord.mPrimCount > 0) + suffix += llformat(" %d att", mRecord.mPrimCount); + return suffix; + } + + std::string suffix = llformat("%.0fm", mRecord.mDistance); + if (mItemType == TYPE_LINKSET && mRecord.mLandImpact > 0.f) + { + suffix += llformat(" LI %.0f", mRecord.mLandImpact); + } + if (mRecord.mNumTriangles > 0) + { + suffix += llformat(" %u tris", mRecord.mNumTriangles); + } + // Owner display name once resolved (toggleable from the eye menu). + static LLCachedControl show_owner(gSavedSettings, "ALSceneExplorerOwnerSuffix", true); + if (show_owner && !mOwnerDisplay.empty()) + { + suffix += " (" + mOwnerDisplay + ")"; + } + return suffix; +} + +std::string ALSceneExplorerItem::getTooltip() const +{ + if (isContainer()) + return mName; + + std::string tip = mName; + if (!mRecord.mDescription.empty()) + tip += "\n" + mRecord.mDescription; + tip += llformat("\nDistance: %.0f m", mRecord.mDistance); + + if (mItemType == TYPE_AVATAR) + { + if (mRecord.mRenderCost > 0.f) + tip += llformat("\nComplexity: %.0f", mRecord.mRenderCost); + if (mRecord.mPrimCount > 0) + tip += llformat("\nAttachments: %d", mRecord.mPrimCount); + return tip; + } + if (isDerenderedType()) + { + tip += "\n(derendered)"; + return tip; + } + + if (mRecord.mPrimCount > 1) + tip += llformat("\nPrims: %d", mRecord.mPrimCount); + if (mRecord.mLandImpact > 0.f) + tip += llformat("\nLand impact: %.0f", mRecord.mLandImpact); + if (mRecord.mRenderCost > 0.f) + tip += llformat("\nRender cost: %.0f", mRecord.mRenderCost); + if (mRecord.mNumTriangles > 0) + tip += llformat("\nTriangles: %u (%u verts, %d faces)", + mRecord.mNumTriangles, mRecord.mNumVertices, mRecord.mNumFaces); + const std::string flags = ALObjectProperties::flagsToString(mRecord.mFlags); + if (!flags.empty()) + tip += "\nFlags: " + flags; + return tip; +} + +void ALSceneExplorerItem::activate() +{ + if (mFloater) + { + mFloater->activateItem(mUUID); + } +} + +void ALSceneExplorerItem::buildContextMenu(LLMenuGL& menu, U32 flags) +{ + if (mFloater) + { + mFloater->buildRowContextMenu(menu, flags); + } +} + +void ALSceneExplorerItem::openItem(void) +{ + // openItem() fires from leaf double-click AND from + // LLFolderViewFolder::setOpenArrangeRecursively whenever a folder is + // expanded (disclosure arrow, keyboard, ancestor auto-open). Expanding a + // linkset must never move the camera or open an editor, so only leaf prims + // activate here; folder-typed scene objects activate from + // ALSceneExplorerFolder::handleDoubleClick. + if (mItemType == TYPE_PRIM) + { + activate(); + } +} + +void ALSceneExplorerItem::setPassedFilter(bool passed, S32 filter_generation, + std::string::size_type string_offset, + std::string::size_type string_size) +{ + // Mirrors LLFolderViewModelItemInventory::setPassedFilter: ask the parent + // folder to re-arrange when this item's filtered state (or its validity + // after a generation bump) changed. arrange() is the only thing that + // applies pass/fail state to widget visibility, and nothing else re-arms + // it on filter changes — without this, toggling a filter only takes + // visual effect when some unrelated event happens to arrange the tree. + const bool generation_skip = mMarkedDirtyGeneration >= 0 + && mPrevPassedAllFilters + && mMarkedDirtyGeneration < mRootViewModel.getFilter().getFirstSuccessGeneration(); + const S32 last_generation = mLastFilterGeneration; + LLFolderViewModelItemCommon::setPassedFilter(passed, filter_generation, string_offset, string_size); + const bool before = mPrevPassedAllFilters; + mPrevPassedAllFilters = passedFilter(filter_generation); + + if (before != mPrevPassedAllFilters // change of state + || generation_skip // was marked dirty while passing + // (Re)stamped as passing after first-success moved past this item's + // last stamp: its visibility-by-generation had been invalidated (a + // more-restrictive change or restart), so it must arrange back in + // even though it passed before too. Deliberately keyed on + // first-SUCCESS, not inventory's first-required: required doesn't + // move on more-restrictive changes, which would leave rows hidden by + // a mid-refilter arrange with nothing to bring them back. + || (mPrevPassedAllFilters + && last_generation < mRootViewModel.getFilter().getFirstSuccessGeneration())) + { + LLFolderViewFolder* parent_folder = + mFolderViewItem ? mFolderViewItem->getParentFolder() : nullptr; + if (parent_folder) + { + parent_folder->requestArrange(); + } + } +} + +bool ALSceneExplorerItem::filter(LLFolderViewFilter& filter) +{ + const S32 filter_generation = filter.getCurrentGeneration(); + const S32 must_pass_generation = filter.getFirstRequiredGeneration(); + + if (getLastFilterGeneration() >= must_pass_generation + && getLastFolderFilterGeneration() >= must_pass_generation + && !passedFilter(must_pass_generation)) + { + // Already failed a filter at least as strict as this one. + setPassedFilter(false, filter_generation); + setPassedFolderFilter(false, filter_generation); + return true; + } + + const bool passed_filter_folder = isFolderType() ? filter.checkFolder(this) : true; + setPassedFolderFilter(passed_filter_folder, filter_generation); + + bool continue_filtering = true; + if (!mChildren.empty() + && (getLastFilterGeneration() < must_pass_generation + || descendantsPassedFilter(must_pass_generation))) + { + for (auto& childp : mChildren) + { + ALSceneExplorerItem* child = static_cast(childp.get()); + if (child->getLastFilterGeneration() < filter_generation) + { + // Child returns false when the per-pass time budget is spent; + // stop here and resume from this child on the next idle. + continue_filtering = child->filter(filter); + } + if (child->passedFilter()) + { + ALSceneExplorerItem* vm = this; + while (vm && vm->mMostFilteredDescendantGeneration < filter_generation) + { + vm->mMostFilteredDescendantGeneration = filter_generation; + vm = static_cast(vm->mParent); + } + } + if (!continue_filtering) + break; + } + } + + if (continue_filtering) + { + const bool passed = filter.check(this); + // The match offset/size make the folder view draw the standard + // inventory-style highlight over the matched substring. + setPassedFilter(passed, filter_generation, + filter.getStringMatchOffset(this), filter.getFilterStringSize()); + continue_filtering = !filter.isTimedOut(); + } + + return continue_filtering; +} + +// ============================================================================ +// ALSceneExplorerFolder +// ============================================================================ +bool ALSceneExplorerFolder::handleDoubleClick(S32 x, S32 y, MASK mask) +{ + ALSceneExplorerItem* item = static_cast(getViewModelItem()); + + // Childless scene objects (single-prim roots) have nothing to expand, so + // double-click activates them the way a leaf prim does. Everything else — + // containers, multi-prim linksets, avatars — keeps the standard + // expand/collapse double-click; activation for those lives in the action + // buttons and context menu. (activate() rather than openItem(): the model + // openItem() also fires on folder expansion and must stay inert here.) + if (item && !item->isContainer() && item->getChildrenCount() == 0) + { + if (getRoot()) + getRoot()->setSelection(this, false); + item->activate(); + return true; + } + return LLFolderViewFolder::handleDoubleClick(x, y, mask); +} + +namespace +{ + // Shared rich-tooltip display for both widget flavours: query the model on + // demand so the contents are always current. + bool showSceneTooltip(LLFolderViewItem* widget) + { + ALSceneExplorerItem* item = static_cast(widget->getViewModelItem()); + if (!item || item->isContainer()) + return false; + LLToolTip::Params params; + params.message = item->getTooltip(); + params.sticky_rect = widget->calcScreenRect(); + LLToolTipMgr::instance().show(params); + return true; + } +} + +bool ALSceneExplorerFolder::handleToolTip(S32 x, S32 y, MASK mask) +{ + // An open folder's children own their own tooltips. + if (isOpen() && LLView::childrenHandleToolTip(x, y, mask) != nullptr) + return true; + if (showSceneTooltip(this)) + return true; + return LLFolderViewFolder::handleToolTip(x, y, mask); +} + +bool ALSceneExplorerListItem::handleToolTip(S32 x, S32 y, MASK mask) +{ + if (showSceneTooltip(this)) + return true; + return LLFolderViewItem::handleToolTip(x, y, mask); +} diff --git a/indra/newview/alsceneexplorermodel.h b/indra/newview/alsceneexplorermodel.h new file mode 100644 index 0000000000..264afc2caa --- /dev/null +++ b/indra/newview/alsceneexplorermodel.h @@ -0,0 +1,407 @@ +/** + * @file alsceneexplorermodel.h + * @brief Folder-view model for the Scene Explorer tree (region -> linkset -> prim, avatars) + * + * Copyright (c) 2026, Rye Mutt + * + * The source code in this file is provided to you under the terms of the + * GNU Lesser General Public License, version 2.1, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. Terms of the LGPL can be found in doc/LGPL-licence.txt + * in this distribution, or online at http://www.gnu.org/licenses/lgpl-2.1.txt + * + */ +#ifndef AL_SCENEEXPLORERMODEL_H +#define AL_SCENEEXPLORERMODEL_H + +#include "../llui/llfolderviewitem.h" +#include "../llui/llfolderviewmodel.h" + +#include "alobjectproperties.h" +#include "alsceneexplorerpredicate.h" +#include "lltimer.h" +#include "lluuid.h" + +class ALFloaterSceneExplorer; +class ALSceneExplorerItem; + +// ============================================================================ +// Sort comparator +// ============================================================================ +class ALSceneExplorerSort +{ +public: + enum ESortMode : U32 + { + SORT_DISTANCE = 0, // from the agent (reorders as the agent moves) + SORT_NAME, + SORT_LAND_IMPACT, + SORT_TRIANGLES, + SORT_TYPE, + SORT_REGION_ORIGIN // from the region's <0,0,0> (agent-independent) + }; + + ALSceneExplorerSort(ESortMode mode = SORT_DISTANCE) : mMode(mode) {} + + void setMode(ESortMode mode) { mMode = mode; } + ESortMode getMode() const { return mMode; } + + bool operator()(const ALSceneExplorerItem* a, const ALSceneExplorerItem* b) const; + +private: + ESortMode mMode; +}; + +// ============================================================================ +// Filter +// ============================================================================ +class ALSceneExplorerFilter final : public LLFolderViewFilter +{ +public: + enum EOwnerMode : U32 + { + OWNER_ANY = 0, + OWNER_MINE, + OWNER_GROUP, + OWNER_OTHERS, + OWNER_SPECIFIC // a single owner/group id ("Filter by this owner") + }; + + // Which field the text predicate searches (the inventory search-type + // combo pattern). Order matches the search_type combo in the XUI. + enum ESearchType : U32 + { + SEARCH_NAME = 0, + SEARCH_DESCRIPTION, + SEARCH_OWNER, // resolved owner/group display name + SEARCH_UUID, + SEARCH_ALL + }; + + // Spatial scope. Order matches the scope radio in the filters floater. + enum EScope : U32 + { + SCOPE_REGION = 0, + SCOPE_PARCEL, // the parcel the agent is standing on + SCOPE_RADIUS // within N m of the agent + }; + + ALSceneExplorerFilter(); + ~ALSceneExplorerFilter() override = default; + + // Predicate setters (called by the floater); each bumps the generation. + void setFilterSubString(const std::string& string); + void setSearchType(ESearchType type); + void setOwnerMode(EOwnerMode mode); + void setOwnerId(const LLUUID& id); // target for OWNER_SPECIFIC + void setGeomMask(U32 mask); // bits: 1 << ALObjectProperties::EGeom + void setFlagMask(U32 mask); // bits: ALObjectProperties::EFlag (require all) + void setScope(EScope scope, F32 radius); + void setMinLandImpact(F32 min_li); + void setMinTriangles(U32 min_tris); + // Scope predicates depend on the agent's position/parcel, which the + // filter can't observe — the floater re-arms on agent movement. + bool isScopeActive() const + { + return mConstraints.mScope != (U32)ALSceneExplorerPredicate::SCOPE_REGION; + } + + // LLFolderViewFilter + bool check(const LLFolderViewModelItem* item) override; + bool checkFolder(const LLFolderViewModelItem* folder) const override { return true; } + + void setEmptyLookupMessage(const std::string& message) override { mEmptyLookupMessage = message; } + std::string getEmptyLookupMessage(bool is_empty_folder = false) const override { return mEmptyLookupMessage; } + + bool showAllResults() const override { return false; } + + // Offset of the text match within the item's display name (npos when the + // match landed in another field), so the folder view draws the standard + // inventory match highlight. + std::string::size_type getStringMatchOffset(LLFolderViewModelItem* item) const override; + std::string::size_type getFilterStringSize() const override { return mConstraints.mFilterSubString.size(); } + + bool isActive() const override; + bool isModified() const override { return mModified; } + void clearModified() override + { + mModified = false; + // The pending refilter is done; the next change starts a new batch. + mFilterModified = FILTER_NONE; + } + const std::string& getName() const override { return mName; } + const std::string& getFilterText() override { return mConstraints.mFilterSubString; } + void setModified(EFilterModified behavior = FILTER_RESTART) override; + + // Time-slice filtering the way LLInventoryFilter does: each pass gets a few + // ms, then bails and resumes next idle, so filtering a 60k-node tree stays + // responsive instead of stalling on one frame. Must be a real-time LLTimer: + // an LLFrameTimer's clock only advances once per frame, so it could never + // expire inside a single pass and the budget would be a no-op. + void resetTime(S32 timeout) override + { + mFilterTime.reset(); + mFilterTime.setTimerExpirySec((F32)timeout / 1000.f); + } + bool isTimedOut() override { return mFilterTime.hasExpired(); } + + bool isDefault() const override { return !isActive(); } + bool isNotDefault() const override { return isActive(); } + void markDefault() override {} + void resetDefault() override {} + + // Multi-generation scheme (the LLInventoryFilter model): current bumps on + // every change; items that PASSED at >= first-success still pass (so they + // stay visible while a less-restrictive change refilters); items that + // FAILED at >= first-required still fail (so a more-restrictive change + // can re-stamp them without re-evaluating the predicate). + S32 getCurrentGeneration() const override { return mCurrentGeneration; } + S32 getFirstSuccessGeneration() const override { return mFirstSuccessGeneration; } + S32 getFirstRequiredGeneration() const override { return mFirstRequiredGeneration; } + +private: + // Map a classified constraint change onto the generation bookkeeping + // (CHANGE_NONE is a no-op). + void applyChange(ALSceneExplorerPredicate::EFilterChange change); + + std::string mName; + std::string mEmptyLookupMessage; + // The predicate state itself lives in the pure, unit-tested constraint + // set; this class is the LLFolderViewFilter adapter around it. The + // setters above keep writing through, and check() supplies the impure + // facts (agent id, parcel containment) per item. + ALSceneExplorerPredicate::Constraints mConstraints; + bool mModified = false; + EFilterModified mFilterModified = FILTER_NONE; // merged kind of the pending batch + S32 mCurrentGeneration = 1; + S32 mFirstSuccessGeneration = 1; + S32 mFirstRequiredGeneration = 1; + LLTimer mFilterTime; // per-pass time budget for filter() +}; + +// ============================================================================ +// View model +// ============================================================================ +class ALSceneExplorerViewModel final +: public LLFolderViewModel +{ +public: + typedef LLFolderViewModel base_t; + + ALSceneExplorerViewModel() : base_t(new ALSceneExplorerSort(), new ALSceneExplorerFilter()) {} + + bool startDrag(std::vector& items) override { return false; } +}; + +// ============================================================================ +// Model item +// ============================================================================ +class ALSceneExplorerItem final : public LLFolderViewModelItemCommon +{ +public: + enum EItemType : U8 + { + TYPE_REGION = 0, + TYPE_CATEGORY_OBJECTS, + TYPE_CATEGORY_AVATARS, + TYPE_CATEGORY_DERENDERED, + TYPE_LINKSET, + TYPE_PRIM, + TYPE_AVATAR, + TYPE_ATTACHMENT_POINT, // per-avatar attachment-point grouping folder + TYPE_ATTACHMENT, // attachment linkset root + TYPE_DERENDERED_OBJECT, // ALDerenderList entry; synthetic, no live object + TYPE_DERENDERED_AVATAR + }; + + ALSceneExplorerItem(EItemType type, const LLUUID& id, const std::string& name, + LLFolderViewModelInterface& root_view_model, ALFloaterSceneExplorer* floater); + + // Accessors used by filter / sort / floater + EItemType getItemType() const { return mItemType; } + bool isContainer() const + { + return mItemType == TYPE_REGION || isCategory() || mItemType == TYPE_ATTACHMENT_POINT; + } + bool isCategory() const + { + return mItemType == TYPE_CATEGORY_OBJECTS + || mItemType == TYPE_CATEGORY_AVATARS + || mItemType == TYPE_CATEGORY_DERENDERED; + } + // Synthetic rows backed by an ALDerenderList entry instead of a live + // object: excluded from the present-set sweep, the props fetch/retry, and + // live-object actions (Restore / Copy-ID only). + bool isDerenderedType() const + { + return mItemType == TYPE_DERENDERED_OBJECT || mItemType == TYPE_DERENDERED_AVATAR; + } + // Folder-ness is fully derived from the item type: structural containers + // plus every root scene object (so single- and multi-prim objects sort + // together in one widget list). Only child prims are leaf widgets. + bool isFolderType() const + { + return isContainer() + || mItemType == TYPE_LINKSET + || mItemType == TYPE_AVATAR + || mItemType == TYPE_ATTACHMENT; + } + const LLUUID& getUUID() const { return mUUID; } + const ALObjectProperties::Record& getRecord() const { return mRecord; } + ALObjectProperties::Record& getRecordRef() { return mRecord; } + + // Pre-lowercased per-field search text for the filter (kept separate so + // the search-type combo can target one field without allocation churn). + const std::string& searchName() const { return mSearchName; } + const std::string& searchDesc() const { return mSearchDesc; } + const std::string& searchUUID() const { return mSearchUUID; } + const std::string& searchOwner() const { return mOwnerSearch; } + + void setName(const std::string& name); + void updateRecord(const ALObjectProperties::Record& rec); + // Same, but also adopts @display_name (when non-empty) in the one + // rebuildSearchable()/dirtyFilter() pass instead of dirtying twice. + void updateRecord(const ALObjectProperties::Record& rec, const std::string& display_name); + + // Index within the parent linkset/avatar child list (1-based; 0 = unset/root). + // Used to keep child prims in native link order regardless of the active sort. + void setLinkOrder(S32 order) { mLinkOrder = order; } + S32 getLinkOrder() const { return mLinkOrder; } + + // Bounded re-request bookkeeping for the floater's property fetch. + S32 getPropsRetries() const { return mPropsRetries; } + void notePropsRetry() { ++mPropsRetries; } + + // One-shot guard for demand-driven cost fetches (visible row / LI sort / + // detail pane): a failed fetch leaves the object's cost stale forever, so + // each node asks at most once per session unless explicitly refreshed. + bool wasCostRequested() const { return mCostRequested; } + void noteCostRequested() { mCostRequested = true; } + + // Explicit Refresh: re-arm the bounded retry and cost demand so the + // fetch pipeline may ask the server again. + void resetFetchState() { mPropsRetries = 0; mCostRequested = false; } + + // Resolved owner (or owning group) display name: folded into the + // searchable text and shown in the row suffix. Resolved and RLVa-gated + // by the floater. + void setOwnerName(const std::string& name); + const std::string& getOwnerName() const { return mOwnerDisplay; } + + // Run the configured activate action (focus/edit/inspect) on this object. + // Invoked from the view's double-click paths only — openItem() also fires + // when a folder is expanded, which must never trigger activation. + void activate(); + + // LLFolderViewModelItem (non-Common) + const std::string& getName() const override { return mName; } + const std::string& getDisplayName() const override { return mName; } + const std::string& getSearchableName() const override { return mSearchName; } + std::string getSearchableDescription() const override { return mRecord.mDescription; } + std::string getSearchableCreatorName() const override { return LLStringUtil::null; } + std::string getSearchableUUIDString() const override { return mUUID.asString(); } + + LLPointer getIcon() const override; + LLFontGL::StyleFlags getLabelStyle() const override; + std::string getLabelSuffix() const override; + + // Multi-line hover tooltip built from the record (always current — the + // widgets query it on demand instead of caching it). + std::string getTooltip() const; + + void openItem(void) override; + void closeItem(void) override {} + void selectItem(void) override {} + void navigateToFolder(bool new_window = false, bool change_mode = false) override {} + + bool isFavorite() const override { return false; } + bool isItemRenameable() const override { return false; } + bool renameItem(const std::string& new_name) override { return false; } + bool isItemMovable(void) const override { return false; } + void move(LLFolderViewModelItem* parent_listener) override {} + bool isItemRemovable(bool check_worn = true) const override { return false; } + bool isItemInTrash(void) const override { return false; } + bool removeItem() override { return false; } + void removeBatch(std::vector& batch) override {} + bool isItemCopyable(bool can_copy_as_link = true) const override { return true; } + bool copyToClipboard() const override { return false; } + bool cutToClipboard() override { return false; } + bool isClipboardPasteable() const override { return false; } + void pasteFromClipboard() override {} + void pasteLinkFromClipboard() override {} + bool isAgentInventory() const override { return false; } + bool isAgentInventoryRoot() const override { return false; } + // Forwards to the floater, which shows/hides the shared superset menu per + // row type (and stages the row as the live selection for reused viewer + // handlers). Also reused by the floater's gear button. + void buildContextMenu(LLMenuGL& menu, U32 flags) override; + + bool hasChildren() const override { return getChildrenCount() > 0; } + + bool dragOrDrop(MASK mask, bool drop, EDragAndDropType cargo_type, + void* cargo_data, std::string& tooltip_msg) override { return false; } + + // Real filtering (mirrors LLFolderViewModelItemInventory::filter). + bool filter(LLFolderViewFilter& filter) override; + // Requests a re-arrange when this item's filtered state changes — the + // framework has no other filter->arrange link, so without this a filter + // toggle only becomes visible when something else happens to arrange + // (mirrors LLFolderViewModelItemInventory::setPassedFilter). + void setPassedFilter(bool passed, S32 filter_generation, + std::string::size_type string_offset = std::string::npos, + std::string::size_type string_size = 0) override; + +private: + void rebuildSearchable(); + + EItemType mItemType; + LLUUID mUUID; + std::string mName; + std::string mSearchName; // lowercased name + std::string mSearchDesc; // lowercased description + std::string mSearchUUID; // uuid string (already lowercase hex) + std::string mOwnerSearch; // lowercased owner display name + std::string mOwnerDisplay; // owner display name (row suffix) + S32 mLinkOrder = 0; + S32 mPropsRetries = 0; + bool mCostRequested = false; + bool mPrevPassedAllFilters = false; + ALObjectProperties::Record mRecord; + ALFloaterSceneExplorer* mFloater; +}; + +// ============================================================================ +// Folder widget +// +// Scene objects (linksets, single prims, avatars, attachment roots) are all +// represented as folders so they sort together in one list regardless of prim +// count (LLFolderViewFolder lays out sub-folders above leaf items, which would +// otherwise force multi-prim linksets above single prims). Childless folders +// simply draw no disclosure arrow. This subclass restores leaf-style activation: +// a double-click acts on the object (focus/edit/inspect) instead of toggling, +// while the disclosure arrow still expands multi-prim linksets. Structural +// containers (region/category/attachment-point) keep the default toggle. +// ============================================================================ +class ALSceneExplorerFolder final : public LLFolderViewFolder +{ +public: + typedef LLFolderViewFolder::Params Params; // LLFolderViewFolder has no Params of its own + ALSceneExplorerFolder(const Params& p) : LLFolderViewFolder(p) {} + ~ALSceneExplorerFolder() override = default; + + bool handleDoubleClick(S32 x, S32 y, MASK mask) override; + bool handleToolTip(S32 x, S32 y, MASK mask) override; +}; + +// Leaf widget: identical to the stock item except for the rich model tooltip. +class ALSceneExplorerListItem final : public LLFolderViewItem +{ +public: + typedef LLFolderViewItem::Params Params; + ALSceneExplorerListItem(const Params& p) : LLFolderViewItem(p) {} + ~ALSceneExplorerListItem() override = default; + + bool handleToolTip(S32 x, S32 y, MASK mask) override; +}; + +#endif // AL_SCENEEXPLORERMODEL_H diff --git a/indra/newview/alsceneexplorerpredicate.cpp b/indra/newview/alsceneexplorerpredicate.cpp new file mode 100644 index 0000000000..4f7e54a8b1 --- /dev/null +++ b/indra/newview/alsceneexplorerpredicate.cpp @@ -0,0 +1,153 @@ +/** + * @file alsceneexplorerpredicate.cpp + * @brief Pure Scene Explorer filter predicate + * + * Copyright (c) 2026, Rye Mutt + * + * The source code in this file is provided to you under the terms of the + * GNU Lesser General Public License, version 2.1, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. Terms of the LGPL can be found in doc/LGPL-licence.txt + * in this distribution, or online at http://www.gnu.org/licenses/lgpl-2.1.txt + * + */ + +#include "linden_common.h" + +#include "alsceneexplorerpredicate.h" + +namespace ALSceneExplorerPredicate +{ + +bool matches(const ItemFacts& item, const Constraints& c) +{ + if (!item.mRecord) + return false; + const ALObjectProperties::Record& rec = *item.mRecord; + + // Spatial scope. Parcel containment is impure (agent parcel state), so + // the caller resolves it into the facts. + if (c.mScope == SCOPE_RADIUS && rec.mDistance > c.mRadius) + return false; + if (c.mScope == SCOPE_PARCEL && !item.mInAgentParcel) + return false; + + if (c.mGeomMask != 0 && !(c.mGeomMask & (1u << rec.mGeom))) + return false; + + // Feature mask: every checked flag is required. + if (c.mFlagMask != 0 && (rec.mFlags & c.mFlagMask) != c.mFlagMask) + return false; + + // Heaviness thresholds apply to root-level rows; the children of a + // passing linkset stay browsable (their own LI/triangles are fractions + // of the total the threshold matched). + if (!item.mIsChildPrim) + { + if (c.mMinLandImpact > 0.f && rec.mLandImpact < c.mMinLandImpact) + return false; + if (c.mMinTriangles > 0 && rec.mNumTriangles < c.mMinTriangles) + return false; + } + + if (c.mOwnerMode != OWNER_ANY) + { + // Avatars are their own "owner" so owner filters behave sensibly for + // them; object rows apply the predicate only once props have arrived. + const LLUUID& owner = item.mIsAvatar ? item.mItemId : rec.mOwnerId; + const bool group_owned = !item.mIsAvatar && rec.mGroupOwned; + if (item.mIsAvatar || rec.mPropsValid) + { + switch (c.mOwnerMode) + { + case OWNER_MINE: if (owner != c.mAgentId) return false; break; + case OWNER_GROUP: if (!group_owned) return false; break; + case OWNER_OTHERS: if (owner == c.mAgentId || group_owned) return false; break; + case OWNER_SPECIFIC: + if (owner != c.mOwnerId && !(group_owned && rec.mGroupId == c.mOwnerId)) + return false; + break; + default: break; + } + } + } + + if (!c.mFilterSubString.empty()) + { + auto contains = [&c](const std::string* haystack) + { + return haystack && haystack->find(c.mFilterSubString) != std::string::npos; + }; + bool found = false; + switch (c.mSearchType) + { + case SEARCH_DESCRIPTION: found = contains(item.mSearchDesc); break; + case SEARCH_OWNER: found = contains(item.mSearchOwner); break; + case SEARCH_UUID: found = contains(item.mSearchUUID); break; + case SEARCH_ALL: + found = contains(item.mSearchName) || contains(item.mSearchDesc) + || contains(item.mSearchUUID) || contains(item.mSearchOwner); + break; + case SEARCH_NAME: + default: found = contains(item.mSearchName); break; + } + if (!found) + return false; + } + + return true; +} + +EFilterChange classifySubstringChange(const std::string& old_needle, const std::string& new_needle) +{ + if (old_needle == new_needle) + return CHANGE_NONE; + // The predicate is "haystack contains needle". If the old needle occurs + // inside the new one, any haystack containing the new needle also + // contains the old — strictly fewer matches. An empty needle (matches + // everything) classifies through find() naturally for both directions. + if (new_needle.find(old_needle) != std::string::npos) + return CHANGE_MORE_RESTRICTIVE; + if (old_needle.find(new_needle) != std::string::npos) + return CHANGE_LESS_RESTRICTIVE; + return CHANGE_RESTART; +} + +EFilterChange classifyRequireAllMaskChange(U32 old_mask, U32 new_mask) +{ + if (old_mask == new_mask) + return CHANGE_NONE; + // All previously required bits still required, plus more. + if ((new_mask & old_mask) == old_mask) + return CHANGE_MORE_RESTRICTIVE; + // A subset of the previous requirements. + if ((new_mask & old_mask) == new_mask) + return CHANGE_LESS_RESTRICTIVE; + return CHANGE_RESTART; +} + +EFilterChange classifyAllowedMaskChange(U32 old_mask, U32 new_mask) +{ + if (old_mask == new_mask) + return CHANGE_NONE; + // 0 means "anything allowed" — treat it as the full set. + const U32 old_allowed = old_mask ? old_mask : ~0u; + const U32 new_allowed = new_mask ? new_mask : ~0u; + if (old_allowed == new_allowed) + return CHANGE_NONE; + // Everything previously allowed is still allowed, plus more. + if ((new_allowed & old_allowed) == old_allowed) + return CHANGE_LESS_RESTRICTIVE; + if ((new_allowed & old_allowed) == new_allowed) + return CHANGE_MORE_RESTRICTIVE; + return CHANGE_RESTART; +} + +EFilterChange classifyMinThresholdChange(F32 old_min, F32 new_min) +{ + if (old_min == new_min) + return CHANGE_NONE; + return (new_min > old_min) ? CHANGE_MORE_RESTRICTIVE : CHANGE_LESS_RESTRICTIVE; +} + +} // namespace ALSceneExplorerPredicate diff --git a/indra/newview/alsceneexplorerpredicate.h b/indra/newview/alsceneexplorerpredicate.h new file mode 100644 index 0000000000..6f216c2cab --- /dev/null +++ b/indra/newview/alsceneexplorerpredicate.h @@ -0,0 +1,116 @@ +/** + * @file alsceneexplorerpredicate.h + * @brief The pure, unit-testable core of the Scene Explorer's filter + * predicate: matches an object Record + pre-lowercased search strings + * against a constraint set, with no viewer-state dependencies. + * + * Copyright (c) 2026, Rye Mutt + * + * The source code in this file is provided to you under the terms of the + * GNU Lesser General Public License, version 2.1, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. Terms of the LGPL can be found in doc/LGPL-licence.txt + * in this distribution, or online at http://www.gnu.org/licenses/lgpl-2.1.txt + * + */ +#ifndef AL_SCENEEXPLORERPREDICATE_H +#define AL_SCENEEXPLORERPREDICATE_H + +#include "alobjectproperties.h" + +namespace ALSceneExplorerPredicate +{ + // Numeric twins of ALSceneExplorerFilter's UI-facing enums, kept here so + // the predicate and its unit test need no llui/view dependencies. The + // model static_asserts the two stay in lockstep. + enum ESearchField : U32 + { + SEARCH_NAME = 0, + SEARCH_DESCRIPTION, + SEARCH_OWNER, + SEARCH_UUID, + SEARCH_ALL + }; + + enum EOwnerMode : U32 + { + OWNER_ANY = 0, + OWNER_MINE, + OWNER_GROUP, + OWNER_OTHERS, + OWNER_SPECIFIC + }; + + enum EScope : U32 + { + SCOPE_REGION = 0, + SCOPE_PARCEL, + SCOPE_RADIUS + }; + + // Per-item inputs. The string pointers reference the item's + // pre-lowercased search copies; facts the predicate cannot derive purely + // (parcel containment) are resolved by the caller. + struct ItemFacts + { + const ALObjectProperties::Record* mRecord = nullptr; + const std::string* mSearchName = nullptr; + const std::string* mSearchDesc = nullptr; + const std::string* mSearchUUID = nullptr; + const std::string* mSearchOwner = nullptr; + LLUUID mItemId; + bool mIsAvatar = false; // avatar rows count as their own owner + bool mIsChildPrim = false; // exempt from the heaviness thresholds + bool mInAgentParcel = true; // resolved by the caller (impure) + }; + + // The active constraint set (the filter's persistent state). + struct Constraints + { + std::string mFilterSubString; // lowercased + U32 mSearchType = SEARCH_NAME; + U32 mOwnerMode = OWNER_ANY; + LLUUID mOwnerId; // OWNER_SPECIFIC target + LLUUID mAgentId; // for OWNER_MINE / OWNER_OTHERS + U32 mGeomMask = 0; // bits: 1 << EGeom; 0 = any + U32 mFlagMask = 0; // EFlag bits; all set bits required + U32 mScope = SCOPE_REGION; + F32 mRadius = 64.f; + F32 mMinLandImpact = 0.f; // 0 = off; root-level rows only + U32 mMinTriangles = 0; // 0 = off; root-level rows only + }; + + bool matches(const ItemFacts& item, const Constraints& c); + + // ------------------------------------------------------------------ + // Change classification for the multi-generation filter scheme: how a + // single constraint change relates to its previous value. Only provably + // monotonic cases classify; anything ambiguous is CHANGE_RESTART. The + // predicate is a conjunction of independent clauses, so tightening or + // loosening one clause tightens/loosens the composite. + // ------------------------------------------------------------------ + enum EFilterChange : U32 + { + CHANGE_NONE = 0, // no effective change + CHANGE_MORE_RESTRICTIVE, // everything that failed before still fails + CHANGE_LESS_RESTRICTIVE, // everything that passed before still passes + CHANGE_RESTART // unrelated; both sides must re-evaluate + }; + + // Substring containment ("contains needle"): extending the needle is + // stricter, shrinking it is looser (empty needle = no predicate). + EFilterChange classifySubstringChange(const std::string& old_needle, const std::string& new_needle); + + // Require-all mask (the feature flag mask): adding required bits is + // stricter, removing them is looser, mixing both is a restart. + EFilterChange classifyRequireAllMaskChange(U32 old_mask, U32 new_mask); + + // Allowed-set mask (the geometry type mask; 0 = anything allowed): + // allowing more kinds is looser, allowing fewer is stricter. + EFilterChange classifyAllowedMaskChange(U32 old_mask, U32 new_mask); + + // Minimum threshold (0 = off): raising is stricter, lowering is looser. + EFilterChange classifyMinThresholdChange(F32 old_min, F32 new_min); +} + +#endif // AL_SCENEEXPLORERPREDICATE_H diff --git a/indra/newview/app_settings/commands.xml b/indra/newview/app_settings/commands.xml index 2cff9a082a..c7374b81d2 100644 --- a/indra/newview/app_settings/commands.xml +++ b/indra/newview/app_settings/commands.xml @@ -297,6 +297,16 @@ is_running_function="Floater.IsOpen" is_running_parameters="region_tracker" /> + + ALSceneExplorerActivateAction + + Comment + Scene Explorer: double-click / Enter action (0=focus, 1=edit, 2=inspect, 3=select). + Persist + 1 + Type + U32 + Value + 0 + + ALSceneExplorerFlagFilter + + Comment + Scene Explorer: feature-flag filter mask (ALObjectProperties::EFlag bits; all set bits required). + Persist + 1 + Type + U32 + Value + 0 + + ALSceneExplorerFullRegion + + Comment + Scene Explorer: ask the simulator to stream the entire region (360 interest list) while the explorer is open. + Persist + 1 + Type + Boolean + Value + 0 + + ALSceneExplorerSearchType + + Comment + Scene Explorer: which field the filter text searches (0 name, 1 description, 2 owner, 3 UUID, 4 all). + Persist + 1 + Type + U32 + Value + 0 + + ALSceneExplorerSelectionSync + + Comment + Scene Explorer: mirror selection between the tree and the world (tree rows select in-world while Build/Inspect is open; in-world selection highlights its row). + Persist + 1 + Type + Boolean + Value + 1 + + ALSceneExplorerScope + + Comment + Scene Explorer: spatial scope of the filter (0 whole region, 1 agent's parcel, 2 within ALSceneExplorerRadius meters). + Persist + 1 + Type + U32 + Value + 0 + + ALSceneExplorerTypeFilter + + Comment + Scene Explorer: geometry-type filter mask (bits: 1 prim, 2 mesh, 4 sculpt; 0 = any). + Persist + 1 + Type + U32 + Value + 0 + + ALSceneExplorerMinLandImpact + + Comment + Scene Explorer: show only linksets with at least this much land impact (0 = off). + Persist + 1 + Type + F32 + Value + 0 + + ALSceneExplorerMinTriangles + + Comment + Scene Explorer: show only objects with at least this many triangles (0 = off). + Persist + 1 + Type + U32 + Value + 0 + + ALSceneExplorerOwnerSuffix + + Comment + Scene Explorer: append the owner's display name to each row once resolved. + Persist + 1 + Type + Boolean + Value + 1 + + ALSceneExplorerOwnerFilter + + Comment + Scene Explorer: owner filter (0=any, 1=mine, 2=group, 3=others). + Persist + 1 + Type + U32 + Value + 0 + + ALSceneExplorerRadius + + Comment + Scene Explorer: radius in meters for the limit-radius filter. + Persist + 1 + Type + F32 + Value + 64.0 + + ALSceneExplorerShowAvatars + + Comment + Scene Explorer: include avatars and their attachments in the tree. + Persist + 1 + Type + Boolean + Value + 1 + + ALSceneExplorerShowDerendered + + Comment + Scene Explorer: list derendered objects/avatars in their own category. + Persist + 1 + Type + Boolean + Value + 0 + + ALSceneExplorerShowDetails + + Comment + Scene Explorer: show the detail pane for the selected row. + Persist + 1 + Type + Boolean + Value + 0 + + ALSceneExplorerSortOrder + + Comment + Scene Explorer: tree sort order (0=distance, 1=name, 2=land impact, 3=triangles, 4=type). + Persist + 1 + Type + U32 + Value + 0 + NumpadControl Comment @@ -2503,7 +2679,7 @@ Type U32 Value - 1023 + 4095 ClickingAvatarKeepsCamera diff --git a/indra/newview/llfloaterinspect.cpp b/indra/newview/llfloaterinspect.cpp index 569b626aca..3203a1542c 100644 --- a/indra/newview/llfloaterinspect.cpp +++ b/indra/newview/llfloaterinspect.cpp @@ -28,6 +28,7 @@ #include "llfloaterinspect.h" +#include "alobjectproperties.h" #include "llfloaterreg.h" #include "llfloatertools.h" #include "llavataractions.h" @@ -59,6 +60,27 @@ //LLFloaterInspect* LLFloaterInspect::sInstance = NULL; +// Opt-in scene-builder metric column bits. Defined once here and reused both in +// mColumnBits and METRIC_COLUMNS_MASK so the two cannot silently drift apart. +namespace +{ + constexpr U32 COL_LAND_IMPACT = 1u << 10; + constexpr U32 COL_RENDERCOST = 1u << 11; + constexpr U32 COL_PHYSICSCOST = 1u << 12; + constexpr U32 COL_STREAMCOST = 1u << 13; + constexpr U32 COL_PRIMCOUNT = 1u << 14; + constexpr U32 COL_DISTANCE = 1u << 15; + constexpr U32 COL_SCRIPTS = 1u << 16; + constexpr U32 COL_LIGHTGLOW = 1u << 17; + constexpr U32 COL_MEDIA = 1u << 18; + constexpr U32 COL_ALPHAPBR = 1u << 19; +} + +// Union of the opt-in scene-builder metric column bits (see mColumnBits). +static constexpr U32 METRIC_COLUMNS_MASK = + COL_LAND_IMPACT | COL_RENDERCOST | COL_PHYSICSCOST | COL_STREAMCOST | COL_PRIMCOUNT | + COL_DISTANCE | COL_SCRIPTS | COL_LIGHTGLOW | COL_MEDIA | COL_ALPHAPBR; + LLFloaterInspect::LLFloaterInspect(const LLSD& key) : LLFloater(key), mDirty(false), @@ -85,6 +107,18 @@ LLFloaterInspect::LLFloaterInspect(const LLSD& key) mColumnBits["tramcount"] = 128; mColumnBits["vramcount"] = 256; mColumnBits["creation_date"] = 512; + // Scene-builder metrics, sourced from ALObjectProperties so this floater + // and the Scene Explorer always report the same numbers. + mColumnBits["land_impact"] = COL_LAND_IMPACT; + mColumnBits["rendercost"] = COL_RENDERCOST; + mColumnBits["physicscost"] = COL_PHYSICSCOST; + mColumnBits["streamingcost"] = COL_STREAMCOST; + mColumnBits["primcount"] = COL_PRIMCOUNT; + mColumnBits["distance"] = COL_DISTANCE; + mColumnBits["scripts"] = COL_SCRIPTS; + mColumnBits["lightglow"] = COL_LIGHTGLOW; + mColumnBits["media"] = COL_MEDIA; + mColumnBits["alphapbr"] = COL_ALPHAPBR; } bool LLFloaterInspect::postBuild() @@ -325,6 +359,7 @@ void LLFloaterInspect::refresh() mTextureMemory = 0; mTextureVRAMMemory = 0; std::string format_res_string; + const U32 column_config = gSavedSettings.getU32("ALInspectColumnConfig"); static LLCachedControl max_complexity_setting(gSavedSettings, "MaxAttachmentComplexity"); F32 max_attachment_complexity = max_complexity_setting; max_attachment_complexity = llmax(max_attachment_complexity, 1.0e6f); @@ -507,6 +542,65 @@ void LLFloaterInspect::refresh() row["columns"][15]["type"] = "text"; row["columns"][15]["value"] = LLSD::Integer(vram_memory / 1024); + // Scene-builder metric columns (opt-in). Only computed while one is + // visible: fromObject() walks faces and triggers the async cost fetch, + // which must stay demand-driven. + if (column_config & METRIC_COLUMNS_MASK) + { + const ALObjectProperties::Record metrics = ALObjectProperties::fromObject(vobj); + const bool is_link_root = vobj->isRoot() || vobj->isRootEdit(); + + S32 col = 16; + auto add_cell = [&row, &col](const char* name, const LLSD& value) + { + row["columns"][col]["column"] = name; + row["columns"][col]["type"] = "text"; + row["columns"][col]["value"] = value; + ++col; + }; + + // Roots show the linkset total; child rows show their own + // contribution, so the heavy prim in a linkset stands out. + const F32 li = is_link_root ? metrics.mLandImpact : metrics.mObjectCost; + add_cell("land_impact", li > 0.f ? llformat("%.0f", li) : std::string("-")); + add_cell("land_impact_sort", LLSD::Integer(ll_round(li))); + + add_cell("rendercost", llformat("%.0f", metrics.mRenderCost)); + add_cell("rendercost_sort", LLSD::Integer(ll_round(metrics.mRenderCost))); + + add_cell("physicscost", metrics.mPhysicsCost > 0.f ? llformat("%.1f", metrics.mPhysicsCost) : std::string("-")); + // One-decimal floats sort wrong as strings ("9.5" vs "9.12"), so + // sort companions carry the value scaled to an integer. + add_cell("physicscost_sort", LLSD::Integer(ll_round(metrics.mPhysicsCost * 10.f))); + + add_cell("streamingcost", metrics.mStreamingCost > 0.f ? llformat("%.1f", metrics.mStreamingCost) : std::string("-")); + add_cell("streamingcost_sort", LLSD::Integer(ll_round(metrics.mStreamingCost * 10.f))); + + add_cell("primcount", is_link_root ? llformat("%d", metrics.mPrimCount) : std::string()); + add_cell("primcount_sort", LLSD::Integer(is_link_root ? metrics.mPrimCount : 0)); + + add_cell("distance", llformat("%.1f", metrics.mDistance)); + add_cell("distance_sort", LLSD::Integer(ll_round(metrics.mDistance * 10.f))); + + add_cell("scripts", metrics.hasFlag(ALObjectProperties::FLAG_SCRIPTED) ? std::string("Yes") : std::string()); + + std::string light_glow; + if (metrics.hasFlag(ALObjectProperties::FLAG_LIGHT)) + light_glow = "Light"; + if (metrics.hasFlag(ALObjectProperties::FLAG_GLOW)) + light_glow += light_glow.empty() ? "Glow" : "+Glow"; + add_cell("lightglow", light_glow); + + add_cell("media", metrics.hasFlag(ALObjectProperties::FLAG_MEDIA) ? std::string("Yes") : std::string()); + + std::string alpha_pbr; + if (metrics.hasFlag(ALObjectProperties::FLAG_ALPHA)) + alpha_pbr = "Alpha"; + if (metrics.hasFlag(ALObjectProperties::FLAG_PBR_MATERIAL)) + alpha_pbr += alpha_pbr.empty() ? "PBR" : "+PBR"; + add_cell("alphapbr", alpha_pbr); + } + primcount = sel_mgr.getSelection()->getObjectCount(); objcount = sel_mgr.getSelection()->getRootObjectCount(); fcount += vobj->getNumFaces(); diff --git a/indra/newview/llselectmgr.cpp b/indra/newview/llselectmgr.cpp index c35130d22a..bd8214202b 100644 --- a/indra/newview/llselectmgr.cpp +++ b/indra/newview/llselectmgr.cpp @@ -51,6 +51,7 @@ #include "llquaternion.h" // viewer includes +#include "alobjectproperties.h" #include "llagent.h" #include "llagentcamera.h" #include "llattachmentsmgr.h" @@ -6327,7 +6328,14 @@ void LLSelectMgr::processObjectProperties(LLMessageSystem* msg, void** user_data if (!node) { - LL_WARNS() << "Couldn't find object " << id << " selected." << LL_ENDL; + // The Scene Explorer probes properties via bulk select/deselect + // without creating select nodes; replies it is awaiting (tracked + // per-id, so late arrivals after it closes are covered too) are + // expected to have no node and warrant no warning. + if (!ALObjectPropertiesCache::isExpectedReply(id)) + { + LL_WARNS() << "Couldn't find object " << id << " selected." << LL_ENDL; + } } else { diff --git a/indra/newview/llstartup.cpp b/indra/newview/llstartup.cpp index 7c46e9980a..f0729376ae 100644 --- a/indra/newview/llstartup.cpp +++ b/indra/newview/llstartup.cpp @@ -151,6 +151,7 @@ #include "llproductinforequest.h" #include "llqueryflags.h" #include "llsecapi.h" +#include "alobjectproperties.h" #include "llselectmgr.h" #include "llsky.h" #include "llstatview.h" @@ -3019,8 +3020,10 @@ void register_viewer_callbacks(LLMessageSystem* msg) msg->setHandlerFuncFast(_PREHASH_ImprovedInstantMessage, process_improved_im); msg->setHandlerFuncFast(_PREHASH_ScriptQuestion, process_script_question); - msg->setHandlerFuncFast(_PREHASH_ObjectProperties, LLSelectMgr::processObjectProperties); - msg->setHandlerFuncFast(_PREHASH_ObjectPropertiesFamily, LLSelectMgr::processObjectPropertiesFamily); + // ALObjectPropertiesCache caches object properties for the Scene Explorer + // (and other non-selection consumers), then forwards to LLSelectMgr. + msg->setHandlerFuncFast(_PREHASH_ObjectProperties, ALObjectPropertiesCache::processObjectProperties); + msg->setHandlerFuncFast(_PREHASH_ObjectPropertiesFamily, ALObjectPropertiesCache::processObjectPropertiesFamily); msg->setHandlerFuncFast(_PREHASH_ForceObjectSelect, LLSelectMgr::processForceObjectSelect); msg->setHandlerFuncFast(_PREHASH_MoneyBalanceReply, process_money_balance_reply); diff --git a/indra/newview/llviewerfloaterreg.cpp b/indra/newview/llviewerfloaterreg.cpp index 818f4d4a97..42643fc2c9 100644 --- a/indra/newview/llviewerfloaterreg.cpp +++ b/indra/newview/llviewerfloaterreg.cpp @@ -115,6 +115,8 @@ #include "llfloaterimagepreview.h" #include "llfloaterimsession.h" #include "llfloaterinspect.h" +#include "alfloatersceneexplorer.h" +#include "alfloatersceneexplorerfilters.h" #include "llfloaterinventorysettings.h" #include "llfloaterinventorythumbnailshelper.h" #include "llfloaterjoystick.h" @@ -437,6 +439,8 @@ void LLViewerFloaterReg::registerFloaters() LLFloaterReg::add("incoming_call", "floater_incoming_call.xml", (LLFloaterBuildFunc)&LLFloaterReg::build); LLFloaterReg::add("inventory", "floater_my_inventory.xml", (LLFloaterBuildFunc)&LLFloaterReg::build); LLFloaterReg::add("inspect", "floater_inspect.xml", (LLFloaterBuildFunc)&LLFloaterReg::build); + LLFloaterReg::add("scene_explorer", "floater_scene_explorer.xml", (LLFloaterBuildFunc)&LLFloaterReg::build); + LLFloaterReg::add("scene_explorer_filters", "floater_scene_explorer_filters.xml", (LLFloaterBuildFunc)&LLFloaterReg::build); LLFloaterReg::add("inventory_thumbnails_helper", "floater_inventory_thumbnails_helper.xml", (LLFloaterBuildFunc) &LLFloaterReg::build); LLFloaterReg::add("item_properties", "floater_item_properties.xml", (LLFloaterBuildFunc)&LLFloaterReg::build); LLFloaterReg::add("task_properties", "floater_task_properties.xml", (LLFloaterBuildFunc)&LLFloaterReg::build); diff --git a/indra/newview/llviewerobject.cpp b/indra/newview/llviewerobject.cpp index ad803fda27..319e9dc640 100644 --- a/indra/newview/llviewerobject.cpp +++ b/indra/newview/llviewerobject.cpp @@ -64,6 +64,7 @@ #include "llcontrolavatar.h" #include "lldrawable.h" #include "llface.h" +#include "llfloaterinspect.h" #include "llfloatertools.h" #include "llfollowcam.h" #include "llhudtext.h" @@ -3959,14 +3960,29 @@ void LLViewerObject::setObjectCostStale() getRootEdit()->mCostStale = true; } +// The async GetObjectCost replies land here; the Inspect floater's opt-in +// cost columns (and the build floater) read them off the selection, so both +// need a refresh poke when a cost arrives for a selected object. +static void dirty_cost_floaters() +{ + if (gFloaterTools) + { + gFloaterTools->dirty(); + } + if (LLFloaterInspect* inspect = LLFloaterReg::findTypedInstance("inspect")) + { + inspect->dirty(); + } +} + void LLViewerObject::setObjectCost(F32 cost) { mObjectCost = cost; mCostStale = false; - if (isSelected() && gFloaterTools) + if (isSelected()) { - gFloaterTools->dirty(); + dirty_cost_floaters(); } } @@ -3984,9 +4000,9 @@ void LLViewerObject::setLinksetCost(F32 cost) iter++; } - if (needs_refresh && gFloaterTools) + if (needs_refresh) { - gFloaterTools->dirty(); + dirty_cost_floaters(); } } @@ -3995,9 +4011,9 @@ void LLViewerObject::setPhysicsCost(F32 cost) mPhysicsCost = cost; mCostStale = false; - if (isSelected() && gFloaterTools) + if (isSelected()) { - gFloaterTools->dirty(); + dirty_cost_floaters(); } } @@ -4006,9 +4022,9 @@ void LLViewerObject::setLinksetPhysicsCost(F32 cost) mLinksetPhysicsCost = cost; mCostStale = false; - if (isSelected() && gFloaterTools) + if (isSelected()) { - gFloaterTools->dirty(); + dirty_cost_floaters(); } } diff --git a/indra/newview/llviewerobject.h b/indra/newview/llviewerobject.h index 05c322a78c..ef3d592e32 100644 --- a/indra/newview/llviewerobject.h +++ b/indra/newview/llviewerobject.h @@ -435,6 +435,15 @@ class LLViewerObject void setLinksetPhysicsCost(F32 cost); F32 getLinksetPhysicsCost(); + // Read the last cached accounting values WITHOUT triggering a fetch. The + // get* variants above queue a GetObjectCost capability request whenever the + // cost is stale — and a failed fetch leaves it stale — so periodic readers + // (e.g. the Scene Explorer's reconcile pass) must use these to avoid + // re-requesting failed/uncosted objects forever. + F32 peekObjectCost() const { return mObjectCost; } + F32 peekLinksetCost() const { return mLinksetCost; } + F32 peekPhysicsCost() const { return mPhysicsCost; } + void sendShapeUpdate(); // [RLVa:KB] - Checked: 2010-02-27 (RLVa-1.2.0a) | Added: RLVa-1.2.0a diff --git a/indra/newview/llviewerobjectlist.cpp b/indra/newview/llviewerobjectlist.cpp index 5fde88cc65..0017af7993 100644 --- a/indra/newview/llviewerobjectlist.cpp +++ b/indra/newview/llviewerobjectlist.cpp @@ -1128,67 +1128,94 @@ void LLViewerObjectList::fetchObjectCostsCoro(std::string url) return; } - LLSD idList(LLSD::emptyArray()); - - for (uuid_set_t::iterator it = diff.begin(); it != diff.end(); ++it) - { - idList.append(*it); - } - mPendingObjectCost.insert(diff.begin(), diff.end()); - LLSD postData = LLSD::emptyMap(); - - postData["object_ids"] = idList; - - LLSD result = httpAdapter->postAndSuspend(httpRequest, url, postData); + // The GetObjectCost capability rejects requests that ask for too many + // objects at once ("Maximum number of resource cost requests allowed is + // 500"), so split large requests into chunks issued sequentially. Stay + // comfortably under the limit since it is enforced at the boundary. + constexpr size_t MAX_OBJECTS_PER_COST_REQUEST = 256; - LLSD httpResults = result[LLCoreHttpUtil::HttpCoroutineAdapter::HTTP_RESULTS]; - LLCore::HttpStatus status = LLCoreHttpUtil::HttpCoroutineAdapter::getStatusFromLLSD(httpResults); - - if (!status || result.has("error")) + for (uuid_set_t::iterator chunk_begin = diff.begin(); chunk_begin != diff.end(); ) { - if (result.has("error")) + LLSD idList(LLSD::emptyArray()); + uuid_set_t::iterator it = chunk_begin; + for (size_t count = 0; count < MAX_OBJECTS_PER_COST_REQUEST && it != diff.end(); ++count, ++it) { - LL_WARNS() << "Application level error when fetching object " - << "cost. Message: " << result["error"]["message"].asString() - << ", identifier: " << result["error"]["identifier"].asString() - << LL_ENDL; - - // TODO*: Adaptively adjust request size if the - // service says we've requested too many and retry + idList.append(*it); } - reportObjectCostFailure(idList); + chunk_begin = it; - return; - } + LLSD postData = LLSD::emptyMap(); + postData["object_ids"] = idList; - // Success, grab the resource cost and linked set costs - // for an object if one was returned - for (LLSD::array_iterator it = idList.beginArray(); it != idList.endArray(); ++it) - { - LLUUID objectId = it->asUUID(); + LLSD result; + try + { + result = httpAdapter->postAndSuspend(httpRequest, url, postData); + } + catch (...) + { + // Coroutine teardown mid-sequence (e.g. LLCoros::Stop at disconnect). + // Retire this run's unresolved ids so they aren't stranded in + // mPendingObjectCost, which would exclude them from every future + // fetch via the set_difference above. + for (LLSD::array_const_iterator cit = idList.beginArray(); cit != idList.endArray(); ++cit) + { + mPendingObjectCost.erase(cit->asUUID()); + } + for (uuid_set_t::iterator rest = chunk_begin; rest != diff.end(); ++rest) + { + mPendingObjectCost.erase(*rest); + } + throw; + } - // Object could have been added to the mStaleObjectCost after request started - mStaleObjectCost.erase(objectId); - mPendingObjectCost.erase(objectId); + LLSD httpResults = result[LLCoreHttpUtil::HttpCoroutineAdapter::HTTP_RESULTS]; + LLCore::HttpStatus status = LLCoreHttpUtil::HttpCoroutineAdapter::getStatusFromLLSD(httpResults); - // Check to see if the request contains data for the object - if (result.has(it->asString())) + if (!status || result.has("error")) { - LLSD objectData = result[it->asString()]; - - F32 linkCost = (F32)objectData["linked_set_resource_cost"].asReal(); - F32 objectCost = (F32)objectData["resource_cost"].asReal(); - F32 physicsCost = (F32)objectData["physics_cost"].asReal(); - F32 linkPhysicsCost = (F32)objectData["linked_set_physics_cost"].asReal(); + if (result.has("error")) + { + LL_WARNS() << "Application level error when fetching object " + << "cost. Message: " << result["error"]["message"].asString() + << ", identifier: " << result["error"]["identifier"].asString() + << LL_ENDL; + } + reportObjectCostFailure(idList); - gObjectList.updateObjectCost(objectId, objectCost, linkCost, physicsCost, linkPhysicsCost); + // A failed chunk shouldn't abort the remaining ones. + continue; } - else + + // Success, grab the resource cost and linked set costs + // for an object if one was returned + for (LLSD::array_iterator result_it = idList.beginArray(); result_it != idList.endArray(); ++result_it) { - // TODO*: Give user feedback about the missing data? - gObjectList.onObjectCostFetchFailure(objectId); + LLUUID objectId = result_it->asUUID(); + + // Object could have been added to the mStaleObjectCost after request started + mStaleObjectCost.erase(objectId); + mPendingObjectCost.erase(objectId); + + // Check to see if the request contains data for the object + if (result.has(result_it->asString())) + { + LLSD objectData = result[result_it->asString()]; + + F32 linkCost = (F32)objectData["linked_set_resource_cost"].asReal(); + F32 objectCost = (F32)objectData["resource_cost"].asReal(); + F32 physicsCost = (F32)objectData["physics_cost"].asReal(); + F32 linkPhysicsCost = (F32)objectData["linked_set_physics_cost"].asReal(); + + gObjectList.updateObjectCost(objectId, objectCost, linkCost, physicsCost, linkPhysicsCost); + } + else + { + // TODO*: Give user feedback about the missing data? + gObjectList.onObjectCostFetchFailure(objectId); + } } } diff --git a/indra/newview/skins/default/xui/en/floater_inspect.xml b/indra/newview/skins/default/xui/en/floater_inspect.xml index f0d30ce95c..3b0d006b69 100644 --- a/indra/newview/skins/default/xui/en/floater_inspect.xml +++ b/indra/newview/skins/default/xui/en/floater_inspect.xml @@ -108,6 +108,54 @@ VRAM: [VRAM_USAGE] KB name="vramcount" width="55" sort_column="vramcount_sort"/> + + + + + + + + + + + + + + + + diff --git a/indra/newview/skins/default/xui/en/floater_scene_explorer.xml b/indra/newview/skins/default/xui/en/floater_scene_explorer.xml new file mode 100644 index 0000000000..ecb010aadb --- /dev/null +++ b/indra/newview/skins/default/xui/en/floater_scene_explorer.xml @@ -0,0 +1,692 @@ + + + No matching objects + + + + + + + + + + + + + + + + + + + + + + + + + + + +