summaryrefslogtreecommitdiff
path: root/indra/llui
diff options
context:
space:
mode:
authorErik Kundiman <erik@megapahit.org>2026-09-01 18:35:23 +0800
committerErik Kundiman <erik@megapahit.org>2026-09-04 21:58:53 +0800
commit00bb3bb6f07660bd914d29a1389342bb3060d254 (patch)
tree431f574922494d2201718f13085f17bc6bd7fb42 /indra/llui
parenta8636720129952c10cf49ab80da21bf8b340b96c (diff)
parent4ef9f8f14b35ed388c294d53767a0dca0e890924 (diff)
Merge remote-tracking branch 'secondlife/release/26.4' into 26.4
Diffstat (limited to 'indra/llui')
-rw-r--r--indra/llui/CMakeLists.txt14
-rw-r--r--indra/llui/llcallbackmap.h6
-rw-r--r--indra/llui/llcheckboxctrl.h5
-rw-r--r--indra/llui/llcombobox.cpp19
-rw-r--r--indra/llui/llcombobox.h5
-rw-r--r--indra/llui/llemojihelper.h2
-rw-r--r--indra/llui/llflatlistview.cpp473
-rw-r--r--indra/llui/llflatlistview.h78
-rw-r--r--indra/llui/llfloater.cpp2
-rw-r--r--indra/llui/llflyoutbutton.cpp20
-rw-r--r--indra/llui/llflyoutbutton.h2
-rw-r--r--indra/llui/llfolderview.cpp13
-rw-r--r--indra/llui/llfolderviewitem.cpp30
-rw-r--r--indra/llui/llfolderviewitem.h7
-rw-r--r--indra/llui/llgestureautocompletehelper.cpp165
-rw-r--r--indra/llui/llgestureautocompletehelper.h80
-rw-r--r--indra/llui/llkeywords.cpp402
-rw-r--r--indra/llui/llkeywords.h40
-rw-r--r--indra/llui/lllineeditor.cpp7
-rw-r--r--indra/llui/llresmgr.cpp40
-rw-r--r--indra/llui/llresmgr.h4
-rw-r--r--indra/llui/llscrollcontainer.cpp33
-rw-r--r--indra/llui/llscrollcontainer.h5
-rw-r--r--indra/llui/llscrolllistctrl.cpp13
-rw-r--r--indra/llui/llscrolllistctrl.h3
-rw-r--r--indra/llui/lltextbase.cpp1
-rw-r--r--indra/llui/lltextbase.h1
-rw-r--r--indra/llui/lltexteditor.cpp20
-rw-r--r--indra/llui/lltexteditor.h2
-rw-r--r--indra/llui/lltextvalidate.cpp12
-rw-r--r--indra/llui/lltextvalidate.h2
-rw-r--r--indra/llui/lltoolbar.cpp52
-rw-r--r--indra/llui/lltoolbar.h1
-rw-r--r--indra/llui/llui.cpp2
-rw-r--r--indra/llui/lluicolortable.cpp2
-rw-r--r--indra/llui/llurlaction.cpp13
-rw-r--r--indra/llui/llurlaction.h1
-rw-r--r--indra/llui/llviewereventrecorder.h2
-rw-r--r--indra/llui/llxuiparser.cpp6
-rw-r--r--indra/llui/llxyvector.cpp11
-rw-r--r--[-rwxr-xr-x]indra/llui/tests/llurlentry_stub.cpp8
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)