diff options
| author | Erik Kundiman <erik@megapahit.org> | 2026-09-01 18:35:23 +0800 |
|---|---|---|
| committer | Erik Kundiman <erik@megapahit.org> | 2026-09-04 21:58:53 +0800 |
| commit | 00bb3bb6f07660bd914d29a1389342bb3060d254 (patch) | |
| tree | 431f574922494d2201718f13085f17bc6bd7fb42 /indra/llui | |
| parent | a8636720129952c10cf49ab80da21bf8b340b96c (diff) | |
| parent | 4ef9f8f14b35ed388c294d53767a0dca0e890924 (diff) | |
Merge remote-tracking branch 'secondlife/release/26.4' into 26.4
Diffstat (limited to 'indra/llui')
41 files changed, 1382 insertions, 222 deletions
diff --git a/indra/llui/CMakeLists.txt b/indra/llui/CMakeLists.txt index 9077670b67..18329b3b8a 100644 --- a/indra/llui/CMakeLists.txt +++ b/indra/llui/CMakeLists.txt @@ -41,6 +41,7 @@ set(llui_SOURCE_FILES llfloaterreglistener.cpp llflyoutbutton.cpp llfocusmgr.cpp + llgestureautocompletehelper.cpp llfolderview.cpp llfolderviewitem.cpp llfolderviewmodel.cpp @@ -154,6 +155,7 @@ set(llui_HEADER_FILES llfloaterreglistener.h llflyoutbutton.h llfocusmgr.h + llgestureautocompletehelper.h llfolderview.h llfolderviewitem.h llfolderviewmodel.h @@ -278,6 +280,10 @@ if (CMAKE_CXX_COMPILER_ID MATCHES GNU) set_source_files_properties(llxuiparser.cpp PROPERTIES COMPILE_FLAGS -Wno-sfinae-incomplete) endif () +if (USE_PRECOMPILED_HEADERS) + target_precompile_headers(llui REUSE_FROM llprecompiled) +endif () + # Add tests if(LL_TESTS) include(LLAddBuildTest) @@ -288,10 +294,8 @@ if(LL_TESTS) ) set_property( SOURCE ${llui_TEST_SOURCE_FILES} PROPERTY LL_TEST_ADDITIONAL_LIBRARIES ${test_libs}) LL_ADD_PROJECT_UNIT_TESTS(llui "${llui_TEST_SOURCE_FILES}") - # INTEGRATION TESTS - if(NOT LINUX) - set(test_libs llui llmessage llcorehttp llxml llrender llcommon ll::hunspell ) - LL_ADD_INTEGRATION_TEST(llurlentry llurlentry.cpp "${test_libs}") - endif(NOT LINUX) + # INTEGRATION TESTS + set(test_libs llui llmessage llcorehttp llxml llrender llcommon ll::hunspell ) + LL_ADD_INTEGRATION_TEST(llurlentry llurlentry.cpp "${test_libs}") endif(LL_TESTS) diff --git a/indra/llui/llcallbackmap.h b/indra/llui/llcallbackmap.h index 69b80db1e8..3115606d91 100644 --- a/indra/llui/llcallbackmap.h +++ b/indra/llui/llcallbackmap.h @@ -27,11 +27,9 @@ #ifndef LLCALLBACKMAP_H #define LLCALLBACKMAP_H -#include "llstl.h" - +#include <map> #include <string> #include <functional> -#include <unordered_map> class LLCallbackMap { @@ -39,7 +37,7 @@ public: // callback definition. typedef std::function<void* (void* data)> callback_t; - typedef std::unordered_map<std::string, LLCallbackMap> map_t; + typedef std::map<std::string, LLCallbackMap> map_t; typedef map_t::iterator map_iter_t; typedef map_t::const_iterator map_const_iter_t; diff --git a/indra/llui/llcheckboxctrl.h b/indra/llui/llcheckboxctrl.h index 4068741978..3507d3f34a 100644 --- a/indra/llui/llcheckboxctrl.h +++ b/indra/llui/llcheckboxctrl.h @@ -124,6 +124,11 @@ public: virtual bool isDirty() const; // Returns true if the user has modified this control. virtual void resetDirty(); // Clear dirty state + LLTextBox* getTextBox() // Provide direct access to label textbox control + { + return mLabel; + } + protected: virtual std::string _getSearchText() const { diff --git a/indra/llui/llcombobox.cpp b/indra/llui/llcombobox.cpp index 01463b4962..7535295312 100644 --- a/indra/llui/llcombobox.cpp +++ b/indra/llui/llcombobox.cpp @@ -251,11 +251,26 @@ void LLComboBox::resetDirty() } } -bool LLComboBox::itemExists(const std::string& name) +bool LLComboBox::itemExists(const std::string& name) const { return mList->getItemByLabel(name); } +bool LLComboBox::valueExists(const std::string& value) const +{ + return mList->getItemByValue(value); +} + +LLScrollListItem* LLComboBox::findItemByValue(const std::string& value) const +{ + return mList->getItemByValue(value); +} + +std::vector<LLScrollListItem*> LLComboBox::getAllData() const +{ + return mList->getAllData(); +} + // add item "name" to menu LLScrollListItem* LLComboBox::add(const std::string& name, EAddPosition pos, bool enabled) { @@ -521,7 +536,7 @@ bool LLComboBox::setCurrentByIndex(S32 index) if (item->getEnabled()) { mList->selectItem(item, -1, true); - LLSD::String label = item->getColumn(0)->getValue().asString(); + LLSD::String label = getSelectedItemLabel(); if (mTextEntry) { mTextEntry->setText(label); diff --git a/indra/llui/llcombobox.h b/indra/llui/llcombobox.h index dad60ffb65..acd63e0198 100644 --- a/indra/llui/llcombobox.h +++ b/indra/llui/llcombobox.h @@ -142,7 +142,10 @@ public: LLScrollListItem* addSeparator(EAddPosition pos = ADD_BOTTOM); bool remove( S32 index ); // remove item by index, return true if found and removed void removeall() { clearRows(); } - bool itemExists(const std::string& name); + bool itemExists(const std::string& name) const; + bool valueExists(const std::string& value) const; + LLScrollListItem* findItemByValue(const std::string& value) const; + std::vector<LLScrollListItem*> getAllData() const; void sortByName(bool ascending = true); // Sort the entries in the combobox by name diff --git a/indra/llui/llemojihelper.h b/indra/llui/llemojihelper.h index 26840eef94..0dc74b1a74 100644 --- a/indra/llui/llemojihelper.h +++ b/indra/llui/llemojihelper.h @@ -69,5 +69,5 @@ private: boost::signals2::connection mHelperCommitConn; boost::signals2::connection mHelperCloseConn; std::function<void(llwchar)> mEmojiCommitCb; - bool mIsHideDisabled; + bool mIsHideDisabled = false; }; diff --git a/indra/llui/llflatlistview.cpp b/indra/llui/llflatlistview.cpp index 34eb1ea3fc..d863cc5143 100644 --- a/indra/llui/llflatlistview.cpp +++ b/indra/llui/llflatlistview.cpp @@ -31,6 +31,14 @@ #include "llflatlistview.h" +#include "llfocusmgr.h" +#include "llrender2dutils.h" +#include "llui.h" +#include "lluicolortable.h" +#include "llwindow.h" + +#include <algorithm> + static const LLDefaultChildRegistry::Register<LLFlatListView> flat_list_view("flat_list_view"); const LLSD SELECTED_EVENT = LLSD().with("selected", true); @@ -45,17 +53,28 @@ LLFlatListView::Params::Params() multi_select("multi_select"), keep_one_selected("keep_one_selected"), keep_selection_visible_on_reshape("keep_selection_visible_on_reshape",false), + allow_reorder("allow_reorder", false), + drag_indicator_color("drag_indicator_color"), no_items_text("no_items_text") {}; void LLFlatListView::reshape(S32 width, S32 height, bool called_from_parent /* = true */) { S32 delta = height - getRect().getHeight(); + LLRect visible_rc; + LLRect selected_rc = getLastSelectedItemRect(); + bool keep_selection_visible = false; + if (delta != 0 && mKeepSelectionVisibleOnReshape && selected_rc.isValid()) + { + visible_rc = getVisibleContentRect(); + keep_selection_visible = visible_rc.overlaps(selected_rc); + } + LLScrollContainer::reshape(width, height, called_from_parent); setItemsNoScrollWidth(width); rearrangeItems(); - if(delta!= 0 && mKeepSelectionVisibleOnReshape) + if(keep_selection_visible) { ensureSelectedVisible(); } @@ -91,7 +110,7 @@ bool LLFlatListView::addItem(LLPanel * item, const LLSD& value /*= LLUUID::null* } //_4 is for MASK - item->setMouseDownCallback(boost::bind(&LLFlatListView::onItemMouseClick, this, new_pair, _4)); + item->setMouseDownCallback(boost::bind(&LLFlatListView::onItemMouseClick, this, new_pair, _4, CLICK_LEFT)); item->setRightMouseDownCallback(boost::bind(&LLFlatListView::onItemRightMouseClick, this, new_pair, _4)); // Children don't accept the focus @@ -142,7 +161,7 @@ bool LLFlatListView::addItemPairs(pairs_list_t panel_list, bool rearrange /*= tr mItemsPanel->addChild(panel); //_4 is for MASK - panel->setMouseDownCallback(boost::bind(&LLFlatListView::onItemMouseClick, this, new_pair, _4)); + panel->setMouseDownCallback(boost::bind(&LLFlatListView::onItemMouseClick, this, new_pair, _4, CLICK_LEFT)); panel->setRightMouseDownCallback(boost::bind(&LLFlatListView::onItemRightMouseClick, this, new_pair, _4)); // Children don't accept the focus panel->setTabStop(false); @@ -165,7 +184,7 @@ bool LLFlatListView::addItemPairs(pairs_list_t panel_list, bool rearrange /*= tr mItemsPanel->addChild(panel); //_4 is for MASK - panel->setMouseDownCallback(boost::bind(&LLFlatListView::onItemMouseClick, this, item_pair, _4)); + panel->setMouseDownCallback(boost::bind(&LLFlatListView::onItemMouseClick, this, item_pair, _4, CLICK_LEFT)); panel->setRightMouseDownCallback(boost::bind(&LLFlatListView::onItemRightMouseClick, this, item_pair, _4)); // Children don't accept the focus panel->setTabStop(false); @@ -217,7 +236,7 @@ bool LLFlatListView::insertItemAfter(LLPanel* after_item, LLPanel* item_to_add, } //_4 is for MASK - item_to_add->setMouseDownCallback(boost::bind(&LLFlatListView::onItemMouseClick, this, new_pair, _4)); + item_to_add->setMouseDownCallback(boost::bind(&LLFlatListView::onItemMouseClick, this, new_pair, _4, CLICK_LEFT)); item_to_add->setRightMouseDownCallback(boost::bind(&LLFlatListView::onItemRightMouseClick, this, new_pair, _4)); rearrangeItems(); @@ -396,6 +415,11 @@ U32 LLFlatListView::size(const bool only_visible_items) const void LLFlatListView::clear() { + if (mReorderDragPair) + { + cancelReorderDrag(); + } + // This will clear mSelectedItemPairs, calling all appropriate callbacks. resetSelection(); @@ -460,6 +484,16 @@ LLFlatListView::LLFlatListView(const LLFlatListView::Params& p) , mIsConsecutiveSelection(false) , mKeepSelectionVisibleOnReshape(p.keep_selection_visible_on_reshape) , mFocusOnItemClicked(true) + , mAllowReorder(p.allow_reorder) + , mIsReordering(false) + , mReorderDragPair(nullptr) + , mDeferredSelectPair(nullptr) + , mReorderMouseDownX(0) + , mReorderMouseDownY(0) + , mReorderInsertIndex(-1) + , mDragIndicatorColor(p.drag_indicator_color.isProvided() + ? p.drag_indicator_color() + : LLUIColorTable::instance().getColor("EmphasisColor")) { mBorderThickness = getBorderWidth(); @@ -520,12 +554,29 @@ LLFlatListView::~LLFlatListView() // virtual void LLFlatListView::draw() { + // Keep scrolling while a reorder drag rests over the top/bottom edge, so the + // user does not have to drop, scroll, and grab again. + if (mIsReordering) + { + S32 local_x, local_y; + LLUI::getInstance()->getMousePositionLocal(this, &local_x, &local_y); + if (autoScroll(local_x, local_y)) + { + mReorderInsertIndex = constrainInsertIndex(getInsertIndexAt(local_x, local_y)); + } + } + // Highlight border if a child of this container has keyboard focus if( mSelectedItemsBorder->getVisible() ) { mSelectedItemsBorder->setKeyboardFocusHighlight( hasFocus() ); } LLScrollContainer::draw(); + + if (mIsReordering && mReorderInsertIndex >= 0) + { + drawReorderIndicator(); + } } // virtual @@ -602,7 +653,7 @@ void LLFlatListView::rearrangeItems() mSelectedItemsBorder->setRect(getLastSelectedItemRect().stretch(-1)); } -void LLFlatListView::onItemMouseClick(item_pair_t* item_pair, MASK mask) +void LLFlatListView::onItemMouseClick(item_pair_t* item_pair, MASK mask, EMouseClickType clicktype) { if (!item_pair) return; @@ -617,6 +668,11 @@ void LLFlatListView::onItemMouseClick(item_pair_t* item_pair, MASK mask) setFocus(true); } + if (clicktype == CLICK_LEFT && !(mask & (MASK_SHIFT | MASK_CONTROL))) + { + armReorderDrag(item_pair); + } + bool select_item = !isSelected(item_pair); //*TODO find a better place for that enforcing stuff @@ -695,6 +751,16 @@ void LLFlatListView::onItemMouseClick(item_pair_t* item_pair, MASK mask) return; } + // When a reorderable list has a multi-selection, a plain click on one of the + // selected rows must not collapse the selection yet: the user may be about to + // drag the whole selection. Defer collapsing to mouse-up if no drag happens. + if (mAllowReorder && mMultipleSelection && !(mask & (MASK_CONTROL | MASK_SHIFT)) + && isSelected(item_pair) && numSelected() > 1) + { + mDeferredSelectPair = item_pair; + return; + } + //no need to do additional commit on selection reset if (!(mask & MASK_CONTROL) || !mMultipleSelection) resetSelection(true); @@ -715,8 +781,387 @@ void LLFlatListView::onItemRightMouseClick(item_pair_t* item_pair, MASK mask) if ( !(mask & MASK_CONTROL) && mMultipleSelection && isSelected(item_pair) ) return; - // else got same behavior as at onItemMouseClick - onItemMouseClick(item_pair, mask); + // else got same behavior as at onItemMouseClick, but a right click must never start a drag + onItemMouseClick(item_pair, mask, CLICK_RIGHT); +} + +static constexpr S32 REORDER_DRAG_THRESHOLD = 5; + +void LLFlatListView::armReorderDrag(item_pair_t* item_pair) +{ + if (!mAllowReorder) + { + return; + } + + if (!item_pair || !item_pair->first) + { + return; + } + + if (size() < 2) + { + return; // nothing to reorder against + } + + if (gFocusMgr.getMouseCapture() && !hasMouseCapture()) + { + return; + } + + mReorderDragPair = item_pair; + mReorderDragGroup.clear(); + mDeferredSelectPair = nullptr; + mIsReordering = false; + mReorderInsertIndex = -1; + + LLUI::getInstance()->getMousePositionLocal(this, &mReorderMouseDownX, &mReorderMouseDownY); + gFocusMgr.setMouseCapture(this); +} + +void LLFlatListView::updateReorderDrag(S32 x, S32 y) +{ + if (!mReorderDragPair) + return; + + if (!mIsReordering) + { + if (abs(y - mReorderMouseDownY) < REORDER_DRAG_THRESHOLD + && abs(x - mReorderMouseDownX) < REORDER_DRAG_THRESHOLD) + { + return; // still within the click slop, not a drag yet + } + mIsReordering = true; + buildReorderGroup(); + } + + mReorderInsertIndex = constrainInsertIndex(getInsertIndexAt(x, y)); +} + +void LLFlatListView::buildReorderGroup() +{ + mReorderDragGroup.clear(); + + bool drag_selection = mMultipleSelection && isSelected(mReorderDragPair) && numSelected() > 1; + + for (item_pair_t* pair : mItemPairs) + { + if (!pair->first->getVisible()) + continue; + + if (pair == mReorderDragPair) + { + mReorderDragGroup.push_back(pair); + } + else if (drag_selection && isSelected(pair) + && (!mReorderValidateCallback || mReorderValidateCallback(mReorderDragPair->second, pair->second))) + { + // only carry along selected rows that belong to the grabbed row's group + mReorderDragGroup.push_back(pair); + } + } +} + +bool LLFlatListView::isInReorderGroup(item_pair_t* item_pair) const +{ + return std::find(mReorderDragGroup.begin(), mReorderDragGroup.end(), item_pair) != mReorderDragGroup.end(); +} + +void LLFlatListView::getReorderRemaining(pairs_list_t& remaining) const +{ + remaining.clear(); + for (item_pair_t* pair : mItemPairs) + { + if (!pair->first->getVisible() || isInReorderGroup(pair)) + continue; + remaining.push_back(pair); + } +} + +void LLFlatListView::finishReorderDrag() +{ + if (mReorderDragPair && mIsReordering && mReorderInsertIndex >= 0) + { + pairs_list_t remaining; + getReorderRemaining(remaining); + + // Resolve the drop boundary to the remaining row it lands before (nullptr = append). + item_pair_t* anchor = nullptr; + S32 cur = 0; + for (item_pair_t* pair : remaining) + { + if (cur == mReorderInsertIndex) + { + anchor = pair; + break; + } + ++cur; + } + + LLSD moved_value = mReorderDragPair->second; + + for (item_pair_t* pair : mReorderDragGroup) + { + mItemPairs.remove(pair); + } + + pairs_iterator_t it = (anchor != nullptr) + ? std::find(mItemPairs.begin(), mItemPairs.end(), anchor) + : mItemPairs.end(); + for (item_pair_t* pair : mReorderDragGroup) + { + mItemPairs.insert(it, pair); // preserves group order; it stays before anchor + } + + rearrangeItems(); + + if (mReorderCallback) + { + S32 visible_index = 0; + for (item_pair_t* pair : mItemPairs) + { + if (!pair->first->getVisible()) + continue; + if (pair == mReorderDragPair) + break; + ++visible_index; + } + + mReorderCallback(moved_value, visible_index); + } + } + + cancelReorderDrag(); +} + +void LLFlatListView::cancelReorderDrag() +{ + if (hasMouseCapture()) + { + gFocusMgr.setMouseCapture(nullptr); + } + + clearReorderDragState(); +} + +void LLFlatListView::clearReorderDragState() +{ + if (mIsReordering) + { + getWindow()->setCursor(UI_CURSOR_ARROW); + } + + bool was_armed = (mReorderDragPair != nullptr); + + mReorderDragPair = nullptr; + mReorderDragGroup.clear(); + mDeferredSelectPair = nullptr; + mIsReordering = false; + mReorderInsertIndex = -1; + + if (was_armed && mReorderEndedCallback) + { + mReorderEndedCallback(); + } +} + +void LLFlatListView::onMouseCaptureLost() +{ + clearReorderDragState(); +} + +void LLFlatListView::onVisibilityChange(bool new_visibility) +{ + if (!new_visibility && mReorderDragPair) + { + cancelReorderDrag(); + } + + LLScrollContainer::onVisibilityChange(new_visibility); +} + +S32 LLFlatListView::getInsertIndexAt(S32 x, S32 y) const +{ + S32 panel_x, panel_y; + localPointToOtherView(x, y, &panel_x, &panel_y, mItemsPanel); + + // The drop boundary among the remaining rows is the count of remaining rows + // whose vertical centre sits above the cursor. + S32 index = 0; + for (item_pair_t* pair : mItemPairs) + { + if (!pair->first->getVisible() || isInReorderGroup(pair)) + continue; + + if (pair->first->getRect().getCenterY() > panel_y) + { + ++index; + } + } + return index; +} + +LLFlatListView::item_pair_t* LLFlatListView::getReorderPairAt(S32 x, S32 y) const +{ + S32 panel_x, panel_y; + localPointToOtherView(x, y, &panel_x, &panel_y, mItemsPanel); + + for (item_pair_t* pair : mItemPairs) + { + if (!pair->first->getVisible()) + continue; + + // Claim the padding above each row so gap presses still resolve to a row. + LLRect rc = pair->first->getRect(); + rc.mTop += mItemPad; + if (rc.pointInRect(panel_x, panel_y)) + { + return pair; + } + } + return nullptr; +} + +S32 LLFlatListView::constrainInsertIndex(S32 dest_index) const +{ + if (!mReorderValidateCallback) + return dest_index; + + // Clamp the boundary to the contiguous run of remaining rows that share the + // grabbed row's group, so a drag can't leave its group. + pairs_list_t remaining; + getReorderRemaining(remaining); + + const LLSD& dragged = mReorderDragPair->second; + S32 first_valid = -1; + S32 last_valid = -1; + S32 i = 0; + for (item_pair_t* pair : remaining) + { + if (mReorderValidateCallback(dragged, pair->second)) + { + if (first_valid < 0) + first_valid = i; + last_valid = i; + } + ++i; + } + + if (first_valid < 0) + return -1; // no valid neighbour (whole group is being dragged) + + if (dest_index < first_valid) + return first_valid; + if (dest_index > last_valid + 1) + return last_valid + 1; + return dest_index; +} + +void LLFlatListView::drawReorderIndicator() +{ + pairs_list_t remaining; + getReorderRemaining(remaining); + if (remaining.empty()) + return; + + const LLRect& panel_rc = mItemsPanel->getRect(); + const LLColor4& color = mDragIndicatorColor.get(); + + // faint highlight on each row being moved + for (item_pair_t* pair : mReorderDragGroup) + { + const LLRect& dr = pair->first->getRect(); + gl_rect_2d(panel_rc.mLeft + dr.mLeft, panel_rc.mBottom + dr.mTop, + panel_rc.mLeft + dr.mRight, panel_rc.mBottom + dr.mBottom, + color % 0.15f, true); + } + + // insertion line at the drop boundary among the remaining rows + S32 count = (S32)remaining.size(); + bool at_end = mReorderInsertIndex >= count; + S32 anchor_idx = at_end ? count - 1 : mReorderInsertIndex; + + item_pair_t* anchor = nullptr; + S32 cur = 0; + for (item_pair_t* pair : remaining) + { + if (cur == anchor_idx) + { + anchor = pair; + break; + } + ++cur; + } + if (!anchor) + return; + + const LLRect& item_rc = anchor->first->getRect(); + S32 left = panel_rc.mLeft + item_rc.mLeft; + S32 right = panel_rc.mLeft + item_rc.mRight; + S32 line_y = panel_rc.mBottom + (at_end ? item_rc.mBottom : item_rc.mTop); + + if (line_y < 0 || line_y > getRect().getHeight()) + return; + + gl_rect_2d(left, line_y, right, line_y - 1, color, true); +} + +bool LLFlatListView::handleHover(S32 x, S32 y, MASK mask) +{ + if (mAllowReorder && mReorderDragPair && hasMouseCapture()) + { + updateReorderDrag(x, y); + if (mIsReordering) + { + getWindow()->setCursor(UI_CURSOR_ARROWDRAG); + return true; + } + } + return LLScrollContainer::handleHover(x, y, mask); +} + +bool LLFlatListView::handleMouseDown(S32 x, S32 y, MASK mask) +{ + bool handled = LLScrollContainer::handleMouseDown(x, y, mask); + + // A press in the padding between rows misses every item, so the per-item + // mouse-down never arms a reorder. Map it to the nearest row and arm there; + // armReorderDrag's capture guard ignores presses a child control already took. + if (mAllowReorder && !mReorderDragPair && !(mask & (MASK_CONTROL | MASK_SHIFT))) + { + if (item_pair_t* pair = getReorderPairAt(x, y)) + { + onItemMouseClick(pair, mask, CLICK_LEFT); + } + } + + return handled || (mReorderDragPair != nullptr); +} + +bool LLFlatListView::handleMouseUp(S32 x, S32 y, MASK mask) +{ + if (mReorderDragPair && hasMouseCapture()) + { + bool was_reordering = mIsReordering; + item_pair_t* deferred = mDeferredSelectPair; + mDeferredSelectPair = nullptr; + + finishReorderDrag(); + + if (was_reordering) + { + return true; // a drag happened: keep the (multi-)selection on the moved rows + } + + // No drag: apply the selection collapse that was deferred on mouse-down. + if (deferred) + { + resetSelection(true); + selectItemPair(deferred, true); + return true; + } + } + return LLScrollContainer::handleMouseUp(x, y, mask); } bool LLFlatListView::handleKeyHere(KEY key, MASK mask) @@ -1087,6 +1532,18 @@ bool LLFlatListView::removeItemPair(item_pair_t* item_pair, bool rearrange) { llassert(item_pair); + bool removing_reorder_pair = item_pair == mReorderDragPair + || std::find(mReorderDragGroup.begin(), mReorderDragGroup.end(), item_pair) != mReorderDragGroup.end(); + + if (removing_reorder_pair) + { + cancelReorderDrag(); + } + if (item_pair == mDeferredSelectPair) + { + mDeferredSelectPair = nullptr; + } + bool deleted = false; bool selection_changed = false; for (pairs_iterator_t it = mItemPairs.begin(); it != mItemPairs.end(); ++it) diff --git a/indra/llui/llflatlistview.h b/indra/llui/llflatlistview.h index 39afa33be8..ae20a4b1eb 100644 --- a/indra/llui/llflatlistview.h +++ b/indra/llui/llflatlistview.h @@ -30,6 +30,7 @@ #include "llpanel.h" #include "llscrollcontainer.h" #include "lltextbox.h" +#include "lluicolor.h" /** @@ -106,12 +107,28 @@ public: /** padding between items */ Optional<U32> item_pad; + /** allow items to be reordered by dragging them with the mouse */ + Optional<bool> allow_reorder; + + /** colour of the insertion indicator drawn while reordering */ + Optional<LLUIColor> drag_indicator_color; + /** textbox with info message when list is empty*/ Optional<LLTextBox::Params> no_items_text; Params(); }; + /** Fired after the user drags an item to a new position: (moved value, new visible index). */ + typedef boost::function<void(const LLSD& value, S32 new_index)> reorder_signal_t; + + /** + * Returns true if the dragged item is allowed to move past the given neighbour. + * Lets owners constrain reordering (e.g. keep items within a group). Optional; + * when unset any reorder is permitted. + */ + typedef boost::function<bool(const LLSD& dragged, const LLSD& neighbour)> reorder_validate_signal_t; + // disable traversal when finding widget to hand focus off to /*virtual*/ bool canFocusChildren() const override { return false; } @@ -259,6 +276,18 @@ public: /** Turn on/off selection support */ void setAllowSelection(bool can_select) { mAllowSelection = can_select; } + /** Turn on/off drag-to-reorder support */ + void setAllowReorder(bool allow) { mAllowReorder = allow; } + + void setReorderCallback(reorder_signal_t cb) { mReorderCallback = cb; } + void setReorderValidateCallback(reorder_validate_signal_t cb) { mReorderValidateCallback = cb; } + + /** Fired when a reorder grab is released, whether or not it moved anything */ + void setReorderEndedCallback(boost::function<void()> cb) { mReorderEndedCallback = cb; } + + /** True while an item is grabbed for a drag-to-reorder (before or during the drag) */ + bool isReorderActive() const { return mReorderDragPair != NULL; } + /** Sets flag whether onCommit should be fired if selection was changed */ // FIXME: this should really be a separate signal, since "Commit" implies explicit user action, and selection changes can happen more indirectly. void setCommitOnSelectionChange(bool b) { mCommitOnSelectionChange = b; } @@ -330,7 +359,7 @@ protected: LLFlatListView(const LLFlatListView::Params& p); /** Manage selection on mouse events */ - void onItemMouseClick(item_pair_t* item_pair, MASK mask); + void onItemMouseClick(item_pair_t* item_pair, MASK mask, EMouseClickType clicktype); void onItemRightMouseClick(item_pair_t* item_pair, MASK mask); @@ -368,6 +397,16 @@ protected: virtual bool handleKeyHere(KEY key, MASK mask) override; + virtual bool handleHover(S32 x, S32 y, MASK mask) override; + + virtual bool handleMouseDown(S32 x, S32 y, MASK mask) override; + + virtual bool handleMouseUp(S32 x, S32 y, MASK mask) override; + + virtual void onMouseCaptureLost() override; + + virtual void onVisibilityChange(bool new_visibility) override; + virtual bool postBuild() override; virtual void onFocusReceived() override; @@ -384,6 +423,29 @@ protected: private: + // Drag-to-reorder helpers. + void armReorderDrag(item_pair_t* item_pair); + void updateReorderDrag(S32 x, S32 y); + void finishReorderDrag(); + void cancelReorderDrag(); + void clearReorderDragState(); + + // Builds the set of pairs being dragged (the whole selection when the grabbed + // row is part of a multi-selection, constrained to the validator's group). + void buildReorderGroup(); + bool isInReorderGroup(item_pair_t* item_pair) const; + + // The remaining visible pairs (those not being dragged), in visual order. + void getReorderRemaining(pairs_list_t& remaining) const; + // Number of leading non-dragged items whose centre sits above (x, y). + S32 getInsertIndexAt(S32 x, S32 y) const; + // Row under (x, y), counting the padding above each row, so presses in the + // inter-row gaps still map to a row instead of falling through. + item_pair_t* getReorderPairAt(S32 x, S32 y) const; + // Clamps an insertion boundary to the validator's contiguous group. + S32 constrainInsertIndex(S32 dest_index) const; + void drawReorderIndicator(); + void setItemsNoScrollWidth(S32 new_width) {mItemsNoScrollWidth = new_width - 2 * mBorderThickness;} void setNoItemsCommentVisible(bool visible) const; @@ -427,6 +489,20 @@ private: bool mFocusOnItemClicked; + /** Drag-to-reorder state */ + bool mAllowReorder; + bool mIsReordering; // a drag is currently in progress + item_pair_t* mReorderDragPair; // the grabbed pair (drag anchor) + pairs_list_t mReorderDragGroup; // all pairs being dragged, in visual order + item_pair_t* mDeferredSelectPair; // collapse selection to this on mouse-up if no drag + S32 mReorderMouseDownX; + S32 mReorderMouseDownY; + S32 mReorderInsertIndex; // current drop boundary among the remaining (non-dragged) items + LLUIColor mDragIndicatorColor; + reorder_signal_t mReorderCallback; + reorder_validate_signal_t mReorderValidateCallback; + boost::function<void()> mReorderEndedCallback; + /** All pairs of the list */ pairs_list_t mItemPairs; diff --git a/indra/llui/llfloater.cpp b/indra/llui/llfloater.cpp index 6de2c18620..b5e26db1b5 100644 --- a/indra/llui/llfloater.cpp +++ b/indra/llui/llfloater.cpp @@ -250,6 +250,7 @@ LLFloater::LLFloater(const LLSD& key, const LLFloater::Params& p) mShortTitle(p.short_title), mSingleInstance(p.single_instance), mReuseInstance(p.reuse_instance.isProvided() ? p.reuse_instance : p.single_instance), // reuse single-instance floaters by default + mIsReuseInitialized(p.reuse_instance.isProvided()), mKey(key), mCanTearOff(p.can_tear_off), mCanMinimize(p.can_minimize), @@ -3359,6 +3360,7 @@ void LLFloater::initFromParams(const LLFloater::Params& p) mLegacyHeaderHeight = p.legacy_header_height; mSingleInstance = p.single_instance; mReuseInstance = p.reuse_instance.isProvided() ? p.reuse_instance : p.single_instance; + mIsReuseInitialized = p.reuse_instance.isProvided(); mDefaultRelativeX = p.rel_x; mDefaultRelativeY = p.rel_y; diff --git a/indra/llui/llflyoutbutton.cpp b/indra/llui/llflyoutbutton.cpp index 2e198e8dbc..9d511bcf87 100644 --- a/indra/llui/llflyoutbutton.cpp +++ b/indra/llui/llflyoutbutton.cpp @@ -31,7 +31,7 @@ //static LLDefaultChildRegistry::Register<LLFlyoutButton> r2("flyout_button"); -const S32 FLYOUT_BUTTON_ARROW_WIDTH = 24; +constexpr S32 FLYOUT_BUTTON_ARROW_WIDTH = 22; LLFlyoutButton::LLFlyoutButton(const Params& p) : LLComboBox(p), @@ -43,9 +43,13 @@ LLFlyoutButton::LLFlyoutButton(const Params& p) LLButton::Params bp(p.action_button); bp.name(p.label); bp.label(p.label); - bp.rect.left(0).bottom(0).width(getRect().getWidth() - FLYOUT_BUTTON_ARROW_WIDTH).height(getRect().getHeight()); + bp.rect.left(0).bottom(0).width(getRect().getWidth() - FLYOUT_BUTTON_ARROW_WIDTH - 2 * BTN_DROP_SHADOW).height(getRect().getHeight()); bp.click_callback.function(boost::bind(&LLFlyoutButton::onActionButtonClick, this, _2)); bp.follows.flags(FOLLOWS_ALL); + if (p.font.isProvided()) + { + bp.font(p.font); + } mActionButton = LLUICtrlFactory::create<LLButton>(bp); addChild(mActionButton); @@ -74,4 +78,16 @@ void LLFlyoutButton::setToggleState(bool state) mToggleState = state; } +bool LLFlyoutButton::postBuild() +{ + bool result = LLComboBox::postBuild(); + + LLRect rect = getLocalRect(); + S32 arrow_width = llmax(FLYOUT_BUTTON_ARROW_WIDTH, mArrowImage ? mArrowImage->getWidth() : 0); + mButton->setRect(LLRect(getRect().getWidth() - llmax(8, arrow_width) - 2 * BTN_DROP_SHADOW, + rect.mTop, rect.mRight, rect.mBottom)); + mButton->setFollows(FOLLOWS_BOTTOM | FOLLOWS_TOP | FOLLOWS_RIGHT); + + return result; +} diff --git a/indra/llui/llflyoutbutton.h b/indra/llui/llflyoutbutton.h index 73190fc984..81cdcdc382 100644 --- a/indra/llui/llflyoutbutton.h +++ b/indra/llui/llflyoutbutton.h @@ -56,6 +56,8 @@ protected: public: void draw() override; + bool postBuild() override; + void setToggleState(bool state); void onActionButtonClick(const LLSD& data); diff --git a/indra/llui/llfolderview.cpp b/indra/llui/llfolderview.cpp index db4ab8487e..baab17830b 100644 --- a/indra/llui/llfolderview.cpp +++ b/indra/llui/llfolderview.cpp @@ -4,7 +4,7 @@ * * $LicenseInfo:firstyear=2001&license=viewerlgpl$ * Second Life Viewer Source Code - * Copyright (C) 2010, Linden Research, Inc. + * Copyright (C) 2026, Linden Research, Inc. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -651,9 +651,11 @@ bool LLFolderView::startDrag() void LLFolderView::commitRename( const LLSD& data ) { + // Intentionally doesn't check mRenamer->getVisible(), + // since this can be called from 'focus lost' event. + // Ex: clicking inworld should commit the rename. finishRenamingItem(); arrange( NULL, NULL ); - } void LLFolderView::draw() @@ -727,13 +729,17 @@ void LLFolderView::draw() void LLFolderView::finishRenamingItem( void ) { - if(!mRenamer) + if (!mRenamer) { return; } if( mRenameItem ) { mRenameItem->rename( mRenamer->getText() ); + // Clear text to avoid duplicate renames. + // Ex: 'return' key will trigger rename from handleKeyHere + // and might trigger commitRename from focus lost event. + mRenamer->setText(LLStringUtil::null); } closeRenamer(); @@ -1423,6 +1429,7 @@ bool LLFolderView::search(LLFolderViewItem* first_item, const std::string &searc } } + // Note: for inventory getSearchableName should already be 'upper' case. std::string current_item_label(search_item->getViewModelItem()->getSearchableName()); LLStringUtil::toUpper(current_item_label); auto search_string_length = llmin(upper_case_string.size(), current_item_label.size()); diff --git a/indra/llui/llfolderviewitem.cpp b/indra/llui/llfolderviewitem.cpp index fcc1964bd6..c8ef421677 100644 --- a/indra/llui/llfolderviewitem.cpp +++ b/indra/llui/llfolderviewitem.cpp @@ -4,7 +4,7 @@ * * $LicenseInfo:firstyear=2001&license=viewerlgpl$ * Second Life Viewer Source Code -* Copyright (C) 2010, Linden Research, Inc. +* Copyright (C) 2026, Linden Research, Inc. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -190,7 +190,7 @@ LLFolderViewItem::LLFolderViewItem(const LLFolderViewItem::Params& p) mItemHeight(p.item_height), mControlLabelRotation(0.f), mDragAndDropTarget(false), - mLabel(utf8str_to_wstring(p.name)), + mLabel(utf8str_to_wstring(p.name)), // will be immediately reset in postBuild() mRoot(p.root), mViewModelItem(p.listener), mIsMouseOverTitle(false), @@ -244,21 +244,19 @@ bool LLFolderViewItem::postBuild() llassert(vmi); // not supposed to happen, if happens, find out why and fix if (vmi) { - // getDisplayName() is expensive (due to internal getLabelSuffix() and name building) - // it also sets search strings so it requires a filter reset + // First getDisplayName() is expensive due to internal + // lazy getLabelSuffix(), it is however needed as it sets + // search string, which can later determine visibility. + // Refreshing a search string also requires a filter reset. mLabel = utf8str_to_wstring(vmi->getDisplayName()); mIsFavorite = vmi->isFavorite() && !vmi->isItemInTrash(); - setToolTip(vmi->getName()); // Dirty the filter flag of the model from the view (CHUI-849) vmi->dirtyFilter(); } - // Don't do full refresh on constructor if it is possible to avoid + // Don't do full refresh on constructor if it is possible to avoid, // it significantly slows down bulk view creation. - // Todo: Ideally we need to move getDisplayName() out of constructor as well. - // Like: make a logic that will let filter update search string, - // while LLFolderViewItem::arrange() updates visual part mSuffixNeedsRefresh = true; mLabelWidthDirty = true; return true; @@ -363,7 +361,6 @@ void LLFolderViewItem::refresh() mLabel = utf8str_to_wstring(vmi.getDisplayName()); mLabelFontBuffer.reset(); mIsFavorite = vmi.isFavorite() && !vmi.isItemInTrash(); - setToolTip(vmi.getName()); // icons are slightly expensive to get, can be optimized // see LLInventoryIcon::getIcon() mIcon = vmi.getIcon(); @@ -623,6 +620,19 @@ const std::string& LLFolderViewItem::getName( void ) const return getViewModelItem() ? getViewModelItem()->getName() : noName; } +const std::string LLFolderViewItem::getToolTip() const +{ + // Return the item name as tooltip without storing it + if (!LLView::sDebugUnicode) + { + if (const LLFolderViewModelItem* vmi = getViewModelItem()) + { + return vmi->getName(); + } + } + return LLView::getToolTip(); +} + // LLView functionality bool LLFolderViewItem::handleRightMouseDown( S32 x, S32 y, MASK mask ) { diff --git a/indra/llui/llfolderviewitem.h b/indra/llui/llfolderviewitem.h index 258a806b91..8d6de2fee3 100644 --- a/indra/llui/llfolderviewitem.h +++ b/indra/llui/llfolderviewitem.h @@ -4,7 +4,7 @@ * * $LicenseInfo:firstyear=2001&license=viewerlgpl$ * Second Life Viewer Source Code -* Copyright (C) 2010, Linden Research, Inc. +* Copyright (C) 2026, Linden Research, Inc. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public @@ -253,6 +253,11 @@ public: // viewed. This method will ask the viewed object itself. const std::string& getName( void ) const; + // Override to provide lazy tooltip generation without memory overhead + // Inventory can consist of millions of items, yet most stay invisible, + // much less need to show a tooltip, so avoid storing tooltips. + virtual const std::string getToolTip() const; + // This method returns the label displayed on the view. This // method was primarily added to allow sorting on the folder // contents possible before the entire view has been constructed. diff --git a/indra/llui/llgestureautocompletehelper.cpp b/indra/llui/llgestureautocompletehelper.cpp new file mode 100644 index 0000000000..f00f1406e5 --- /dev/null +++ b/indra/llui/llgestureautocompletehelper.cpp @@ -0,0 +1,165 @@ +/** + * @file llgestureautocompletehelper.cpp + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2026, Linden Research, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + */ + +#include "linden_common.h" + +#include "llgestureautocompletehelper.h" + +#include "llfloater.h" +#include "llfloaterreg.h" +#include "llfocusmgr.h" +#include "lluictrl.h" + +constexpr char GESTURE_AUTOCOMPLETE_FLOATER[] = "gesture_autocomplete_picker"; + +bool LLGestureAutocompleteHelper::isActive(const LLUICtrl* ctrl) const +{ + return mHostHandle.get() == ctrl; +} + +void LLGestureAutocompleteHelper::showHelper( + LLUICtrl* host_ctrl, + const std::vector<Row>& rows, + size_t total, + std::function<void(std::string)> commit_cb) +{ + if (mHelperHandle.isDead()) + { + LLFloater* helper_floater = LLFloaterReg::getInstance(GESTURE_AUTOCOMPLETE_FLOATER); + mHelperHandle = helper_floater->getHandle(); + mHelperCommitConn = helper_floater->setCommitCallback( + [this](LLUICtrl*, const LLSD& param) { onCommitGesture(param.asString()); }); + } + + setHostCtrl(host_ctrl); + mRows = rows; + mTotal = total; + mGestureCommitCb = commit_cb; + + S32 floater_x, floater_y; + LLRect host_rect = host_ctrl->getRect(); + if (!host_ctrl->localPointToOtherView(0, host_rect.getHeight(), &floater_x, &floater_y, gFloaterView)) + { + LL_WARNS() << "Cannot show gesture autocomplete helper for non-floater controls." << LL_ENDL; + return; + } + + LLFloater* helper_floater = mHelperHandle.get(); + LLRect rect = helper_floater->getRect(); + rect.setLeftTopAndSize(floater_x, floater_y + rect.getHeight(), rect.getWidth(), rect.getHeight()); + helper_floater->setRect(rect); + + refreshPicker(); +} + +void LLGestureAutocompleteHelper::hideHelper(const LLUICtrl* ctrl) +{ + if (ctrl && !isActive(ctrl)) + { + return; + } + + setHostCtrl(nullptr); +} + +bool LLGestureAutocompleteHelper::handleKey(const LLUICtrl* ctrl, KEY key, MASK mask) +{ + if (mHelperHandle.isDead() || !isActive(ctrl)) + { + return false; + } + + return mHelperHandle.get()->handleKey(key, mask, true); +} + +void LLGestureAutocompleteHelper::onCommitGesture(const std::string& trigger) +{ + if (!mHostHandle.isDead() && mGestureCommitCb) + { + mGestureCommitCb(trigger); + } + + hideHelper(getHostCtrl()); +} + +void LLGestureAutocompleteHelper::refreshPicker() +{ + if (mHelperHandle.isDead()) + { + return; + } + + LLFloater* helper_floater = mHelperHandle.get(); + + if (helper_floater->isShown()) + { + helper_floater->onOpen(LLSD()); + } + else + { + helper_floater->openFloater(LLSD()); + } +} + +void LLGestureAutocompleteHelper::setHostCtrl(LLUICtrl* host_ctrl) +{ + const LLUICtrl* cur_host_ctrl = mHostHandle.get(); + + if (cur_host_ctrl != host_ctrl) + { + mHostCtrlFocusLostConn.disconnect(); + mHostHandle.markDead(); + mGestureCommitCb = {}; + mRows.clear(); + mTotal = 0; + + if (!mHelperHandle.isDead()) + { + mHelperHandle.get()->closeFloater(); + } + + if (host_ctrl) + { + mHostHandle = host_ctrl->getHandle(); + mHostCtrlFocusLostConn = host_ctrl->setFocusLostCallback( + [this](auto*) + { + // Scroll list grabs focus on click. + // Keep focus on the host when the click was ours. + LLFloater* helper_floater = mHelperHandle.get(); + if (helper_floater && gFocusMgr.childHasKeyboardFocus(helper_floater)) + { + if (LLUICtrl* host = getHostCtrl()) + { + host->setFocus(true); + } + return; + } + + hideHelper(getHostCtrl()); + }); + } + } +} diff --git a/indra/llui/llgestureautocompletehelper.h b/indra/llui/llgestureautocompletehelper.h new file mode 100644 index 0000000000..6292655fc7 --- /dev/null +++ b/indra/llui/llgestureautocompletehelper.h @@ -0,0 +1,80 @@ +/** + * @file llgestureautocompletehelper.h + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2026, Linden Research, Inc. + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; + * version 2.1 of the License only. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + * + * Linden Research, Inc., 945 Battery Street, San Francisco, CA 94111 USA + * $/LicenseInfo$ + */ + +#pragma once + +#include "llhandle.h" +#include "llsingleton.h" + +#include <boost/signals2.hpp> +#include <functional> +#include <string> +#include <vector> + +class LLFloater; +class LLUICtrl; + +class LLGestureAutocompleteHelper : public LLSingleton<LLGestureAutocompleteHelper> +{ + LLSINGLETON(LLGestureAutocompleteHelper) {} + ~LLGestureAutocompleteHelper() override {} + +public: + struct Row + { + std::string value; + std::string trigger; + std::string name; + }; + + bool isActive(const LLUICtrl* ctrl) const; + void showHelper( + LLUICtrl* host_ctrl, + const std::vector<Row>& rows, + size_t total, + std::function<void(std::string)> commit_cb); + void hideHelper(const LLUICtrl* ctrl = nullptr); + bool handleKey(const LLUICtrl* ctrl, KEY key, MASK mask); + void onCommitGesture(const std::string& trigger); + + const std::vector<Row>& rows() const { return mRows; } + size_t total() const { return mTotal; } + +protected: + void setHostCtrl(LLUICtrl* host_ctrl); + LLUICtrl* getHostCtrl() const { return mHostHandle.get(); } + +private: + void refreshPicker(); + + LLHandle<LLUICtrl> mHostHandle; + LLHandle<LLFloater> mHelperHandle; + boost::signals2::connection mHostCtrlFocusLostConn; + boost::signals2::connection mHelperCommitConn; + std::function<void(std::string)> mGestureCommitCb; + + std::vector<Row> mRows; + size_t mTotal = 0; +}; diff --git a/indra/llui/llkeywords.cpp b/indra/llui/llkeywords.cpp index 2bea8fb4ed..b1f1fb77f3 100644 --- a/indra/llui/llkeywords.cpp +++ b/indra/llui/llkeywords.cpp @@ -33,38 +33,20 @@ #include "llsdserialize.h" #include "lltexteditor.h" #include "llstl.h" +#include "llcontrol.h" + +extern LLControlGroup gSavedSettings; inline bool LLKeywordToken::isHead(const llwchar* s) const { - // strncmp is much faster than string compare - bool res = true; - const llwchar* t = mToken.c_str(); - auto len = mToken.size(); - for (S32 i=0; i<len; i++) - { - if (s[i] != t[i]) - { - res = false; - break; - } - } - return res; + size_t bytes = mToken.size() * sizeof(llwchar); + return std::memcmp(s, mToken.c_str(), bytes) == 0; } inline bool LLKeywordToken::isTail(const llwchar* s) const { - bool res = true; - const llwchar* t = mDelimiter.c_str(); - auto len = mDelimiter.size(); - for (S32 i=0; i<len; i++) - { - if (s[i] != t[i]) - { - res = false; - break; - } - } - return res; + size_t len_bytes = mDelimiter.size() * sizeof(llwchar); + return std::memcmp(s, mDelimiter.c_str(), len_bytes) == 0; } LLKeywords::LLKeywords() @@ -80,6 +62,8 @@ LLKeywords::~LLKeywords() mLineTokenList.clear(); std::for_each(mDelimiterTokenList.begin(), mDelimiterTokenList.end(), DeletePointer()); mDelimiterTokenList.clear(); + mLineTokenByFirstChar.clear(); + mDelimiterTokenByFirstChar.clear(); } // Add the token as described @@ -114,14 +98,29 @@ void LLKeywords::addToken(LLKeywordToken::ETokenType type, break; case LLKeywordToken::TT_LINE: - mLineTokenList.push_front(new LLKeywordToken(type, color, key, tool_tip, LLWStringUtil::null)); + { + LLKeywordToken* token = new LLKeywordToken(type, color, key, tool_tip, LLWStringUtil::null); + mLineTokenList.push_front(token); + if (!key.empty()) + { + mLineTokenByFirstChar[key[0]].push_front(token); + } break; + } case LLKeywordToken::TT_TWO_SIDED_DELIMITER: case LLKeywordToken::TT_DOUBLE_QUOTATION_MARKS: case LLKeywordToken::TT_ONE_SIDED_DELIMITER: - mDelimiterTokenList.push_front(new LLKeywordToken(type, color, key, tool_tip, delimiter)); + case LLKeywordToken::TT_LONG_BRACKET: + { + LLKeywordToken* token = new LLKeywordToken(type, color, key, tool_tip, delimiter); + mDelimiterTokenList.push_front(token); + if (!key.empty()) + { + mDelimiterTokenByFirstChar[key[0]].push_front(token); + } break; + } default: llassert(0); @@ -219,9 +218,10 @@ LLUIColor LLKeywords::getColorGroup(std::string_view key_in) const return LLUIColorTable::instance().getColor(color_group); } -void LLKeywords::initialize(LLSD SyntaxXML) +void LLKeywords::initialize(LLSD SyntaxXML, bool luau_language) { mSyntax = SyntaxXML; + mLuauLanguage = luau_language; mLoaded = true; } @@ -234,13 +234,30 @@ void LLKeywords::processTokens() // Add 'standard' stuff: Quotes, Comments, Strings, Labels, etc. before processing the LLSD std::string delimiter; - addToken(LLKeywordToken::TT_LABEL, "@", getColorGroup("misc-flow-label"), "Label\nTarget for jump statement", delimiter ); - addToken(LLKeywordToken::TT_ONE_SIDED_DELIMITER, "//", LLUIColorTable::instance().getColor("SyntaxLslComment"), "Comment (single-line)\nNon-functional commentary or disabled code", delimiter ); - addToken(LLKeywordToken::TT_TWO_SIDED_DELIMITER, "/*", LLUIColorTable::instance().getColor("SyntaxLslComment"), "Comment (multi-line)\nNon-functional commentary or disabled code", "*/" ); - addToken(LLKeywordToken::TT_DOUBLE_QUOTATION_MARKS, "\"", LLUIColorTable::instance().getColor("SyntaxLslStringLiteral"), "String literal", "\"" ); + addToken(LLKeywordToken::TT_DOUBLE_QUOTATION_MARKS, "\"", LLUIColorTable::instance().getColor("SyntaxLslStringLiteral"), "String literal", "\""); + + if (mLuauLanguage) + { + addToken(LLKeywordToken::TT_DOUBLE_QUOTATION_MARKS, "\'", LLUIColorTable::instance().getColor("SyntaxLslStringLiteral"), "String literal", "\'"); + // TODO: Might be nice to add a special case for this so we can still highlight expressions in `{}`s + addToken(LLKeywordToken::TT_DOUBLE_QUOTATION_MARKS, "`", LLUIColorTable::instance().getColor("SyntaxLslStringLiteral"), "String literal", "`"); + // Add Lua-style comments + addToken(LLKeywordToken::TT_ONE_SIDED_DELIMITER, "--", LLUIColorTable::instance().getColor("SyntaxLslComment"), "Comment (Lua-style single-line)\nNon-functional commentary or disabled code", delimiter); + // Add Lua multi-line comments (long brackets) + addToken(LLKeywordToken::TT_LONG_BRACKET, "--[", LLUIColorTable::instance().getColor("SyntaxLslComment"), "Comment (Lua-style multi-line)\nNon-functional commentary or disabled code", delimiter); + // Add Lua multi-line strings (long brackets) + addToken(LLKeywordToken::TT_LONG_BRACKET, "[", LLUIColorTable::instance().getColor("SyntaxLslStringLiteral"), "String literal (Lua-style multi-line)", delimiter); + } + else + { + addToken(LLKeywordToken::TT_LABEL, "@", getColorGroup("misc-flow-label"), "Label\nTarget for jump statement", delimiter); + // Add LSL-style comments + addToken(LLKeywordToken::TT_ONE_SIDED_DELIMITER, "//", LLUIColorTable::instance().getColor("SyntaxLslComment"), "Comment (single-line)\nNon-functional commentary or disabled code", delimiter); + addToken(LLKeywordToken::TT_TWO_SIDED_DELIMITER, "/*", LLUIColorTable::instance().getColor("SyntaxLslComment"), "Comment (multi-line)\nNon-functional commentary or disabled code", "*/"); + } LLSD::map_iterator itr = mSyntax.beginMap(); - for ( ; itr != mSyntax.endMap(); ++itr) + for (; itr != mSyntax.endMap(); ++itr) { if (itr->first == "llsd-lsl-syntax-version") { @@ -258,6 +275,7 @@ void LLKeywords::processTokens() } } } + LL_INFOS("SyntaxLSL") << "Finished processing tokens." << LL_ENDL; } @@ -346,8 +364,11 @@ void LLKeywords::processTokensGroup(const LLSD& tokens, std::string_view group) break; case LLKeywordToken::TT_FUNCTION: tooltip = getAttribute("return") + " " + outer_itr->first + "(" + getArguments(arguments) + ");"; - tooltip.append("\nEnergy: "); - tooltip.append(getAttribute("energy").empty() ? "0.0" : getAttribute("energy")); + if (std::stod(getAttribute("energy")) >= 0) + { + tooltip.append("\nEnergy: "); + tooltip.append(getAttribute("energy").empty() ? "0.0" : getAttribute("energy")); + } if (!getAttribute("sleep").empty()) { tooltip += ", Sleep: " + getAttribute("sleep"); @@ -476,34 +497,29 @@ bool LLKeywords::WStringMapIndex::operator<(const LLKeywords::WStringMapIndex &o } LLTrace::BlockTimerStatHandle FTM_SYNTAX_COLORING("Syntax Coloring"); +constexpr size_t AVERAGE_SEGMENT_LENGTH = 8; -// Walk through a string, applying the rules specified by the keyword token list and -// create a list of color segments. -void LLKeywords::findSegments(std::vector<LLTextSegmentPtr>* seg_list, const LLWString& wtext, LLTextEditor& editor, LLStyleConstSP style) +void LLKeywords::collectSegmentOps(segment_ops_t& ops, const LLWString& wtext, bool disable_syntax_highlighting) const { - LL_RECORD_BLOCK_TIME(FTM_SYNTAX_COLORING); - seg_list->clear(); + ops.clear(); - if( wtext.empty() ) + if (wtext.empty()) { return; } - - S32 text_len = static_cast<S32>(wtext.size()) + 1; - - seg_list->push_back( new LLNormalTextSegment( style, 0, text_len, editor ) ); + // Heuristic to reduce reallocation churn on large scripts. + ops.reserve(wtext.size() / AVERAGE_SEGMENT_LENGTH); const llwchar* base = wtext.c_str(); const llwchar* cur = base; + while( *cur ) { if( *cur == '\n' || cur == base ) { if( *cur == '\n' ) { - LLTextSegmentPtr text_segment = new LLLineBreakTextSegment(style, (S32)(cur - base)); - text_segment->setToken( 0 ); - insertSegment( *seg_list, text_segment, text_len, style, editor); + ops.push_back({SegmentOp::OP_LINE_BREAK, (S32)(cur - base), 0, nullptr}); cur++; if( !*cur || *cur == '\n' ) { @@ -526,24 +542,25 @@ void LLKeywords::findSegments(std::vector<LLTextSegmentPtr>* seg_list, const LLW // Line start tokens { bool line_done = false; - for (token_list_t::iterator iter = mLineTokenList.begin(); - iter != mLineTokenList.end(); ++iter) + auto line_token_it = mLineTokenByFirstChar.find(*cur); + if (line_token_it != mLineTokenByFirstChar.end()) { - LLKeywordToken* cur_token = *iter; - if( cur_token->isHead( cur ) ) + for (auto* cur_token : line_token_it->second) { - S32 seg_start = (S32)(cur - base); - while( *cur && *cur != '\n' ) + if( cur_token->isHead( cur ) ) { - // skip the rest of the line - cur++; - } - S32 seg_end = (S32)(cur - base); + S32 seg_start = (S32)(cur - base); + while( *cur && *cur != '\n' ) + { + // skip the rest of the line + cur++; + } + S32 seg_end = (S32)(cur - base); - //create segments from seg_start to seg_end - insertSegments(wtext, *seg_list,cur_token, text_len, seg_start, seg_end, style, editor); - line_done = true; // to break out of second loop. - break; + ops.push_back({SegmentOp::OP_TOKEN, seg_start, seg_end, cur_token}); + line_done = true; // to break out of second loop. + break; + } } } @@ -560,20 +577,32 @@ void LLKeywords::findSegments(std::vector<LLTextSegmentPtr>* seg_list, const LLW cur++; } + // Check if syntax highlighting is disabled + if (disable_syntax_highlighting) + { + while (*cur && *cur != '\n') + { + ++cur; + } + continue; // skip processing any further syntax highlighting + } + while( *cur && *cur != '\n' ) { // Check against delimiters { S32 seg_start = 0; LLKeywordToken* cur_delimiter = NULL; - for (token_list_t::iterator iter = mDelimiterTokenList.begin(); - iter != mDelimiterTokenList.end(); ++iter) + auto delimiter_it = mDelimiterTokenByFirstChar.find(*cur); + if (delimiter_it != mDelimiterTokenByFirstChar.end()) { - LLKeywordToken* delimiter = *iter; - if( delimiter->isHead( cur ) ) + for (auto* delimiter : delimiter_it->second) { - cur_delimiter = delimiter; - break; + if( delimiter->isHead( cur ) ) + { + cur_delimiter = delimiter; + break; + } } } @@ -583,10 +612,79 @@ void LLKeywords::findSegments(std::vector<LLTextSegmentPtr>* seg_list, const LLW S32 seg_end = 0; seg_start = (S32)(cur - base); - cur += cur_delimiter->getLengthHead(); LLKeywordToken::ETokenType type = cur_delimiter->getType(); - if( type == LLKeywordToken::TT_TWO_SIDED_DELIMITER || type == LLKeywordToken::TT_DOUBLE_QUOTATION_MARKS ) + + // Handle Lua long brackets specially - need to verify full pattern + if (type == LLKeywordToken::TT_LONG_BRACKET) + { + const llwchar* p = cur + cur_delimiter->getLengthHead(); // after --[ or [ + + // Count equals signs + S32 level = 0; + while (*p == '=') + { + level++; + p++; + } + + // Must have second [ + if (*p == '[') + { + p++; // skip the second [ + + // Build the closing pattern: ] + level equals + ] + // Search for it in the remaining text + while (*p) + { + if (*p == ']') + { + // Check if this is our closing bracket + const llwchar* close_check = p + 1; + S32 close_equals = 0; + while (*close_check == '=') + { + close_equals++; + close_check++; + } + if (close_equals == level && *close_check == ']') + { + // Found the matching close + seg_end = (S32)(close_check + 1 - base); + cur = close_check + 1; + ops.push_back({SegmentOp::OP_TOKEN, seg_start, seg_end, cur_delimiter}); + break; + } + } + p++; + } + + if (!*p) + { + // No closing found, highlight to end of file + seg_end = static_cast<S32>(wtext.size()); + cur = base + seg_end; + ops.push_back({SegmentOp::OP_TOKEN, seg_start, seg_end, cur_delimiter}); + } + continue; + } + else + { + // Not a valid long bracket (e.g., --[abc), skip this delimiter + cur_delimiter = NULL; + } + } + + if (!cur_delimiter) + { + // Long bracket validation failed, continue to next character + cur++; + continue; + } + + cur += cur_delimiter->getLengthHead(); + + if(type == LLKeywordToken::TT_TWO_SIDED_DELIMITER || type == LLKeywordToken::TT_DOUBLE_QUOTATION_MARKS) { while( *cur && !cur_delimiter->isTail(cur)) { @@ -627,7 +725,7 @@ void LLKeywords::findSegments(std::vector<LLTextSegmentPtr>* seg_list, const LLW if( *cur ) { - cur += cur_delimiter->getLengthHead(); + cur += cur_delimiter->getLengthTail(); seg_end = seg_start + between_delimiters + cur_delimiter->getLengthHead() + cur_delimiter->getLengthTail(); } else @@ -648,12 +746,7 @@ void LLKeywords::findSegments(std::vector<LLTextSegmentPtr>* seg_list, const LLW seg_end = seg_start + between_delimiters + cur_delimiter->getLengthHead(); } - insertSegments(wtext, *seg_list,cur_delimiter, text_len, seg_start, seg_end, style, editor); - /* - LLTextSegmentPtr text_segment = new LLNormalTextSegment( cur_delimiter->getColor(), seg_start, seg_end, editor ); - text_segment->setToken( cur_delimiter ); - insertSegment( seg_list, text_segment, text_len, defaultColor, editor); - */ + ops.push_back({SegmentOp::OP_TOKEN, seg_start, seg_end, cur_delimiter}); // Note: we don't increment cur, since the end of one delimited seg may be immediately // followed by the start of another one. continue; @@ -662,34 +755,77 @@ void LLKeywords::findSegments(std::vector<LLTextSegmentPtr>* seg_list, const LLW // check against words llwchar prev = cur > base ? *(cur-1) : 0; - if( !iswalnum( prev ) && (prev != '_') ) + if (!iswalnum(prev) && prev != '_' && prev != '.') { - const llwchar* p = cur; - while( iswalnum( *p ) || (*p == '_') ) + const llwchar* word_start = cur; + S32 namespace_dots = 0; + const llwchar* last_dot = nullptr; + + // Find the full extent of the word, potentially including namespace dots + while (iswalnum(*cur) || *cur == '_' || (mLuauLanguage && *cur == '.' && iswalnum(*(cur+1)))) { - p++; + if (mLuauLanguage && *cur == '.') + { + namespace_dots++; + last_dot = cur; + } + cur++; } - S32 seg_len = (S32)(p - cur); - if( seg_len > 0 ) + + S32 seg_len = (S32)(cur - word_start); + if (seg_len > 0) { - WStringMapIndex word( cur, seg_len ); - word_token_map_t::iterator map_iter = mWordTokenMap.find(word); - if( map_iter != mWordTokenMap.end() ) + S32 seg_start = (S32)(word_start - base); + S32 seg_end = seg_start + seg_len; + + // First try to match the whole token (including dots for Lua namespaces) + word_token_map_t::const_iterator map_iter = mWordTokenMap.find(WStringMapIndex(word_start, seg_len)); + + if (map_iter != mWordTokenMap.end()) + { + // Found a match for the complete token (including any namespace) + ops.push_back({SegmentOp::OP_TOKEN, seg_start, seg_end, map_iter->second}); + } + else if (namespace_dots > 0 && mLuauLanguage) { - LLKeywordToken* cur_token = map_iter->second; - S32 seg_start = (S32)(cur - base); - S32 seg_end = seg_start + seg_len; + // If using Lua and we have namespace dots but didn't match the whole token, + // check if we have a match for just the namespace prefix (e.g., "ll") + if (last_dot > word_start) + { + // Get the namespace prefix (part before the first dot) + S32 prefix_len = (S32)(last_dot - word_start); + map_iter = mWordTokenMap.find(WStringMapIndex(word_start, prefix_len)); - // LL_INFOS("SyntaxLSL") << "Seg: [" << word.c_str() << "]" << LL_ENDL; + if (map_iter != mWordTokenMap.end()) + { + // Found a match for the namespace prefix, highlight just that part + ops.push_back({SegmentOp::OP_TOKEN, seg_start, seg_start + prefix_len, map_iter->second}); - insertSegments(wtext, *seg_list,cur_token, text_len, seg_start, seg_end, style, editor); + // Now try to match the function part (after the dot) + const llwchar* func_part = last_dot + 1; + S32 func_len = (S32)(cur - func_part); + + if (func_len > 0) + { + // Look for complete function matches + map_iter = mWordTokenMap.find(WStringMapIndex(func_part, func_len)); + + if (map_iter != mWordTokenMap.end()) + { + // Found a match for the function part. + // Start must be after the dot (seg_start + prefix_len + 1), + // not at seg_start, to avoid overlapping with the prefix segment. + ops.push_back({SegmentOp::OP_TOKEN, seg_start + prefix_len + 1, seg_end, map_iter->second}); + } + } + } + } } - cur += seg_len; - continue; + continue; // Continue to next token regardless of match } } - if( *cur && *cur != '\n' ) + if (*cur && *cur != '\n') { cur++; } @@ -697,6 +833,84 @@ void LLKeywords::findSegments(std::vector<LLTextSegmentPtr>* seg_list, const LLW } } +bool LLKeywords::applySegmentOpsRange(std::vector<LLTextSegmentPtr> *seg_list, + const LLWString& wtext, + const segment_ops_t& ops, + size_t& op_index, + size_t max_ops, + LLTextEditor& editor, + LLStyleConstSP style) +{ + if (wtext.empty()) + { + return true; + } + + if (op_index == 0) + { + // Clear the segment list + seg_list->clear(); + // Reserve capacity for segments based on an estimated average of 8 characters per segment. + seg_list->reserve(wtext.size() / AVERAGE_SEGMENT_LENGTH); + + S32 text_len = static_cast<S32>(wtext.size()) + 1; + seg_list->push_back( new LLNormalTextSegment( style, 0, text_len, editor ) ); + } + + S32 text_len = static_cast<S32>(wtext.size()) + 1; + size_t end_index = op_index + max_ops; + if (end_index > ops.size()) + { + end_index = ops.size(); + } + + for (; op_index < end_index; ++op_index) + { + const auto& op = ops[op_index]; + if (op.type == SegmentOp::OP_LINE_BREAK) + { + LLTextSegmentPtr text_segment = new LLLineBreakTextSegment(style, op.start); + text_segment->setToken( 0 ); + insertSegment( *seg_list, text_segment, text_len, style, editor); + } + else + { + insertSegments(wtext, *seg_list, op.token, text_len, op.start, op.end, style, editor); + } + } + + return op_index >= ops.size(); +} + +void LLKeywords::applySegmentOps(std::vector<LLTextSegmentPtr> *seg_list, + const LLWString& wtext, + const segment_ops_t& ops, + LLTextEditor& editor, + LLStyleConstSP style) +{ + size_t op_index = 0; + applySegmentOpsRange(seg_list, wtext, ops, op_index, ops.size(), editor, style); +} + +// Walk through a string, applying the rules specified by the keyword token list and +// create a list of color segments. +void LLKeywords::findSegments(std::vector<LLTextSegmentPtr>* seg_list, const LLWString& wtext, LLTextEditor& editor, LLStyleConstSP style) +{ + LL_RECORD_BLOCK_TIME(FTM_SYNTAX_COLORING); + + if( wtext.empty() ) + { + return; + } + + static LLCachedControl<bool> sDisableSyntaxHighlighting(gSavedSettings, "ScriptEditorDisableSyntaxHighlight", false); + const bool disable_syntax_highlighting = sDisableSyntaxHighlighting; + + segment_ops_t ops; + collectSegmentOps(ops, wtext, disable_syntax_highlighting); + applySegmentOps(seg_list, wtext, ops, editor, style); +} + void LLKeywords::insertSegments(const LLWString& wtext, std::vector<LLTextSegmentPtr>& seg_list, LLKeywordToken* cur_token, S32 text_len, S32 seg_start, S32 seg_end, LLStyleConstSP style, LLTextEditor& editor ) { std::string::size_type pos = wtext.find('\n',seg_start); diff --git a/indra/llui/llkeywords.h b/indra/llui/llkeywords.h index 53b5435324..9d9c8f964c 100644 --- a/indra/llui/llkeywords.h +++ b/indra/llui/llkeywords.h @@ -36,6 +36,8 @@ #include <map> #include <list> #include <deque> +#include <vector> +#include <cstddef> #include "llpointer.h" class LLTextSegment; @@ -53,6 +55,7 @@ public: * - TT_ONE_SIDED_DELIMITER are for open-ended delimiters which are terminated by EOL. * - TT_TWO_SIDED_DELIMITER are for delimiters that end with a different delimiter than they open with. * - TT_DOUBLE_QUOTATION_MARKS are for delimiting areas using the same delimiter to open and close. + * - TT_LONG_BRACKET are for Lua tokens that use brackets with counted equals signs. */ typedef enum e_token_type { @@ -62,6 +65,7 @@ public: TT_TWO_SIDED_DELIMITER, TT_ONE_SIDED_DELIMITER, TT_DOUBLE_QUOTATION_MARKS, + TT_LONG_BRACKET, // Lua long brackets: --[=*[ or [=*[ // Following constants are more specific versions of the preceding ones TT_CONSTANT, // WORD TT_CONTROL, // WORD @@ -82,6 +86,10 @@ public: { } + ~LLKeywordToken() + { + } + S32 getLengthHead() const { return static_cast<S32>(mToken.size()); } S32 getLengthTail() const { return static_cast<S32>(mDelimiter.size()); } bool isHead(const llwchar* s) const; @@ -118,7 +126,33 @@ public: const LLWString& text, class LLTextEditor& editor, LLStyleConstSP style); - void initialize(LLSD SyntaxXML); + struct SegmentOp + { + enum EOpType + { + OP_LINE_BREAK, + OP_TOKEN + }; + EOpType type; + S32 start; + S32 end; + LLKeywordToken* token; + }; + typedef std::vector<SegmentOp> segment_ops_t; + void collectSegmentOps(segment_ops_t& ops, const LLWString& text, bool disable_syntax_highlighting) const; + void applySegmentOps(std::vector<LLTextSegmentPtr> *seg_list, + const LLWString& text, + const segment_ops_t& ops, + class LLTextEditor& editor, + LLStyleConstSP style); + bool applySegmentOpsRange(std::vector<LLTextSegmentPtr> *seg_list, + const LLWString& text, + const segment_ops_t& ops, + size_t& op_index, + size_t max_ops, + class LLTextEditor& editor, + LLStyleConstSP style); + void initialize(LLSD SyntaxXML, bool luau_language = false); void processTokens(); // Add the token as described @@ -189,10 +223,14 @@ protected: bool mLoaded; LLSD mSyntax; + bool mLuauLanguage; word_token_map_t mWordTokenMap; typedef std::deque<LLKeywordToken*> token_list_t; token_list_t mLineTokenList; token_list_t mDelimiterTokenList; + typedef std::map<llwchar, token_list_t> token_by_first_char_map_t; + token_by_first_char_map_t mLineTokenByFirstChar; + token_by_first_char_map_t mDelimiterTokenByFirstChar; typedef std::unordered_map<std::string, std::string, ll::string_hash, std::equal_to<>> element_attributes_t; typedef element_attributes_t::const_iterator attribute_iterator_t; diff --git a/indra/llui/lllineeditor.cpp b/indra/llui/lllineeditor.cpp index 9a88083a5d..404e394ace 100644 --- a/indra/llui/lllineeditor.cpp +++ b/indra/llui/lllineeditor.cpp @@ -493,12 +493,13 @@ void LLLineEditor::setCursor( S32 pos ) S32 pixels_after_scroll = findPixelNearestPos(); if( pixels_after_scroll > mTextRightEdge ) { - S32 width_chars_to_left = mGLFont->getWidth(mText.getWString().c_str(), 0, mScrollHPos); - S32 last_visible_char = mGLFont->maxDrawableChars(mText.getWString().c_str(), llmax(0.f, (F32)(mTextRightEdge - mTextLeftEdge + width_chars_to_left))); + const LLWString& wtext = mText.getWString(); + S32 width_chars_to_left = mGLFont->getWidth(wtext.c_str(), 0, mScrollHPos); + S32 last_visible_char = mGLFont->maxDrawableChars(wtext.c_str(), llmax(0.f, (F32)(mTextRightEdge - mTextLeftEdge + width_chars_to_left))); // character immediately to left of cursor should be last one visible (SCROLL_INCREMENT_ADD will scroll in more characters) // or first character if cursor is at beginning S32 new_last_visible_char = llmax(0, getCursor() - 1); - S32 min_scroll = mGLFont->firstDrawableChar(mText.getWString().c_str(), (F32)(mTextRightEdge - mTextLeftEdge), mText.length(), new_last_visible_char); + S32 min_scroll = mGLFont->firstDrawableChar(wtext.c_str(), (F32)(mTextRightEdge - mTextLeftEdge), mText.length(), new_last_visible_char); if (old_cursor_pos == last_visible_char) { mScrollHPos = llmin(mText.length(), llmax(min_scroll, mScrollHPos + SCROLL_INCREMENT_ADD)); diff --git a/indra/llui/llresmgr.cpp b/indra/llui/llresmgr.cpp index 111c732548..f00d876b2b 100644 --- a/indra/llui/llresmgr.cpp +++ b/indra/llui/llresmgr.cpp @@ -54,11 +54,9 @@ char LLResMgr::getDecimalPoint() const return decimal; } -char LLResMgr::getThousandsSeparator() const +std::string LLResMgr::getThousandsSeparator() const { - char separator = localeconv()->thousands_sep[0]; - - return separator; + return localeconv()->thousands_sep; } char LLResMgr::getMonetaryDecimalPoint() const @@ -68,11 +66,9 @@ char LLResMgr::getMonetaryDecimalPoint() const return decimal; } -char LLResMgr::getMonetaryThousandsSeparator() const +std::string LLResMgr::getMonetaryThousandsSeparator() const { - char separator = localeconv()->mon_thousands_sep[0]; - - return separator; + return localeconv()->mon_thousands_sep; } @@ -85,7 +81,7 @@ std::string LLResMgr::getMonetaryString( S32 input ) const struct lconv *conv = localeconv(); char* negative_sign = conv->negative_sign; - char separator = getMonetaryThousandsSeparator(); + std::string separator = getMonetaryThousandsSeparator(); char* grouping = conv->mon_grouping; // Note on mon_grouping: @@ -135,10 +131,7 @@ std::string LLResMgr::getMonetaryString( S32 input ) const } S32 group_count = cur_group; - char reversed_output[20] = ""; /* Flawfinder: ignore */ - char forward_output[20] = ""; /* Flawfinder: ignore */ - S32 output_pos = 0; - + std::string formatted; cur_group = 0; S32 pos = static_cast<S32>(digits.size()) - 1; S32 count_within_group = 0; @@ -148,7 +141,7 @@ std::string LLResMgr::getMonetaryString( S32 input ) const if( count_within_group > groupings[cur_group] ) { count_within_group = 1; - reversed_output[ output_pos++ ] = separator; + formatted = separator + formatted; if( (cur_group + 1) >= group_count ) { @@ -160,28 +153,19 @@ std::string LLResMgr::getMonetaryString( S32 input ) const cur_group++; } } - reversed_output[ output_pos++ ] = digits[pos--]; + formatted = std::string(1, digits[pos--]) + formatted; } while( pos >= 0 ) { - reversed_output[ output_pos++ ] = digits[pos--]; - } - - - reversed_output[ output_pos ] = '\0'; - forward_output[ output_pos ] = '\0'; - - for( S32 i = 0; i < output_pos; i++ ) - { - forward_output[ output_pos - 1 - i ] = reversed_output[ i ]; + formatted = std::string(1, digits[pos--]) + formatted; } if( negative_before ) { output.append( negative_sign ); } - output.append( forward_output ); + output.append( formatted ); if( negative_after ) { output.append( negative_sign ); @@ -210,11 +194,11 @@ void LLResMgr::getIntegerString( std::string& output, S32 input ) const { if (fraction == remaining_count) { - fraction_string = llformat_to_utf8("%d%c", fraction, getThousandsSeparator()); + fraction_string = llformat_to_utf8("%d", fraction) + getThousandsSeparator(); } else { - fraction_string = llformat_to_utf8("%3.3d%c", fraction, getThousandsSeparator()); + fraction_string = llformat_to_utf8("%3.3d", fraction) + getThousandsSeparator(); } output = fraction_string + output; } diff --git a/indra/llui/llresmgr.h b/indra/llui/llresmgr.h index 7ad6659a44..8ceb584ec5 100644 --- a/indra/llui/llresmgr.h +++ b/indra/llui/llresmgr.h @@ -49,10 +49,10 @@ public: LLLOCALE_ID getLocale() const { return mLocale; } char getDecimalPoint() const; - char getThousandsSeparator() const; + std::string getThousandsSeparator() const; char getMonetaryDecimalPoint() const; - char getMonetaryThousandsSeparator() const; + std::string getMonetaryThousandsSeparator() const; std::string getMonetaryString( S32 input ) const; void getIntegerString( std::string& output, S32 input ) const; diff --git a/indra/llui/llscrollcontainer.cpp b/indra/llui/llscrollcontainer.cpp index e36fd45bb4..ad2f58db69 100644 --- a/indra/llui/llscrollcontainer.cpp +++ b/indra/llui/llscrollcontainer.cpp @@ -75,6 +75,7 @@ LLScrollContainer::Params::Params() max_auto_scroll_rate("max_auto_scroll_rate", 1000), max_auto_scroll_zone("max_auto_scroll_zone", 16), reserve_scroll_corner("reserve_scroll_corner", false), + keep_scroll_pos("keep_scroll_pos", false), size("size", -1) {} @@ -93,8 +94,12 @@ LLScrollContainer::LLScrollContainer(const LLScrollContainer::Params& p) mMaxAutoScrollRate(p.max_auto_scroll_rate), mMaxAutoScrollZone(p.max_auto_scroll_zone), mScrolledView(NULL), + mKeepScrollPos(p.keep_scroll_pos), mSize(p.size) { + mStoredDocPos[VERTICAL] = 0; + mStoredDocPos[HORIZONTAL] = 0; + static LLUICachedControl<S32> scrollbar_size_control ("UIScrollbarSize", 0); S32 scrollbar_size = (mSize == -1 ? scrollbar_size_control : mSize); @@ -605,6 +610,22 @@ void LLScrollContainer::updateScroll() calcVisibleSize( &visible_width, &visible_height, &show_h_scrollbar, &show_v_scrollbar ); S32 border_width = getBorderWidth(); + + // Remember the position only while the scrollbar is genuinely showing scrollable + // content, so a transient empty pass (e.g. a list clearing before it repopulates) + // cannot overwrite it with 0. + if (mKeepScrollPos) + { + if (show_v_scrollbar && mScrollbar[VERTICAL]->getVisible()) + { + mStoredDocPos[VERTICAL] = mScrollbar[VERTICAL]->getDocPos(); + } + if (show_h_scrollbar && mScrollbar[HORIZONTAL]->getVisible()) + { + mStoredDocPos[HORIZONTAL] = mScrollbar[HORIZONTAL]->getDocPos(); + } + } + if( show_v_scrollbar ) { if( doc_rect.mTop < getRect().getHeight() - border_width ) @@ -673,6 +694,18 @@ void LLScrollContainer::updateScroll() mScrollbar[VERTICAL]->setDocSize( doc_height ); mScrollbar[VERTICAL]->setPageSize( visible_height ); + + if (mKeepScrollPos) + { + if (show_v_scrollbar) + { + mScrollbar[VERTICAL]->setDocPos(mStoredDocPos[VERTICAL]); + } + if (show_h_scrollbar) + { + mScrollbar[HORIZONTAL]->setDocPos(mStoredDocPos[HORIZONTAL]); + } + } } // end updateScroll void LLScrollContainer::setBorderVisible(bool b) diff --git a/indra/llui/llscrollcontainer.h b/indra/llui/llscrollcontainer.h index 859750d71c..7761824dbb 100644 --- a/indra/llui/llscrollcontainer.h +++ b/indra/llui/llscrollcontainer.h @@ -64,7 +64,8 @@ public: reserve_scroll_corner, border_visible, hide_scrollbar, - ignore_arrow_keys; + ignore_arrow_keys, + keep_scroll_pos; Optional<F32> min_auto_scroll_rate, max_auto_scroll_rate; Optional<U32> max_auto_scroll_zone; @@ -138,6 +139,8 @@ private: void calcVisibleSize( S32 *visible_width, S32 *visible_height, bool* show_h_scrollbar, bool* show_v_scrollbar ) const; LLScrollbar* mScrollbar[ORIENTATION_COUNT]; + S32 mStoredDocPos[ORIENTATION_COUNT]; + bool mKeepScrollPos; S32 mSize; bool mIsOpaque; LLUIColor mBackgroundColor; diff --git a/indra/llui/llscrolllistctrl.cpp b/indra/llui/llscrolllistctrl.cpp index 558ce6a7fd..662fe97689 100644 --- a/indra/llui/llscrolllistctrl.cpp +++ b/indra/llui/llscrolllistctrl.cpp @@ -1281,6 +1281,19 @@ LLScrollListItem* LLScrollListCtrl::getItemByLabel(const std::string& label, boo return NULL; } +LLScrollListItem* LLScrollListCtrl::getItemByValue(const std::string& value) +{ + for (LLScrollListItem* item : mItemList) + { + if (item->getValue().asString() == value) + { + return item; + } + } + + return NULL; +} + LLScrollListItem* LLScrollListCtrl::getItemByIndex(S32 index) { if (index >= 0 && index < (S32)mItemList.size()) diff --git a/indra/llui/llscrolllistctrl.h b/indra/llui/llscrolllistctrl.h index d3735c5052..b8d63972a5 100644 --- a/indra/llui/llscrolllistctrl.h +++ b/indra/llui/llscrolllistctrl.h @@ -262,7 +262,8 @@ public: bool selectItemByLabel(const std::string& item, bool case_sensitive = true, S32 column = 0); // false if item not found bool selectItemByPrefix(const std::string& target, bool case_sensitive = true, S32 column = -1); bool selectItemByPrefix(const LLWString& target, bool case_sensitive = true, S32 column = -1); - LLScrollListItem* getItemByLabel(const std::string& item, bool case_sensitive = true, S32 column = 0); + LLScrollListItem* getItemByLabel(const std::string& label, bool case_sensitive = true, S32 column = 0); + LLScrollListItem* getItemByValue(const std::string& value); LLScrollListItem* getItemByIndex(S32 index); std::string getSelectedItemLabel(S32 column = 0) const; LLSD getSelectedValue(); diff --git a/indra/llui/lltextbase.cpp b/indra/llui/lltextbase.cpp index 0521853b02..802367720f 100644 --- a/indra/llui/lltextbase.cpp +++ b/indra/llui/lltextbase.cpp @@ -2288,6 +2288,7 @@ void LLTextBase::createUrlContextMenu(S32 x, S32 y, const std::string &in_url) registrar.add("Url.AddFriend", boost::bind(&LLUrlAction::addFriend, url)); registrar.add("Url.RemoveFriend", boost::bind(&LLUrlAction::removeFriend, url)); registrar.add("Url.ReportAbuse", boost::bind(&LLUrlAction::reportAbuse, url)); + registrar.add("Url.ReportAbuseObj", boost::bind(&LLUrlAction::reportAbuseObj, url)); registrar.add("Url.SendIM", boost::bind(&LLUrlAction::sendIM, url)); registrar.add("Url.ZoomInObject", boost::bind(&LLUrlAction::zoomInObject, url)); registrar.add("Url.ShowOnMap", boost::bind(&LLUrlAction::showLocationOnMap, url)); diff --git a/indra/llui/lltextbase.h b/indra/llui/lltextbase.h index bc39d9732c..62f984b8fc 100644 --- a/indra/llui/lltextbase.h +++ b/indra/llui/lltextbase.h @@ -327,6 +327,7 @@ class LLTextBase public: friend class LLTextSegment; friend class LLNormalTextSegment; + friend class LLEmbeddedItemSegment; friend class LLUICtrlFactory; typedef boost::signals2::signal<bool (const LLUUID& user_id)> is_friend_signal_t; diff --git a/indra/llui/lltexteditor.cpp b/indra/llui/lltexteditor.cpp index 7689b93374..6588c481ff 100644 --- a/indra/llui/lltexteditor.cpp +++ b/indra/llui/lltexteditor.cpp @@ -61,6 +61,7 @@ #include "lltooltip.h" #include "llmenugl.h" #include "llchatmentionhelper.h" +#include "llgestureautocompletehelper.h" #include <queue> #include "llcombobox.h" @@ -1692,28 +1693,25 @@ void LLTextEditor::pasteTextWithLinebreaks(LLWString & clean_string) std::basic_string<llwchar>::size_type start = 0; std::basic_string<llwchar>::size_type pos = clean_string.find('\n',start); - while((pos != -1) && (pos != clean_string.length() -1)) + while (pos != std::basic_string<llwchar>::npos) { - if(pos!=start) + if (pos != start) { std::basic_string<llwchar> str = std::basic_string<llwchar>(clean_string,start,pos-start); setCursorPos(mCursorPos + insert(mCursorPos, str, true, LLTextSegmentPtr())); } - addLineBreakChar(true); // Add a line break and group with the next addition. + const bool trailing_linebreak = (pos == clean_string.length() - 1); + addLineBreakChar(!trailing_linebreak); start = pos+1; pos = clean_string.find('\n',start); } - if (pos != start) + if (start < clean_string.length()) { std::basic_string<llwchar> str = std::basic_string<llwchar>(clean_string,start,clean_string.length()-start); setCursorPos(mCursorPos + insert(mCursorPos, str, false, LLTextSegmentPtr())); } - else - { - addLineBreakChar(false); // Add a line break and end the grouping. - } } // copy selection to primary @@ -1950,7 +1948,8 @@ bool LLTextEditor::handleKeyHere(KEY key, MASK mask ) // not handled and let the parent take care of field movement. if (KEY_TAB == key && mTabsToNextField) { - return mShowChatMentionPicker && LLChatMentionHelper::instance().handleKey(this, key, mask); + return (mShowChatMentionPicker && LLChatMentionHelper::instance().handleKey(this, key, mask)) + || LLGestureAutocompleteHelper::instance().handleKey(this, key, mask); } if (mReadOnly && mScroller) @@ -1964,7 +1963,8 @@ bool LLTextEditor::handleKeyHere(KEY key, MASK mask ) if (!mReadOnly) { if ((mShowEmojiHelper && LLEmojiHelper::instance().handleKey(this, key, mask)) || - (mShowChatMentionPicker && LLChatMentionHelper::instance().handleKey(this, key, mask))) + (mShowChatMentionPicker && LLChatMentionHelper::instance().handleKey(this, key, mask)) || + LLGestureAutocompleteHelper::instance().handleKey(this, key, mask)) { return true; } diff --git a/indra/llui/lltexteditor.h b/indra/llui/lltexteditor.h index d9742db34d..43f47a404c 100644 --- a/indra/llui/lltexteditor.h +++ b/indra/llui/lltexteditor.h @@ -208,6 +208,7 @@ public: void setShowContextMenu(bool show) { mShowContextMenu = show; } bool getShowContextMenu() const { return mShowContextMenu; } + void showContextMenu(S32 x, S32 y); void showEmojiHelper(); void hideEmojiHelper(); @@ -219,7 +220,6 @@ public: LLWString getConvertedText() const; protected: - void showContextMenu(S32 x, S32 y); void drawPreeditMarker(); void removeCharOrTab(); diff --git a/indra/llui/lltextvalidate.cpp b/indra/llui/lltextvalidate.cpp index 9a087d8230..1f2e7e65db 100644 --- a/indra/llui/lltextvalidate.cpp +++ b/indra/llui/lltextvalidate.cpp @@ -434,7 +434,7 @@ public: } validatorASCIINoLeadingSpaceImpl; Validator validateASCIINoLeadingSpace(validatorASCIINoLeadingSpaceImpl); -class ValidatorASCIIWithNewLine : public ValidatorImpl +class ValidatorASCIIWithNewLineNoPipe : public ValidatorImpl { // Used for multiline text stored on the server. // Example is landmark description in Places SP. @@ -446,9 +446,9 @@ class ValidatorASCIIWithNewLine : public ValidatorImpl { CHAR ch = str[len]; - if ((ch < 0x20 && ch != 0xA) || ch > 0x7f) + if ((ch < 0x20 && ch != 0xA) || ch > 0x7f || ch == '|') { - return setError("Validator_ShouldBeNewLineOrASCII", LLSD().with("NR", len + 1).with("CH", llsd(ch))); + return setError("Validator_ShouldBeNewLineOrASCIINoPipe", LLSD().with("NR", len + 1).with("CH", llsd(ch))); } } @@ -458,8 +458,8 @@ class ValidatorASCIIWithNewLine : public ValidatorImpl public: /*virtual*/ bool validate(const std::string& str) override { return validate<char>(str); } /*virtual*/ bool validate(const LLWString& str) override { return validate<llwchar>(str); } -} validatorASCIIWithNewLineImpl; -Validator validateASCIIWithNewLine(validatorASCIIWithNewLineImpl); +} validatorASCIIWithNewLineNoPipeImpl; +Validator validateASCIIWithNewLineNoPipe(validatorASCIIWithNewLineNoPipeImpl); void Validators::declareValues() { @@ -472,7 +472,7 @@ void Validators::declareValues() declare("alpha_num_space", validateAlphaNumSpace); declare("ascii_printable_no_pipe", validateASCIIPrintableNoPipe); declare("ascii_printable_no_space", validateASCIIPrintableNoSpace); - declare("ascii_with_newline", validateASCIIWithNewLine); + declare("ascii_with_newline_no_pipe", validateASCIIWithNewLineNoPipe); } } // namespace LLTextValidate diff --git a/indra/llui/lltextvalidate.h b/indra/llui/lltextvalidate.h index 096c28b448..a95f79bfcb 100644 --- a/indra/llui/lltextvalidate.h +++ b/indra/llui/lltextvalidate.h @@ -88,7 +88,7 @@ namespace LLTextValidate extern Validator validateASCIIPrintableNoSpace; extern Validator validateASCII; extern Validator validateASCIINoLeadingSpace; - extern Validator validateASCIIWithNewLine; + extern Validator validateASCIIWithNewLineNoPipe; // Add available validators to the internal map struct Validators : public LLInitParam::TypeValuesHelper<Validator, Validators> diff --git a/indra/llui/lltoolbar.cpp b/indra/llui/lltoolbar.cpp index 785dc85448..2a9bf2585d 100644 --- a/indra/llui/lltoolbar.cpp +++ b/indra/llui/lltoolbar.cpp @@ -792,6 +792,11 @@ void LLToolBar::updateLayoutAsNeeded() mNeedsLayout = false; } +bool LLToolBar::postBuild() +{ + mCaretIcon = getChild<LLIconCtrl>("caret"); + return LLUICtrl::postBuild(); +} void LLToolBar::draw() { @@ -835,35 +840,36 @@ void LLToolBar::draw() LLUI::translate((F32)getRect().mLeft, (F32)getRect().mBottom); // Position the caret - if (!mCaretIcon) - { - mCaretIcon = getChild<LLIconCtrl>("caret"); - } - - LLIconCtrl* caret = mCaretIcon; - caret->setVisible(false); - if (mDragAndDropTarget && !mButtonCommands.empty()) + // Todo: This shouldn't be on draw, but, as example, on hover + if (mCaretIcon) { - LLRect caret_rect = caret->getRect(); - if (getOrientation(mSideType) == LLLayoutStack::HORIZONTAL) - { - caret->setRect(LLRect(mDragx-caret_rect.getWidth()/2+1, - mDragy, - mDragx+caret_rect.getWidth()/2+1, - mDragy-mDragGirth)); - } - else + mCaretIcon->setVisible(false); + if (mDragAndDropTarget && !mButtonCommands.empty()) { - caret->setRect(LLRect(mDragx, - mDragy+caret_rect.getHeight()/2, - mDragx+mDragGirth, - mDragy-caret_rect.getHeight()/2)); + LLRect caret_rect = mCaretIcon->getRect(); + if (getOrientation(mSideType) == LLLayoutStack::HORIZONTAL) + { + mCaretIcon->setRect(LLRect(mDragx - caret_rect.getWidth() / 2 + 1, + mDragy, + mDragx + caret_rect.getWidth() / 2 + 1, + mDragy - mDragGirth)); + } + else + { + mCaretIcon->setRect(LLRect(mDragx, + mDragy + caret_rect.getHeight() / 2, + mDragx + mDragGirth, + mDragy - caret_rect.getHeight() / 2)); + } + mCaretIcon->setVisible(true); } - caret->setVisible(true); } LLUICtrl::draw(); - caret->setVisible(false); + if (mCaretIcon) + { + mCaretIcon->setVisible(false); + } mDragAndDropTarget = false; } diff --git a/indra/llui/lltoolbar.h b/indra/llui/lltoolbar.h index abf44f259a..b4fb3fab02 100644 --- a/indra/llui/lltoolbar.h +++ b/indra/llui/lltoolbar.h @@ -213,6 +213,7 @@ public: }; // virtuals + bool postBuild(); void draw(); void reshape(S32 width, S32 height, bool called_from_parent = true); bool handleRightMouseDown(S32 x, S32 y, MASK mask); diff --git a/indra/llui/llui.cpp b/indra/llui/llui.cpp index 5e01cffe38..5550223e1e 100644 --- a/indra/llui/llui.cpp +++ b/indra/llui/llui.cpp @@ -226,7 +226,7 @@ void LLUI::getMousePositionScreen(S32 *x, S32 *y) LLCoordGL cursor_pos_gl(cursor_pos_window.convert()); *x = ll_round((F32)cursor_pos_gl.mX / getScaleFactor().mV[VX]); *y = ll_round((F32)cursor_pos_gl.mY / getScaleFactor().mV[VY]); -#if defined(LL_SDL) +#if defined(LL_SDL_WINDOW) *x = *x * LLView::getWindow()->getSystemUISize(); *y = *y * LLView::getWindow()->getSystemUISize(); #endif diff --git a/indra/llui/lluicolortable.cpp b/indra/llui/lluicolortable.cpp index 7a4f72fe4f..90ba6f1ecc 100644 --- a/indra/llui/lluicolortable.cpp +++ b/indra/llui/lluicolortable.cpp @@ -298,7 +298,7 @@ void LLUIColorTable::saveUserSettings() const if(!output_node->isNull()) { const std::string& filename = gDirUtilp->getExpandedFilename(LL_PATH_USER_SETTINGS, "colors.xml"); - LLFILE *fp = LLFile::fopen(filename, "w"); + LLFILE *fp = LLFile::fopen(filename, LLFILE_MODE("w")); if(fp != NULL) { diff --git a/indra/llui/llurlaction.cpp b/indra/llui/llurlaction.cpp index 8b320b59cc..61a529c5d2 100644 --- a/indra/llui/llurlaction.cpp +++ b/indra/llui/llurlaction.cpp @@ -269,13 +269,22 @@ void LLUrlAction::reportAbuse(std::string url) } } +void LLUrlAction::reportAbuseObj(std::string url) +{ + std::string object_id = getObjectId(url); + if (LLUUID::validate(object_id)) + { + executeSLURL("secondlife:///app/object/" + object_id + "/reportAbuse"); + } +} + void LLUrlAction::blockObject(std::string url) { std::string object_id = getObjectId(url); std::string object_name = getObjectName(url); if (LLUUID::validate(object_id)) { - executeSLURL("secondlife:///app/agent/" + object_id + "/block/" + LLURI::escape(object_name)); + executeSLURL("secondlife:///app/object/" + object_id + "/block/" + LLURI::escape(object_name)); } } @@ -285,6 +294,6 @@ void LLUrlAction::unblockObject(std::string url) std::string object_name = getObjectName(url); if (LLUUID::validate(object_id)) { - executeSLURL("secondlife:///app/agent/" + object_id + "/unblock/" + object_name); + executeSLURL("secondlife:///app/object/" + object_id + "/unblock/" + LLURI::escape(object_name)); } } diff --git a/indra/llui/llurlaction.h b/indra/llui/llurlaction.h index c4cfd0f3fb..d110fe56d3 100644 --- a/indra/llui/llurlaction.h +++ b/indra/llui/llurlaction.h @@ -89,6 +89,7 @@ public: static void addFriend(std::string url); static void removeFriend(std::string url); static void reportAbuse(std::string url); + static void reportAbuseObj(std::string url); static void blockObject(std::string url); static void unblockObject(std::string url); diff --git a/indra/llui/llviewereventrecorder.h b/indra/llui/llviewereventrecorder.h index 5636c068d8..3bd8eb1f75 100644 --- a/indra/llui/llviewereventrecorder.h +++ b/indra/llui/llviewereventrecorder.h @@ -75,7 +75,7 @@ public: bool logEvents; std::string mLogFilename; - llofstream mLog; + llofstream mLog; private: diff --git a/indra/llui/llxuiparser.cpp b/indra/llui/llxuiparser.cpp index 52afac430e..dfb978624d 100644 --- a/indra/llui/llxuiparser.cpp +++ b/indra/llui/llxuiparser.cpp @@ -637,7 +637,7 @@ void LLXUIXSDWriter::writeXSD(const std::string& type_name, const std::string& p } } - LLFILE* xsd_file = LLFile::fopen(file_name.c_str(), "w"); + LLFILE* xsd_file = LLFile::fopen(file_name.c_str(), LLFILE_MODE("w")); LLXMLNode::writeHeaderToFile(xsd_file); root_nodep->writeToFile(xsd_file); fclose(xsd_file); @@ -1326,7 +1326,7 @@ void LLXUIParser::parserError(const std::string& message) struct ScopedFile { - ScopedFile( const std::string& filename, const char* accessmode ) + ScopedFile( const std::string& filename, const LLFile::fopen_flags_t* accessmode ) { mFile = LLFile::fopen(filename, accessmode); } @@ -1397,7 +1397,7 @@ bool LLSimpleXUIParser::readXUI(const std::string& filename, LLInitParam::BaseBl mCurReadDepth = 0; setParseSilently(silent); - ScopedFile file(filename, "rb"); + ScopedFile file(filename, LLFILE_MODE("rb")); if( !file.isOpen() ) { LL_WARNS("ReadXUI") << "Unable to open file " << filename << LL_ENDL; diff --git a/indra/llui/llxyvector.cpp b/indra/llui/llxyvector.cpp index 1521823ce2..b3c38bf82d 100644 --- a/indra/llui/llxyvector.cpp +++ b/indra/llui/llxyvector.cpp @@ -211,7 +211,16 @@ void LLXYVector::draw() mGhostY = pointY; } - if (abs(mValueX) >= mIncrementX || abs(mValueY) >= mIncrementY) + S32 dx = abs(pointX - centerX); + S32 dy = abs(pointY - centerY); + bool draw_arrow = + (abs(mValueX) >= mIncrementX || abs(mValueY) >= mIncrementY) + && (dx >= 1 || dy >= 1); // At least 1 pixel displacement + + // Todo: Arrow doesn't display well with small values. + // Ex: (0.1, 0.05) will point to the right, as if it is (0.1, 0.0) + // as position will be offset by a single pixes on X, and 0 pixels on Y. + if (draw_arrow) { // draw the vector arrow drawArrow(centerX, centerY, pointX, pointY, mArrowColor); diff --git a/indra/llui/tests/llurlentry_stub.cpp b/indra/llui/tests/llurlentry_stub.cpp index 95d1586495..8277c23691 100755..100644 --- a/indra/llui/tests/llurlentry_stub.cpp +++ b/indra/llui/tests/llurlentry_stub.cpp @@ -50,16 +50,16 @@ LLAvatarNameCache::callback_connection_t LLAvatarNameCache::get(const LLUUID& ag // // Stub implementation for LLCacheName // -BOOL LLCacheName::getFullName(const LLUUID& id, std::string& fullname) +bool LLCacheName::getFullName(const LLUUID& id, std::string& fullname) { fullname = "Lynx Linden"; - return TRUE; + return true; } -BOOL LLCacheName::getGroupName(const LLUUID& id, std::string& group) +bool LLCacheName::getGroupName(const LLUUID& id, std::string& group) { group = "My Group"; - return TRUE; + return true; } boost::signals2::connection LLCacheName::get(const LLUUID& id, bool is_group, const LLCacheNameCallback& callback) |
