Skip to content

#3758 initial chat mention support #3943

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Apr 25, 2025
2 changes: 2 additions & 0 deletions indra/llui/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ set(llui_SOURCE_FILES
llbadgeowner.cpp
llbutton.cpp
llchatentry.cpp
llchatmentionhelper.cpp
llcheckboxctrl.cpp
llclipboard.cpp
llcombobox.cpp
Expand Down Expand Up @@ -130,6 +131,7 @@ set(llui_HEADER_FILES
llcallbackmap.h
llchatentry.h
llchat.h
llchatmentionhelper.h
llcheckboxctrl.h
llclipboard.h
llcombobox.h
Expand Down
1 change: 1 addition & 0 deletions indra/llui/llchatentry.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ LLChatEntry::LLChatEntry(const Params& p)
mCurrentHistoryLine = mLineHistory.begin();

mAutoIndent = false;
mShowChatMentionPicker = true;
keepSelectionOnReturn(true);
}

Expand Down
151 changes: 151 additions & 0 deletions indra/llui/llchatmentionhelper.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
/**
* @file llchatmentionhelper.cpp
*
* $LicenseInfo:firstyear=2025&license=viewerlgpl$
* Second Life Viewer Source Code
* Copyright (C) 2025, 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 "llchatmentionhelper.h"
#include "llfloater.h"
#include "llfloaterreg.h"
#include "lluictrl.h"

constexpr char CHAT_MENTION_HELPER_FLOATER[] = "chat_mention_picker";

bool LLChatMentionHelper::isActive(const LLUICtrl* ctrl) const
{
return mHostHandle.get() == ctrl;
}

bool LLChatMentionHelper::isCursorInNameMention(const LLWString& wtext, S32 cursor_pos, S32* mention_start_pos) const
{
if (cursor_pos <= 0 || cursor_pos > static_cast<S32>(wtext.size()))
return false;

// Find the beginning of the current word
S32 start = cursor_pos - 1;
while (start > 0 && wtext[start - 1] != U32(' ') && wtext[start - 1] != U32('\n'))
{
--start;
}

if (wtext[start] != U32('@'))
return false;

if (mention_start_pos)
*mention_start_pos = start;

S32 word_length = cursor_pos - start;

if (word_length == 1)
{
return true;
}

// Get the name after '@'
std::string name = wstring_to_utf8str(wtext.substr(start + 1, word_length - 1));
LLStringUtil::toLower(name);
for (const auto& av_name : mAvatarNames)
{
if (av_name == name || av_name.find(name) == 0)
{
return true;
}
}

return false;
}

void LLChatMentionHelper::showHelper(LLUICtrl* host_ctrl, S32 local_x, S32 local_y, const std::string& av_name, std::function<void(std::string)> cb)
{
if (mHelperHandle.isDead())
{
LLFloater* av_picker_floater = LLFloaterReg::getInstance(CHAT_MENTION_HELPER_FLOATER);
mHelperHandle = av_picker_floater->getHandle();
mHelperCommitConn = av_picker_floater->setCommitCallback([&](LLUICtrl* ctrl, const LLSD& param) { onCommitName(param.asString()); });
}
setHostCtrl(host_ctrl);
mNameCommitCb = cb;

S32 floater_x, floater_y;
if (!host_ctrl->localPointToOtherView(local_x, local_y, &floater_x, &floater_y, gFloaterView))
{
LL_WARNS() << "Cannot show helper for non-floater controls." << LL_ENDL;
return;
}

LLFloater* av_picker_floater = mHelperHandle.get();
LLRect rect = av_picker_floater->getRect();
rect.setLeftTopAndSize(floater_x, floater_y + rect.getHeight(), rect.getWidth(), rect.getHeight());
av_picker_floater->setRect(rect);
av_picker_floater->openFloater(LLSD().with("av_name", av_name));
}

void LLChatMentionHelper::hideHelper(const LLUICtrl* ctrl)
{
if ((ctrl && !isActive(ctrl)))
{
return;
}
setHostCtrl(nullptr);
}

bool LLChatMentionHelper::handleKey(const LLUICtrl* ctrl, KEY key, MASK mask)
{
if (mHelperHandle.isDead() || !isActive(ctrl))
{
return false;
}

return mHelperHandle.get()->handleKey(key, mask, true);
}

void LLChatMentionHelper::onCommitName(std::string name_url)
{
if (!mHostHandle.isDead() && mNameCommitCb)
{
mNameCommitCb(name_url);
}
}

void LLChatMentionHelper::setHostCtrl(LLUICtrl* host_ctrl)
{
const LLUICtrl* pCurHostCtrl = mHostHandle.get();
if (pCurHostCtrl != host_ctrl)
{
mHostCtrlFocusLostConn.disconnect();
mHostHandle.markDead();
mNameCommitCb = {};

if (!mHelperHandle.isDead())
{
mHelperHandle.get()->closeFloater();
}

if (host_ctrl)
{
mHostHandle = host_ctrl->getHandle();
mHostCtrlFocusLostConn = host_ctrl->setFocusLostCallback(std::bind([&]() { hideHelper(getHostCtrl()); }));
}
}
}
66 changes: 66 additions & 0 deletions indra/llui/llchatmentionhelper.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/**
* @file llchatmentionhelper.h
* @brief Header file for LLChatMentionHelper
*
* $LicenseInfo:firstyear=2025&license=viewerlgpl$
* Second Life Viewer Source Code
* Copyright (C) 2025, 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>

class LLFloater;
class LLUICtrl;

class LLChatMentionHelper : public LLSingleton<LLChatMentionHelper>
{
LLSINGLETON(LLChatMentionHelper) {}
~LLChatMentionHelper() override {}

public:

bool isActive(const LLUICtrl* ctrl) const;
bool isCursorInNameMention(const LLWString& wtext, S32 cursor_pos, S32* mention_start_pos = nullptr) const;
void showHelper(LLUICtrl* host_ctrl, S32 local_x, S32 local_y, const std::string& av_name, std::function<void(std::string)> commit_cb);
void hideHelper(const LLUICtrl* ctrl = nullptr);

bool handleKey(const LLUICtrl* ctrl, KEY key, MASK mask);
void onCommitName(std::string name_url);

void updateAvatarList(std::vector<std::string> av_names) { mAvatarNames = av_names; }

protected:
void setHostCtrl(LLUICtrl* host_ctrl);
LLUICtrl* getHostCtrl() const { return mHostHandle.get(); }

private:
LLHandle<LLUICtrl> mHostHandle;
LLHandle<LLFloater> mHelperHandle;
boost::signals2::connection mHostCtrlFocusLostConn;
boost::signals2::connection mHelperCommitConn;
std::function<void(std::string)> mNameCommitCb;

std::vector<std::string> mAvatarNames;
};
6 changes: 5 additions & 1 deletion indra/llui/llflatlistview.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,7 @@ LLFlatListView::LLFlatListView(const LLFlatListView::Params& p)
, mNoItemsCommentTextbox(NULL)
, mIsConsecutiveSelection(false)
, mKeepSelectionVisibleOnReshape(p.keep_selection_visible_on_reshape)
, mFocusOnItemClicked(true)
{
mBorderThickness = getBorderWidth();

Expand Down Expand Up @@ -610,7 +611,10 @@ void LLFlatListView::onItemMouseClick(item_pair_t* item_pair, MASK mask)
return;
}

setFocus(true);
if (mFocusOnItemClicked)
{
setFocus(true);
}

bool select_item = !isSelected(item_pair);

Expand Down
4 changes: 4 additions & 0 deletions indra/llui/llflatlistview.h
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,8 @@ class LLFlatListView : public LLScrollContainer, public LLEditMenuHandler

virtual S32 notify(const LLSD& info) override;

void setFocusOnItemClicked(bool b) { mFocusOnItemClicked = b; }

virtual ~LLFlatListView();

protected:
Expand Down Expand Up @@ -423,6 +425,8 @@ class LLFlatListView : public LLScrollContainer, public LLEditMenuHandler

bool mKeepSelectionVisibleOnReshape;

bool mFocusOnItemClicked;

/** All pairs of the list */
pairs_list_t mItemPairs;

Expand Down
8 changes: 6 additions & 2 deletions indra/llui/llstyle.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,13 @@ LLStyle::Params::Params()
color("color", LLColor4::black),
readonly_color("readonly_color", LLColor4::black),
selected_color("selected_color", LLColor4::black),
highlight_bg_color("highlight_bg_color", LLColor4::green),
alpha("alpha", 1.f),
font("font", LLStyle::getDefaultFont()),
image("image"),
link_href("href"),
is_link("is_link")
is_link("is_link"),
draw_highlight_bg("draw_highlight_bg", false)
{}


Expand All @@ -51,12 +53,14 @@ LLStyle::LLStyle(const LLStyle::Params& p)
mColor(p.color),
mReadOnlyColor(p.readonly_color),
mSelectedColor(p.selected_color),
mHighlightBgColor(p.highlight_bg_color),
mFont(p.font()),
mLink(p.link_href),
mIsLink(p.is_link.isProvided() ? p.is_link : !p.link_href().empty()),
mDropShadow(p.drop_shadow),
mImagep(p.image()),
mAlpha(p.alpha)
mAlpha(p.alpha),
mDrawHighlightBg(p.draw_highlight_bg)
{}

void LLStyle::setFont(const LLFontGL* font)
Expand Down
21 changes: 19 additions & 2 deletions indra/llui/llstyle.h
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,25 @@ class LLStyle : public LLRefCount
Optional<LLFontGL::ShadowType> drop_shadow;
Optional<LLUIColor> color,
readonly_color,
selected_color;
selected_color,
highlight_bg_color;
Optional<F32> alpha;
Optional<const LLFontGL*> font;
Optional<LLUIImage*> image;
Optional<std::string> link_href;
Optional<bool> is_link;
Optional<bool> draw_highlight_bg;
Params();
};
LLStyle(const Params& p = Params());

enum EUnderlineLink
{
UNDERLINE_ALWAYS = 0,
UNDERLINE_ON_HOVER,
UNDERLINE_NEVER
};

public:
const LLUIColor& getColor() const { return mColor; }
void setColor(const LLUIColor &color) { mColor = color; }
Expand Down Expand Up @@ -84,18 +94,23 @@ class LLStyle : public LLRefCount

bool isImage() const { return mImagep.notNull(); }

bool getDrawHighlightBg() const { return mDrawHighlightBg; }
const LLUIColor& getHighlightBgColor() const { return mHighlightBgColor; }

bool operator==(const LLStyle &rhs) const
{
return
mVisible == rhs.mVisible
&& mColor == rhs.mColor
&& mReadOnlyColor == rhs.mReadOnlyColor
&& mSelectedColor == rhs.mSelectedColor
&& mHighlightBgColor == rhs.mHighlightBgColor
&& mFont == rhs.mFont
&& mLink == rhs.mLink
&& mImagep == rhs.mImagep
&& mDropShadow == rhs.mDropShadow
&& mAlpha == rhs.mAlpha;
&& mAlpha == rhs.mAlpha
&& mDrawHighlightBg == rhs.mDrawHighlightBg;
}

bool operator!=(const LLStyle& rhs) const { return !(*this == rhs); }
Expand All @@ -112,11 +127,13 @@ class LLStyle : public LLRefCount
LLUIColor mColor;
LLUIColor mReadOnlyColor;
LLUIColor mSelectedColor;
LLUIColor mHighlightBgColor;
const LLFontGL* mFont;
LLPointer<LLUIImage> mImagep;
F32 mAlpha;
bool mVisible;
bool mIsLink;
bool mDrawHighlightBg;
};

typedef LLPointer<LLStyle> LLStyleSP;
Expand Down
Loading