diff options
24 files changed, 617 insertions, 24 deletions
| diff --git a/indra/llui/CMakeLists.txt b/indra/llui/CMakeLists.txt index a0314cb5f2..908e94b24c 100644 --- a/indra/llui/CMakeLists.txt +++ b/indra/llui/CMakeLists.txt @@ -18,6 +18,7 @@ set(llui_SOURCE_FILES      llbadgeowner.cpp      llbutton.cpp      llchatentry.cpp +    llchatmentionhelper.cpp      llcheckboxctrl.cpp      llclipboard.cpp      llcombobox.cpp @@ -130,6 +131,7 @@ set(llui_HEADER_FILES      llcallbackmap.h      llchatentry.h      llchat.h +    llchatmentionhelper.h      llcheckboxctrl.h      llclipboard.h      llcombobox.h diff --git a/indra/llui/llchatentry.cpp b/indra/llui/llchatentry.cpp index da5afd0386..55e4beafb6 100644 --- a/indra/llui/llchatentry.cpp +++ b/indra/llui/llchatentry.cpp @@ -51,6 +51,7 @@ LLChatEntry::LLChatEntry(const Params& p)      mCurrentHistoryLine = mLineHistory.begin();      mAutoIndent = false; +    mShowChatMentionPicker = true;      keepSelectionOnReturn(true);  } @@ -249,3 +250,21 @@ void LLChatEntry::enableSingleLineMode(bool single_line_mode)      mPrevLinesCount = -1;      setWordWrap(!single_line_mode);  } + +LLWString LLChatEntry::getConvertedText() const +{ +    LLWString text = getWText(); +    S32 diff = 0; +    for (auto segment : mSegments) +    { +        if (segment && segment->getStyle() && segment->getStyle()->getDrawHighlightBg()) +        { +            S32 seg_length = segment->getEnd() - segment->getStart(); +            std::string slurl = segment->getStyle()->getLinkHREF(); + +            text.replace(segment->getStart() + diff, seg_length, utf8str_to_wstring(slurl)); +            diff += (S32)slurl.size() - seg_length; +        } +    } +    return text; +} diff --git a/indra/llui/llchatentry.h b/indra/llui/llchatentry.h index 5621ede1e7..bb5eb8024d 100644 --- a/indra/llui/llchatentry.h +++ b/indra/llui/llchatentry.h @@ -68,6 +68,8 @@ public:      void enableSingleLineMode(bool single_line_mode);      boost::signals2::connection setTextExpandedCallback(const commit_signal_t::slot_type& cb); +    LLWString getConvertedText() const; +  private:      /** diff --git a/indra/llui/llchatmentionhelper.cpp b/indra/llui/llchatmentionhelper.cpp new file mode 100644 index 0000000000..98d846b947 --- /dev/null +++ b/indra/llui/llchatmentionhelper.cpp @@ -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) +{ +    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()); })); +        } +    } +} diff --git a/indra/llui/llchatmentionhelper.h b/indra/llui/llchatmentionhelper.h new file mode 100644 index 0000000000..4da8c8264e --- /dev/null +++ b/indra/llui/llchatmentionhelper.h @@ -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); +    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; +}; diff --git a/indra/llui/llflatlistview.cpp b/indra/llui/llflatlistview.cpp index 53f39766c6..25fe9f2556 100644 --- a/indra/llui/llflatlistview.cpp +++ b/indra/llui/llflatlistview.cpp @@ -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(); @@ -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); diff --git a/indra/llui/llflatlistview.h b/indra/llui/llflatlistview.h index 6d75e9f282..2a06ded5cb 100644 --- a/indra/llui/llflatlistview.h +++ b/indra/llui/llflatlistview.h @@ -299,6 +299,8 @@ public:      virtual S32 notify(const LLSD& info) ; +    void setFocusOnItemClicked(bool b) { mFocusOnItemClicked = b; } +      virtual ~LLFlatListView();  protected: @@ -423,6 +425,8 @@ private:      bool mKeepSelectionVisibleOnReshape; +    bool mFocusOnItemClicked; +      /** All pairs of the list */      pairs_list_t mItemPairs; diff --git a/indra/llui/lltextbase.cpp b/indra/llui/lltextbase.cpp index cb682a3625..5f62763683 100644 --- a/indra/llui/lltextbase.cpp +++ b/indra/llui/lltextbase.cpp @@ -2322,14 +2322,14 @@ static LLUIImagePtr image_from_icon_name(const std::string& icon_name)  } -void LLTextBase::appendTextImpl(const std::string &new_text, const LLStyle::Params& input_params) +void LLTextBase::appendTextImpl(const std::string& new_text, const LLStyle::Params& input_params, bool force_slurl)  {      LL_PROFILE_ZONE_SCOPED_CATEGORY_UI;      LLStyle::Params style_params(getStyleParams());      style_params.overwriteFrom(input_params);      S32 part = (S32)LLTextParser::WHOLE; -    if (mParseHTML && !style_params.is_link) // Don't search for URLs inside a link segment (STORM-358). +    if ((mParseHTML || force_slurl) && !style_params.is_link) // Don't search for URLs inside a link segment (STORM-358).      {          S32 start=0,end=0;          LLUrlMatch match; diff --git a/indra/llui/lltextbase.h b/indra/llui/lltextbase.h index fa8d22c819..a2895e6bd6 100644 --- a/indra/llui/lltextbase.h +++ b/indra/llui/lltextbase.h @@ -676,7 +676,7 @@ protected:      // avatar names are looked up.      void replaceUrl(const std::string &url, const std::string &label, const std::string& icon); -    void                            appendTextImpl(const std::string &new_text, const LLStyle::Params& input_params = LLStyle::Params()); +    void                            appendTextImpl(const std::string &new_text, const LLStyle::Params& input_params = LLStyle::Params(), bool force_slurl = false);      void                            appendAndHighlightTextImpl(const std::string &new_text, S32 highlight_part, const LLStyle::Params& style_params, e_underline underline_link = e_underline::UNDERLINE_ALWAYS);      S32 normalizeUri(std::string& uri); diff --git a/indra/llui/lltexteditor.cpp b/indra/llui/lltexteditor.cpp index fe4cce29ab..bd726c3d49 100644 --- a/indra/llui/lltexteditor.cpp +++ b/indra/llui/lltexteditor.cpp @@ -60,6 +60,7 @@  #include "llurlregistry.h"  #include "lltooltip.h"  #include "llmenugl.h" +#include "llchatmentionhelper.h"  #include <queue>  #include "llcombobox.h" @@ -270,6 +271,7 @@ LLTextEditor::LLTextEditor(const LLTextEditor::Params& p) :      mPrevalidator(p.prevalidator()),      mShowContextMenu(p.show_context_menu),      mShowEmojiHelper(p.show_emoji_helper), +    mShowChatMentionPicker(false),      mEnableTooltipPaste(p.enable_tooltip_paste),      mPassDelete(false),      mKeepSelectionOnReturn(false) @@ -714,6 +716,18 @@ void LLTextEditor::handleEmojiCommit(llwchar emoji)      }  } +void LLTextEditor::handleMentionCommit(std::string name_url) +{ +    S32 mention_start_pos; +    if (LLChatMentionHelper::instance().isCursorInNameMention(getWText(), mCursorPos, &mention_start_pos)) +    { +        remove(mention_start_pos, mCursorPos - mention_start_pos, true); +        setCursorPos(mention_start_pos); + +        appendTextImpl(name_url, LLStyle::Params(), true); +    } +} +  bool LLTextEditor::handleMouseDown(S32 x, S32 y, MASK mask)  {      bool    handled = false; @@ -1103,6 +1117,7 @@ void LLTextEditor::removeCharOrTab()          }          tryToShowEmojiHelper(); +        tryToShowMentionHelper();      }      else      { @@ -1128,6 +1143,7 @@ void LLTextEditor::removeChar()          setCursorPos(mCursorPos - 1);          removeChar(mCursorPos);          tryToShowEmojiHelper(); +        tryToShowMentionHelper();      }      else      { @@ -1189,6 +1205,7 @@ void LLTextEditor::addChar(llwchar wc)      setCursorPos(mCursorPos + addChar( mCursorPos, wc ));      tryToShowEmojiHelper(); +    tryToShowMentionHelper();      if (!mReadOnly && mAutoreplaceCallback != NULL)      { @@ -1247,6 +1264,31 @@ void LLTextEditor::tryToShowEmojiHelper()      }  } +void LLTextEditor::tryToShowMentionHelper() +{ +    if (mReadOnly || !mShowChatMentionPicker) +        return; + +    S32 mention_start_pos; +    LLWString text(getWText()); +    if (LLChatMentionHelper::instance().isCursorInNameMention(text, mCursorPos, &mention_start_pos)) +    { +        const LLRect cursor_rect(getLocalRectFromDocIndex(mention_start_pos)); +        std::string name_part(wstring_to_utf8str(text.substr(mention_start_pos, mCursorPos - mention_start_pos))); +        name_part.erase(0, 1); +        auto cb = [this](std::string name_url) +        { +            handleMentionCommit(name_url); +        }; +        LLChatMentionHelper::instance().showHelper(this, cursor_rect.mLeft, cursor_rect.mTop, name_part, cb); +    } +    else +    { +        LLChatMentionHelper::instance().hideHelper(); +    } +} + +  void LLTextEditor::addLineBreakChar(bool group_together)  {      if( !getEnabled() ) @@ -1884,9 +1926,13 @@ bool LLTextEditor::handleKeyHere(KEY key, MASK mask )      }      else      { -        if (!mReadOnly && mShowEmojiHelper && LLEmojiHelper::instance().handleKey(this, key, mask)) +        if (!mReadOnly)          { -            return true; +            if ((mShowEmojiHelper && LLEmojiHelper::instance().handleKey(this, key, mask)) || +                (mShowChatMentionPicker && LLChatMentionHelper::instance().handleKey(this, key, mask))) +            { +                return true; +            }          }          if (mEnableTooltipPaste && diff --git a/indra/llui/lltexteditor.h b/indra/llui/lltexteditor.h index b2b14b01e2..e38908734f 100644 --- a/indra/llui/lltexteditor.h +++ b/indra/llui/lltexteditor.h @@ -95,6 +95,8 @@ public:      void    insertEmoji(llwchar emoji);      void    handleEmojiCommit(llwchar emoji); +    void handleMentionCommit(std::string name_url); +      // mousehandler overrides      virtual bool    handleMouseDown(S32 x, S32 y, MASK mask);      virtual bool    handleMouseUp(S32 x, S32 y, MASK mask); @@ -258,6 +260,7 @@ protected:      S32             remove(S32 pos, S32 length, bool group_with_next_op);      void            tryToShowEmojiHelper(); +    void            tryToShowMentionHelper();      void            focusLostHelper();      void            updateAllowingLanguageInput();      bool            hasPreeditString() const; @@ -295,6 +298,7 @@ protected:      bool                mAutoIndent;      bool                mParseOnTheFly; +    bool                mShowChatMentionPicker;      void                updateLinkSegments();      void                keepSelectionOnReturn(bool keep) { mKeepSelectionOnReturn = keep; } diff --git a/indra/llui/llurlentry.cpp b/indra/llui/llurlentry.cpp index ce5ff0ff75..9657dc9527 100644 --- a/indra/llui/llurlentry.cpp +++ b/indra/llui/llurlentry.cpp @@ -580,7 +580,7 @@ LLUrlEntrySimpleSecondlifeURL::LLUrlEntrySimpleSecondlifeURL()  //  LLUrlEntryAgent::LLUrlEntryAgent()  { -    mPattern = boost::regex(APP_HEADER_REGEX "/agent/[\\da-f-]+/\\w+", +    mPattern = boost::regex(APP_HEADER_REGEX "/agent/[\\da-f-]+/(mention|(?!mention)\\w+)",                              boost::regex::perl|boost::regex::icase);      mMenuName = "menu_url_agent.xml";      mIcon = "Generic_Person"; @@ -784,7 +784,7 @@ std::string LLUrlEntryAgent::getIcon(const std::string &url)  {      // *NOTE: Could look up a badge here by calling getIDStringFromUrl()      // and looking up the badge for the agent. -    return mIcon; +    return LLStringUtil::endsWith(url, "/mention") ? std::string() : mIcon;  }  // diff --git a/indra/newview/CMakeLists.txt b/indra/newview/CMakeLists.txt index fff1597fd9..98151e2f4d 100644 --- a/indra/newview/CMakeLists.txt +++ b/indra/newview/CMakeLists.txt @@ -201,6 +201,7 @@ set(viewer_SOURCE_FILES      llfloatercamera.cpp      llfloatercamerapresets.cpp      llfloaterchangeitemthumbnail.cpp +    llfloaterchatmentionpicker.cpp      llfloaterchatvoicevolume.cpp      llfloaterclassified.cpp      llfloatercolorpicker.cpp @@ -870,6 +871,7 @@ set(viewer_HEADER_FILES      llfloaterbuyland.h      llfloatercamerapresets.h      llfloaterchangeitemthumbnail.h +    llfloaterchatmentionpicker.h      llfloatercamera.h      llfloaterchatvoicevolume.h      llfloaterclassified.h diff --git a/indra/newview/llavatarlist.cpp b/indra/newview/llavatarlist.cpp index 8f858fe4e1..f206474e71 100644 --- a/indra/newview/llavatarlist.cpp +++ b/indra/newview/llavatarlist.cpp @@ -141,6 +141,7 @@ LLAvatarList::LLAvatarList(const Params& p)  , mShowSpeakingIndicator(p.show_speaking_indicator)  , mShowPermissions(p.show_permissions_granted)  , mShowCompleteName(false) +, mForceCompleteName(false)  {      setCommitOnSelectionChange(true); @@ -177,7 +178,7 @@ void LLAvatarList::setShowIcons(std::string param_name)  std::string LLAvatarList::getAvatarName(LLAvatarName av_name)  { -    return mShowCompleteName? av_name.getCompleteName(false) : av_name.getDisplayName(); +    return mShowCompleteName? av_name.getCompleteName(false, mForceCompleteName) : av_name.getDisplayName();  }  // virtual @@ -364,7 +365,7 @@ void LLAvatarList::updateAvatarNames()      for( std::vector<LLPanel*>::const_iterator it = items.begin(); it != items.end(); it++)      {          LLAvatarListItem* item = static_cast<LLAvatarListItem*>(*it); -        item->setShowCompleteName(mShowCompleteName); +        item->setShowCompleteName(mShowCompleteName, mForceCompleteName);          item->updateAvatarName();      }      mNeedUpdateNames = false; @@ -404,6 +405,11 @@ boost::signals2::connection LLAvatarList::setItemDoubleClickCallback(const mouse      return mItemDoubleClickSignal.connect(cb);  } +boost::signals2::connection LLAvatarList::setItemClickedCallback(const mouse_signal_t::slot_type& cb) +{ +    return mItemClickedSignal.connect(cb); +} +  //virtual  S32 LLAvatarList::notifyParent(const LLSD& info)  { @@ -418,7 +424,7 @@ S32 LLAvatarList::notifyParent(const LLSD& info)  void LLAvatarList::addNewItem(const LLUUID& id, const std::string& name, bool is_online, EAddPosition pos)  {      LLAvatarListItem* item = new LLAvatarListItem(); -    item->setShowCompleteName(mShowCompleteName); +    item->setShowCompleteName(mShowCompleteName, mForceCompleteName);      // This sets the name as a side effect      item->setAvatarId(id, mSessionID, mIgnoreOnlineStatus);      item->setOnline(mIgnoreOnlineStatus ? true : is_online); @@ -432,6 +438,7 @@ void LLAvatarList::addNewItem(const LLUUID& id, const std::string& name, bool is      item->setDoubleClickCallback(boost::bind(&LLAvatarList::onItemDoubleClicked, this, _1, _2, _3, _4)); +    item->setMouseDownCallback(boost::bind(&LLAvatarList::onItemClicked, this, _1, _2, _3, _4));      addItem(item, id, pos);  } @@ -550,6 +557,11 @@ void LLAvatarList::onItemDoubleClicked(LLUICtrl* ctrl, S32 x, S32 y, MASK mask)      mItemDoubleClickSignal(ctrl, x, y, mask);  } +void LLAvatarList::onItemClicked(LLUICtrl* ctrl, S32 x, S32 y, MASK mask) +{ +    mItemClickedSignal(ctrl, x, y, mask); +} +  bool LLAvatarItemComparator::compare(const LLPanel* item1, const LLPanel* item2) const  {      const LLAvatarListItem* avatar_item1 = dynamic_cast<const LLAvatarListItem*>(item1); diff --git a/indra/newview/llavatarlist.h b/indra/newview/llavatarlist.h index af5bfefcde..f99da93a3d 100644 --- a/indra/newview/llavatarlist.h +++ b/indra/newview/llavatarlist.h @@ -96,11 +96,13 @@ public:      boost::signals2::connection setItemDoubleClickCallback(const mouse_signal_t::slot_type& cb); +    boost::signals2::connection setItemClickedCallback(const mouse_signal_t::slot_type& cb); +      virtual S32 notifyParent(const LLSD& info);      void handleDisplayNamesOptionChanged(); -    void setShowCompleteName(bool show) { mShowCompleteName = show;}; +    void setShowCompleteName(bool show, bool force = false) { mShowCompleteName = show; mForceCompleteName = force; };  protected:      void refresh(); @@ -113,6 +115,7 @@ protected:      void updateLastInteractionTimes();      void rebuildNames();      void onItemDoubleClicked(LLUICtrl* ctrl, S32 x, S32 y, MASK mask); +    void onItemClicked(LLUICtrl* ctrl, S32 x, S32 y, MASK mask);      void updateAvatarNames();  private: @@ -127,6 +130,7 @@ private:      bool mShowSpeakingIndicator;      bool mShowPermissions;      bool mShowCompleteName; +    bool mForceCompleteName;      LLTimer*                mLITUpdateTimer; // last interaction time update timer      std::string             mIconParamName; @@ -138,6 +142,7 @@ private:      commit_signal_t mRefreshCompleteSignal;      mouse_signal_t mItemDoubleClickSignal; +    mouse_signal_t mItemClickedSignal;  };  /** Abstract comparator for avatar items */ diff --git a/indra/newview/llavatarlistitem.cpp b/indra/newview/llavatarlistitem.cpp index 880910d18e..6ef45ed160 100644 --- a/indra/newview/llavatarlistitem.cpp +++ b/indra/newview/llavatarlistitem.cpp @@ -78,6 +78,7 @@ LLAvatarListItem::LLAvatarListItem(bool not_from_ui_factory/* = true*/)      mShowProfileBtn(true),      mShowPermissions(false),      mShowCompleteName(false), +    mForceCompleteName(false),      mHovered(false),      mAvatarNameCacheConnection(),      mGreyOutUsername("") @@ -324,13 +325,11 @@ void LLAvatarListItem::setShowProfileBtn(bool show)  void LLAvatarListItem::showSpeakingIndicator(bool visible)  { -    // Already done? Then do nothing. -    if (mSpeakingIndicator->getVisible() == (bool)visible) -        return; -// Disabled to not contradict with SpeakingIndicatorManager functionality. EXT-3976 -// probably this method should be totally removed. -//  mSpeakingIndicator->setVisible(visible); -//  updateChildren(); +    if (mSpeakingIndicator) +    { +        mSpeakingIndicator->setIsActiveChannel(visible); +        mSpeakingIndicator->setShowParticipantsSpeaking(visible); +    }  }  void LLAvatarListItem::setAvatarIconVisible(bool visible) @@ -417,8 +416,8 @@ void LLAvatarListItem::onAvatarNameCache(const LLAvatarName& av_name)      mAvatarNameCacheConnection.disconnect();      mGreyOutUsername = ""; -    std::string name_string = mShowCompleteName? av_name.getCompleteName(false) : av_name.getDisplayName(); -    if(av_name.getCompleteName() != av_name.getUserName()) +    std::string name_string = mShowCompleteName? av_name.getCompleteName(false, mForceCompleteName) : av_name.getDisplayName(); +    if(av_name.getCompleteName(false, mForceCompleteName) != av_name.getUserName())      {          mGreyOutUsername = "[ " + av_name.getUserName(true) + " ]";          LLStringUtil::toLower(mGreyOutUsername); diff --git a/indra/newview/llavatarlistitem.h b/indra/newview/llavatarlistitem.h index 2e4c597d30..2ec7a41055 100644 --- a/indra/newview/llavatarlistitem.h +++ b/indra/newview/llavatarlistitem.h @@ -106,7 +106,7 @@ public:      void setShowPermissions(bool show) { mShowPermissions = show; };      void showLastInteractionTime(bool show);      void setAvatarIconVisible(bool visible); -    void setShowCompleteName(bool show) { mShowCompleteName = show;}; +    void setShowCompleteName(bool show, bool force = false) { mShowCompleteName = show; mForceCompleteName = force;};      const LLUUID& getAvatarId() const;      std::string getAvatarName() const; @@ -220,6 +220,7 @@ private:      bool mHovered;      bool mShowCompleteName; +    bool mForceCompleteName;      std::string mGreyOutUsername;      void fetchAvatarName(); diff --git a/indra/newview/llfloaterchatmentionpicker.cpp b/indra/newview/llfloaterchatmentionpicker.cpp new file mode 100644 index 0000000000..dda2cd83f6 --- /dev/null +++ b/indra/newview/llfloaterchatmentionpicker.cpp @@ -0,0 +1,183 @@ +/** + * @file llfloaterchatmentionpicker.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 "llviewerprecompiledheaders.h" + +#include "llfloaterchatmentionpicker.h" + +#include "llavatarlist.h" +#include "llfloaterimcontainer.h" +#include "llchatmentionhelper.h" +#include "llparticipantlist.h" + +LLUUID LLFloaterChatMentionPicker::sSessionID(LLUUID::null); + +LLFloaterChatMentionPicker::LLFloaterChatMentionPicker(const LLSD& key) +: LLFloater(key), mAvatarList(NULL) +{ +    // This floater should hover on top of our dependent (with the dependent having the focus) +    setFocusStealsFrontmost(false); +    setBackgroundVisible(false); +    setAutoFocus(false); +} + +bool LLFloaterChatMentionPicker::postBuild() +{ +    mAvatarList = getChild<LLAvatarList>("avatar_list"); +    mAvatarList->setShowCompleteName(true, true); +    mAvatarList->setFocusOnItemClicked(false); +    mAvatarList->setItemClickedCallback([this](LLUICtrl* ctrl, S32 x, S32 y, MASK mask) +    { +        if (LLAvatarListItem* item = dynamic_cast<LLAvatarListItem*>(ctrl)) +        { +            selectResident(item->getAvatarId()); +        } +    }); +    mAvatarList->setRefreshCompleteCallback([this](LLUICtrl* ctrl, const LLSD& param) +    { +        if (mAvatarList->numSelected() == 0) +        { +            mAvatarList->selectFirstItem(); +        } +    }); + +    return LLFloater::postBuild(); +} + +void LLFloaterChatMentionPicker::onOpen(const LLSD& key) +{ +    buildAvatarList(); +    mAvatarList->setNameFilter(key.has("av_name") ? key["av_name"].asString() : ""); + +    gFloaterView->adjustToFitScreen(this, false); +} + +uuid_vec_t LLFloaterChatMentionPicker::getParticipantIds() +{ +    LLParticipantList* item = dynamic_cast<LLParticipantList*>(LLFloaterIMContainer::getInstance()->getSessionModel(sSessionID)); +    if (!item) +    { +        LL_WARNS() << "Participant list is missing" << LL_ENDL; +        return {}; +    } + +    uuid_vec_t avatar_ids; +    LLFolderViewModelItemCommon::child_list_t::const_iterator current_participant_model = item->getChildrenBegin(); +    LLFolderViewModelItemCommon::child_list_t::const_iterator end_participant_model = item->getChildrenEnd(); +    while (current_participant_model != end_participant_model) +    { +        LLConversationItem* participant_model = dynamic_cast<LLConversationItem*>(*current_participant_model); +        if (participant_model) +        { +            avatar_ids.push_back(participant_model->getUUID()); +        } +        current_participant_model++; +    } +    return avatar_ids; +} + +void LLFloaterChatMentionPicker::buildAvatarList() +{ +    uuid_vec_t& avatar_ids = mAvatarList->getIDs(); +    avatar_ids = getParticipantIds(); +    updateAvatarList(avatar_ids); +    mAvatarList->setDirty(); +} + +void LLFloaterChatMentionPicker::selectResident(const LLUUID& id) +{ +    if (id.isNull()) +        return; + +    setValue(stringize("secondlife:///app/agent/", id.asString(), "/mention ")); +    onCommit(); +    LLChatMentionHelper::instance().hideHelper(); +} + +void LLFloaterChatMentionPicker::onClose(bool app_quitting) +{ +    if (!app_quitting) +    { +        LLChatMentionHelper::instance().hideHelper(); +    } +} + +bool LLFloaterChatMentionPicker::handleKey(KEY key, MASK mask, bool called_from_parent) +{ +    if (mask == MASK_NONE) +    { +        switch (key) +        { +            case KEY_UP: +            case KEY_DOWN: +                return mAvatarList->handleKey(key, mask, called_from_parent); +            case KEY_RETURN: +                selectResident(mAvatarList->getSelectedUUID()); +                return true; +            case KEY_ESCAPE: +                LLChatMentionHelper::instance().hideHelper(); +                return true; +            case KEY_LEFT: +            case KEY_RIGHT: +                return true; +            default: +                break; +        } +    } +    return LLFloater::handleKey(key, mask, called_from_parent); +} + +void LLFloaterChatMentionPicker::goneFromFront() +{ +    LLChatMentionHelper::instance().hideHelper(); +} + +void LLFloaterChatMentionPicker::updateSessionID(LLUUID session_id) +{ +    sSessionID = session_id; + +    LLParticipantList* item = dynamic_cast<LLParticipantList*>(LLFloaterIMContainer::getInstance()->getSessionModel(sSessionID)); +    if (!item) +    { +        LL_WARNS() << "Participant list is missing" << LL_ENDL; +        return; +    } + +    uuid_vec_t avatar_ids = getParticipantIds(); +    updateAvatarList(avatar_ids); +} + +void LLFloaterChatMentionPicker::updateAvatarList(uuid_vec_t& avatar_ids) +{ +    std::vector<std::string> av_names; +    for (auto& id : avatar_ids) +    { +        LLAvatarName av_name; +        LLAvatarNameCache::get(id, &av_name); +        av_names.push_back(utf8str_tolower(av_name.getAccountName())); +        av_names.push_back(utf8str_tolower(av_name.getDisplayName())); +    } +    LLChatMentionHelper::instance().updateAvatarList(av_names); +} diff --git a/indra/newview/llfloaterchatmentionpicker.h b/indra/newview/llfloaterchatmentionpicker.h new file mode 100644 index 0000000000..8d221d7a89 --- /dev/null +++ b/indra/newview/llfloaterchatmentionpicker.h @@ -0,0 +1,58 @@ +/** + * @file llfloaterchatmentionpicker.h + * + * $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$ + */ + +#ifndef LLFLOATERCHATMENTIONPICKER_H +#define LLFLOATERCHATMENTIONPICKER_H + +#include "llfloater.h" + +class LLAvatarList; + +class LLFloaterChatMentionPicker : public LLFloater +{ +public: +    LLFloaterChatMentionPicker(const LLSD& key); + +    virtual bool postBuild() override; +    virtual void goneFromFront() override; + +    void buildAvatarList(); + +    static uuid_vec_t getParticipantIds(); +    static void updateSessionID(LLUUID session_id); +    static void updateAvatarList(uuid_vec_t& avatar_ids); + +private: + +    void onOpen(const LLSD& key) override; +    void onClose(bool app_quitting) override; +    virtual bool handleKey(KEY key, MASK mask, bool called_from_parent) override; +    void selectResident(const LLUUID& id); + +    static LLUUID sSessionID; +    LLAvatarList* mAvatarList; +}; + +#endif diff --git a/indra/newview/llfloaterimnearbychat.cpp b/indra/newview/llfloaterimnearbychat.cpp index db6f9ac22a..b649514bff 100644 --- a/indra/newview/llfloaterimnearbychat.cpp +++ b/indra/newview/llfloaterimnearbychat.cpp @@ -586,7 +586,7 @@ void LLFloaterIMNearbyChat::sendChat( EChatType type )  {      if (mInputEditor)      { -        LLWString text = mInputEditor->getWText(); +        LLWString text = mInputEditor->getConvertedText();          LLWStringUtil::trim(text);          LLWStringUtil::replaceChar(text,182,'\n'); // Convert paragraph symbols back into newlines.          if (!text.empty()) diff --git a/indra/newview/llfloaterimsession.cpp b/indra/newview/llfloaterimsession.cpp index 185274981b..84a9fad708 100644 --- a/indra/newview/llfloaterimsession.cpp +++ b/indra/newview/llfloaterimsession.cpp @@ -251,7 +251,7 @@ void LLFloaterIMSession::sendMsgFromInputEditor()      {          if (mInputEditor)          { -            LLWString text = mInputEditor->getWText(); +            LLWString text = mInputEditor->getConvertedText();              LLWStringUtil::trim(text);              LLWStringUtil::replaceChar(text,182,'\n'); // Convert paragraph symbols back into newlines.              if(!text.empty()) diff --git a/indra/newview/llfloaterimsessiontab.cpp b/indra/newview/llfloaterimsessiontab.cpp index 50e765c236..96aac8c1e6 100644 --- a/indra/newview/llfloaterimsessiontab.cpp +++ b/indra/newview/llfloaterimsessiontab.cpp @@ -35,6 +35,7 @@  #include "llavatariconctrl.h"  #include "llchatentry.h"  #include "llchathistory.h" +#include "llfloaterchatmentionpicker.h"  #include "llchiclet.h"  #include "llchicletbar.h"  #include "lldraghandle.h" @@ -485,6 +486,7 @@ void LLFloaterIMSessionTab::onFocusReceived()          LLIMModel::instance().sendNoUnreadMessages(mSessionID);      } +    LLFloaterChatMentionPicker::updateSessionID(mSessionID);      super::onFocusReceived();  } diff --git a/indra/newview/llviewerfloaterreg.cpp b/indra/newview/llviewerfloaterreg.cpp index 95c2a77ba8..4d9c2f3281 100644 --- a/indra/newview/llviewerfloaterreg.cpp +++ b/indra/newview/llviewerfloaterreg.cpp @@ -58,6 +58,7 @@  #include "llfloatercamera.h"  #include "llfloatercamerapresets.h"  #include "llfloaterchangeitemthumbnail.h" +#include "llfloaterchatmentionpicker.h"  #include "llfloaterchatvoicevolume.h"  #include "llfloaterclassified.h"  #include "llfloaterconversationlog.h" @@ -353,6 +354,7 @@ void LLViewerFloaterReg::registerFloaters()      LLFloaterReg::add("chat_voice", "floater_voice_chat_volume.xml", (LLFloaterBuildFunc)&LLFloaterReg::build<LLFloaterChatVoiceVolume>);      LLFloaterReg::add("change_item_thumbnail", "floater_change_item_thumbnail.xml", (LLFloaterBuildFunc)&LLFloaterReg::build<LLFloaterChangeItemThumbnail>);      LLFloaterReg::add("nearby_chat", "floater_im_session.xml", (LLFloaterBuildFunc)&LLFloaterIMNearbyChat::buildFloater); +    LLFloaterReg::add("chat_mention_picker", "floater_chat_mention_picker.xml", (LLFloaterBuildFunc)&LLFloaterReg::build<LLFloaterChatMentionPicker>);      LLFloaterReg::add("classified", "floater_classified.xml", (LLFloaterBuildFunc)&LLFloaterReg::build<LLFloaterClassified>);      LLFloaterReg::add("compile_queue", "floater_script_queue.xml", (LLFloaterBuildFunc)&LLFloaterReg::build<LLFloaterCompileQueue>);      LLFloaterReg::add("conversation", "floater_conversation_log.xml", (LLFloaterBuildFunc)&LLFloaterReg::build<LLFloaterConversationLog>); diff --git a/indra/newview/skins/default/xui/en/floater_chat_mention_picker.xml b/indra/newview/skins/default/xui/en/floater_chat_mention_picker.xml new file mode 100644 index 0000000000..bbad99f932 --- /dev/null +++ b/indra/newview/skins/default/xui/en/floater_chat_mention_picker.xml @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="utf-8" standalone="yes" ?> +<floater +    name="chat_mention_picker" +    title="CHOOSE RESIDENT" +    single_instance="true" +    can_minimize="false" +    can_tear_off="false" +    can_resize="true" +    auto_close="true" +    layout="topleft" +    min_width="250" +    chrome="true" +    height="125" +    width="310"> +   <avatar_list +    allow_select="true" +    follows="all" +    height="120" +    width="306" +    ignore_online_status="true" +    layout="topleft" +    left="3" +    keep_one_selected="true" +    multi_select="false" +    show_info_btn="false" +    show_profile_btn="false" +    show_speaking_indicator="false" +    name="avatar_list" +    right="-1" +    top="2" /> +</floater> | 
