summaryrefslogtreecommitdiff
path: root/indra/newview/llfloateremojipicker.cpp
diff options
context:
space:
mode:
authorAndrey Lihatskiy <alihatskiy@productengine.com>2024-03-04 18:01:05 +0200
committerAndrey Lihatskiy <alihatskiy@productengine.com>2024-03-04 18:01:05 +0200
commitb68a05e7c30930976ed1273b4c7a0fec01d2a84f (patch)
tree03ababecc49376c0e607b3548d9beb596f79c3a7 /indra/newview/llfloateremojipicker.cpp
parentd47c6536820d1ed6e373147678dd0fab90e80ab8 (diff)
parent701d1a33bb8227aa55a71f48caeb30a453e77ee0 (diff)
Merge branch 'main' into marchcat/x-merge
# Conflicts: # indra/llcommon/llstring.cpp # indra/llcommon/llstring.h
Diffstat (limited to 'indra/newview/llfloateremojipicker.cpp')
-rw-r--r--indra/newview/llfloateremojipicker.cpp1346
1 files changed, 1346 insertions, 0 deletions
diff --git a/indra/newview/llfloateremojipicker.cpp b/indra/newview/llfloateremojipicker.cpp
new file mode 100644
index 0000000000..1578caa39c
--- /dev/null
+++ b/indra/newview/llfloateremojipicker.cpp
@@ -0,0 +1,1346 @@
+/**
+ * @file llfloateremojipicker.cpp
+ *
+ * $LicenseInfo:firstyear=2003&license=viewerlgpl$
+ * Second Life Viewer Source Code
+ * Copyright (C) 2010, 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 "llfloateremojipicker.h"
+
+#include "llappviewer.h"
+#include "llbutton.h"
+#include "llcombobox.h"
+#include "llemojidictionary.h"
+#include "llemojihelper.h"
+#include "llfloaterreg.h"
+#include "llkeyboard.h"
+#include "llscrollcontainer.h"
+#include "llscrollingpanellist.h"
+#include "llscrolllistctrl.h"
+#include "llscrolllistitem.h"
+#include "llsdserialize.h"
+#include "lltextbox.h"
+#include "llviewerchat.h"
+
+namespace {
+// The following variables and constants are used for storing the floater state
+// between different lifecycles of the floater and different sissions of the viewer
+
+// Floater constants
+static const S32 ALL_EMOJIS_GROUP_INDEX = -2;
+// https://www.compart.com/en/unicode/U+1F50D
+static const S32 ALL_EMOJIS_IMAGE_INDEX = 0x1F50D;
+static const S32 USED_EMOJIS_GROUP_INDEX = -1;
+// https://www.compart.com/en/unicode/U+23F2
+static const S32 USED_EMOJIS_IMAGE_INDEX = 0x23F2;
+// https://www.compart.com/en/unicode/U+1F6D1
+static const S32 EMPTY_LIST_IMAGE_INDEX = 0x1F6D1;
+// The following categories should follow the required alphabetic order
+static const std::string RECENTLY_USED_CATEGORY = "1 recently used";
+static const std::string FREQUENTLY_USED_CATEGORY = "2 frequently used";
+
+// Floater state related variables
+static std::list<llwchar> sRecentlyUsed;
+static std::list<std::pair<llwchar, U32>> sFrequentlyUsed;
+
+// State file related values
+static std::string sStateFileName;
+static const std::string sKeyRecentlyUsed("RecentlyUsed");
+static const std::string sKeyFrequentlyUsed("FrequentlyUsed");
+}
+
+class LLEmojiGridRow : public LLScrollingPanel
+{
+public:
+ LLEmojiGridRow(const LLPanel::Params& panel_params,
+ const LLScrollingPanelList::Params& list_params)
+ : LLScrollingPanel(panel_params)
+ , mList(new LLScrollingPanelList(list_params))
+ {
+ addChild(mList);
+ }
+
+ virtual void updatePanel(BOOL allow_modify) override {}
+
+public:
+ LLScrollingPanelList* mList;
+};
+
+class LLEmojiGridDivider : public LLScrollingPanel
+{
+public:
+ LLEmojiGridDivider(const LLPanel::Params& panel_params, std::string text)
+ : LLScrollingPanel(panel_params)
+ , mText(utf8string_to_wstring(text))
+ {
+ }
+
+ virtual void draw() override
+ {
+ LLScrollingPanel::draw();
+
+ F32 x = 4; // padding-left
+ F32 y = getRect().getHeight() / 2;
+ LLFontGL::getFontSansSerif()->render(
+ mText, // wstr
+ 0, // begin_offset
+ x, // x
+ y, // y
+ LLColor4::white, // color
+ LLFontGL::LEFT, // halign
+ LLFontGL::VCENTER, // valign
+ LLFontGL::NORMAL, // style
+ LLFontGL::DROP_SHADOW_SOFT, // shadow
+ mText.size()); // max_chars
+ }
+
+ virtual void updatePanel(BOOL allow_modify) override {}
+
+private:
+ const LLWString mText;
+};
+
+class LLEmojiGridIcon : public LLScrollingPanel
+{
+public:
+ LLEmojiGridIcon(
+ const LLPanel::Params& panel_params
+ , const LLEmojiSearchResult& emoji)
+ : LLScrollingPanel(panel_params)
+ , mData(emoji)
+ , mText(LLWString(1, emoji.Character))
+ {
+ }
+
+ virtual void draw() override
+ {
+ LLScrollingPanel::draw();
+
+ F32 x = getRect().getWidth() / 2;
+ F32 y = getRect().getHeight() / 2;
+ LLFontGL::getFontEmoji()->render(
+ mText, // wstr
+ 0, // begin_offset
+ x, // x
+ y, // y
+ LLColor4::white, // color
+ LLFontGL::HCENTER, // halign
+ LLFontGL::VCENTER, // valign
+ LLFontGL::NORMAL, // style
+ LLFontGL::DROP_SHADOW_SOFT, // shadow
+ 1); // max_chars
+ }
+
+ virtual void updatePanel(BOOL allow_modify) override {}
+
+ const LLEmojiSearchResult& getData() const { return mData; }
+ LLWString getText() const { return mText; }
+
+private:
+ const LLEmojiSearchResult mData;
+ const LLWString mText;
+};
+
+class LLEmojiPreviewPanel : public LLPanel
+{
+public:
+ LLEmojiPreviewPanel()
+ : LLPanel()
+ {
+ }
+
+ void setIcon(const LLEmojiGridIcon* icon)
+ {
+ if (icon)
+ {
+ setData(icon->getData().Character, icon->getData().String, icon->getData().Begin, icon->getData().End);
+ }
+ else
+ {
+ setData(0, LLStringUtil::null, 0, 0);
+ }
+ }
+
+ void setData(llwchar emoji, std::string title, size_t begin, size_t end)
+ {
+ mWStr = LLWString(1, emoji);
+ mEmoji = emoji;
+ mTitle = title;
+ mBegin = begin;
+ mEnd = end;
+ }
+
+ virtual void draw() override
+ {
+ LLPanel::draw();
+
+ S32 clientHeight = getRect().getHeight();
+ S32 clientWidth = getRect().getWidth();
+ S32 iconWidth = clientHeight;
+
+ F32 centerX = 0.5f * iconWidth;
+ F32 centerY = 0.5f * clientHeight;
+ drawIcon(centerX, centerY - 1, iconWidth);
+
+ static LLColor4 defaultColor(0.75f, 0.75f, 0.75f, 1.0f);
+ LLColor4 textColor = LLUIColorTable::instance().getColor("MenuItemEnabledColor", defaultColor);
+ S32 max_pixels = clientWidth - iconWidth;
+ drawName(iconWidth, centerY, max_pixels, textColor);
+ }
+
+protected:
+ void drawIcon(F32 x, F32 y, S32 max_pixels)
+ {
+ LLFontGL::getFontEmojiHuge()->render(
+ mWStr, // wstr
+ 0, // begin_offset
+ x, // x
+ y, // y
+ LLColor4::white, // color
+ LLFontGL::HCENTER, // halign
+ LLFontGL::VCENTER, // valign
+ LLFontGL::NORMAL, // style
+ LLFontGL::DROP_SHADOW_SOFT, // shadow
+ 1, // max_chars
+ max_pixels); // max_pixels
+ }
+
+ void drawName(F32 x, F32 y, S32 max_pixels, LLColor4& color)
+ {
+ F32 x0 = x;
+ F32 x1 = max_pixels;
+ LLFontGL* font = LLFontGL::getFontEmoji();
+ if (mBegin)
+ {
+ std::string text = mTitle.substr(0, mBegin);
+ font->renderUTF8(
+ text, // text
+ 0, // begin_offset
+ x0, // x
+ y, // y
+ color, // color
+ LLFontGL::LEFT, // halign
+ LLFontGL::VCENTER, // valign
+ LLFontGL::NORMAL, // style
+ LLFontGL::DROP_SHADOW_SOFT, // shadow
+ text.size(), // max_chars
+ x1); // max_pixels
+ F32 dx = font->getWidthF32(text);
+ x0 += dx;
+ x1 -= dx;
+ }
+ if (x1 > 0 && mEnd > mBegin)
+ {
+ std::string text = mTitle.substr(mBegin, mEnd - mBegin);
+ font->renderUTF8(
+ text, // text
+ 0, // begin_offset
+ x0, // x
+ y, // y
+ LLColor4::yellow6, // color
+ LLFontGL::LEFT, // halign
+ LLFontGL::VCENTER, // valign
+ LLFontGL::NORMAL, // style
+ LLFontGL::DROP_SHADOW_SOFT, // shadow
+ text.size(), // max_chars
+ x1); // max_pixels
+ F32 dx = font->getWidthF32(text);
+ x0 += dx;
+ x1 -= dx;
+ }
+ if (x1 > 0 && mEnd < mTitle.size())
+ {
+ std::string text = mEnd ? mTitle.substr(mEnd) : mTitle;
+ font->renderUTF8(
+ text, // text
+ 0, // begin_offset
+ x0, // x
+ y, // y
+ color, // color
+ LLFontGL::LEFT, // halign
+ LLFontGL::VCENTER, // valign
+ LLFontGL::NORMAL, // style
+ LLFontGL::DROP_SHADOW_SOFT, // shadow
+ text.size(), // max_chars
+ x1); // max_pixels
+ }
+ }
+
+private:
+ llwchar mEmoji;
+ LLWString mWStr;
+ std::string mTitle;
+ size_t mBegin;
+ size_t mEnd;
+};
+
+LLFloaterEmojiPicker::LLFloaterEmojiPicker(const LLSD& key)
+: super(key)
+{
+ // This floater should hover on top of our dependent (with the dependent having the focus)
+ setFocusStealsFrontmost(FALSE);
+ setBackgroundVisible(FALSE);
+ setAutoFocus(FALSE);
+
+ loadState();
+}
+
+BOOL LLFloaterEmojiPicker::postBuild()
+{
+ mGroups = getChild<LLPanel>("Groups");
+ mBadge = getChild<LLPanel>("Badge");
+ mEmojiScroll = getChild<LLScrollContainer>("EmojiGridContainer");
+ mEmojiGrid = getChild<LLScrollingPanelList>("EmojiGrid");
+ mDummy = getChild<LLTextBox>("Dummy");
+
+ mPreview = new LLEmojiPreviewPanel();
+ mPreview->setVisible(FALSE);
+ addChild(mPreview);
+
+ return LLFloater::postBuild();
+}
+
+void LLFloaterEmojiPicker::onOpen(const LLSD& key)
+{
+ mHint = key["hint"].asString();
+
+ LLEmojiHelper::instance().setIsHideDisabled(mHint.empty());
+ mFilterPattern = mHint;
+
+ initialize();
+
+ gFloaterView->adjustToFitScreen(this, FALSE);
+}
+
+void LLFloaterEmojiPicker::dirtyRect()
+{
+ super::dirtyRect();
+
+ if (!mPreview)
+ return;
+
+ const S32 HPADDING = 4;
+ const S32 VOFFSET = 12;
+ LLRect rect(HPADDING, mDummy->getRect().mTop + 6, getRect().getWidth() - HPADDING, VOFFSET);
+ if (mPreview->getRect() != rect)
+ {
+ mPreview->setRect(rect);
+ }
+
+ if (mEmojiScroll && mEmojiGrid)
+ {
+ S32 outer_width = mEmojiScroll->getRect().getWidth();
+ S32 inner_width = mEmojiGrid->getRect().getWidth();
+ if (outer_width != inner_width)
+ {
+ resizeGroupButtons();
+ fillEmojis(true);
+ }
+ }
+}
+
+void LLFloaterEmojiPicker::initialize()
+{
+ S32 groupIndex = mSelectedGroupIndex && mSelectedGroupIndex <= mFilteredEmojiGroups.size() ?
+ mFilteredEmojiGroups[mSelectedGroupIndex - 1] : ALL_EMOJIS_GROUP_INDEX;
+
+ fillGroups();
+
+ if (mFilteredEmojis.empty())
+ {
+ if (!mHint.empty())
+ {
+ hideFloater();
+ return;
+ }
+
+ mGroups->setVisible(FALSE);
+ mFocusedIconRow = -1;
+ mFocusedIconCol = -1;
+ mFocusedIcon = nullptr;
+ mHoveredIcon = nullptr;
+ mEmojiScroll->goToTop();
+ mEmojiGrid->clearPanels();
+
+ if (mFilterPattern.empty())
+ {
+ showPreview(false);
+ }
+ else
+ {
+ const std::string prompt("No emoji found for ");
+ std::string title(prompt + '"' + mFilterPattern.substr(1) + '"');
+ mPreview->setData(EMPTY_LIST_IMAGE_INDEX, title, prompt.size() + 1, title.size() - 1);
+ showPreview(true);
+ }
+ return;
+ }
+
+ mGroups->setVisible(TRUE);
+ mPreview->setIcon(nullptr);
+ showPreview(true);
+
+ mSelectedGroupIndex = groupIndex == ALL_EMOJIS_GROUP_INDEX ? 0 :
+ (1 + std::distance(mFilteredEmojiGroups.begin(),
+ std::find(mFilteredEmojiGroups.begin(), mFilteredEmojiGroups.end(), groupIndex))) %
+ (1 + mFilteredEmojiGroups.size());
+
+ mGroupButtons[mSelectedGroupIndex]->setToggleState(TRUE);
+ mGroupButtons[mSelectedGroupIndex]->setUseFontColor(TRUE);
+
+ fillEmojis();
+}
+
+void LLFloaterEmojiPicker::fillGroups()
+{
+ // Do not use deleteAllChildren() because mBadge shouldn't be removed
+ for (LLButton* button : mGroupButtons)
+ {
+ mGroups->removeChild(button);
+ }
+ mFilteredEmojiGroups.clear();
+ mFilteredEmojis.clear();
+ mGroupButtons.clear();
+
+ LLButton::Params params;
+ params.font = LLFontGL::getFontEmoji();
+
+ LLRect rect;
+ rect.mTop = mGroups->getRect().getHeight();
+ rect.mBottom = mBadge->getRect().getHeight();
+
+ // Create button for "All categories"
+ createGroupButton(params, rect, ALL_EMOJIS_IMAGE_INDEX);
+
+ // Create group and button for "Recently used" and/or "Frequently used"
+ if (!sRecentlyUsed.empty() || !sFrequentlyUsed.empty())
+ {
+ std::map<std::string, std::vector<LLEmojiSearchResult>> cats;
+ fillCategoryRecentlyUsed(cats);
+ fillCategoryFrequentlyUsed(cats);
+
+ if (!cats.empty())
+ {
+ mFilteredEmojiGroups.push_back(USED_EMOJIS_GROUP_INDEX);
+ mFilteredEmojis.emplace_back(cats);
+ createGroupButton(params, rect, USED_EMOJIS_IMAGE_INDEX);
+ }
+ }
+
+ const std::vector<LLEmojiGroup>& groups = LLEmojiDictionary::instance().getGroups();
+
+ // List all categories in the dictionary
+ for (U32 i = 0; i < groups.size(); ++i)
+ {
+ std::map<std::string, std::vector<LLEmojiSearchResult>> cats;
+
+ fillGroupEmojis(cats, i);
+
+ if (!cats.empty())
+ {
+ mFilteredEmojiGroups.push_back(i);
+ mFilteredEmojis.emplace_back(cats);
+ createGroupButton(params, rect, groups[i].Character);
+ }
+ }
+
+ resizeGroupButtons();
+}
+
+void LLFloaterEmojiPicker::fillCategoryRecentlyUsed(std::map<std::string, std::vector<LLEmojiSearchResult>>& cats)
+{
+ if (sRecentlyUsed.empty())
+ return;
+
+ std::vector<LLEmojiSearchResult> emojis;
+
+ // In case of empty mFilterPattern we'd use sRecentlyUsed directly
+ if (!mFilterPattern.empty())
+ {
+ // List all emojis in "Recently used"
+ const LLEmojiDictionary::emoji2descr_map_t& emoji2descr = LLEmojiDictionary::instance().getEmoji2Descr();
+ std::size_t begin, end;
+ for (llwchar emoji : sRecentlyUsed)
+ {
+ auto e2d = emoji2descr.find(emoji);
+ if (e2d != emoji2descr.end() && !e2d->second->ShortCodes.empty())
+ {
+ const std::string shortcode(e2d->second->ShortCodes.front());
+ if (LLEmojiDictionary::searchInShortCode(begin, end, shortcode, mFilterPattern))
+ {
+ emojis.emplace_back(emoji, shortcode, begin, end);
+ }
+ }
+ }
+ if (emojis.empty())
+ return;
+ }
+
+ cats.emplace(std::make_pair(RECENTLY_USED_CATEGORY, emojis));
+}
+
+void LLFloaterEmojiPicker::fillCategoryFrequentlyUsed(std::map<std::string, std::vector<LLEmojiSearchResult>>& cats)
+{
+ if (sFrequentlyUsed.empty())
+ return;
+
+ std::vector<LLEmojiSearchResult> emojis;
+
+ // In case of empty mFilterPattern we'd use sFrequentlyUsed directly
+ if (!mFilterPattern.empty())
+ {
+ // List all emojis in "Frequently used"
+ const LLEmojiDictionary::emoji2descr_map_t& emoji2descr = LLEmojiDictionary::instance().getEmoji2Descr();
+ std::size_t begin, end;
+ for (const auto& emoji : sFrequentlyUsed)
+ {
+ auto e2d = emoji2descr.find(emoji.first);
+ if (e2d != emoji2descr.end() && !e2d->second->ShortCodes.empty())
+ {
+ const std::string shortcode(e2d->second->ShortCodes.front());
+ if (LLEmojiDictionary::searchInShortCode(begin, end, shortcode, mFilterPattern))
+ {
+ emojis.emplace_back(emoji.first, shortcode, begin, end);
+ }
+ }
+ }
+ if (emojis.empty())
+ return;
+ }
+
+ cats.emplace(std::make_pair(FREQUENTLY_USED_CATEGORY, emojis));
+}
+
+void LLFloaterEmojiPicker::fillGroupEmojis(std::map<std::string, std::vector<LLEmojiSearchResult>>& cats, U32 index)
+{
+ const std::vector<LLEmojiGroup>& groups = LLEmojiDictionary::instance().getGroups();
+ const LLEmojiDictionary::cat2descrs_map_t& category2Descr = LLEmojiDictionary::instance().getCategory2Descrs();
+
+ for (const std::string& category : groups[index].Categories)
+ {
+ const LLEmojiDictionary::cat2descrs_map_t::const_iterator& c2d = category2Descr.find(category);
+ if (c2d == category2Descr.end())
+ continue;
+
+ std::vector<LLEmojiSearchResult> emojis;
+
+ // In case of empty mFilterPattern we'd use category2Descr directly
+ if (!mFilterPattern.empty())
+ {
+ // List all emojis in category
+ std::size_t begin, end;
+ for (const LLEmojiDescriptor* descr : c2d->second)
+ {
+ if (!descr->ShortCodes.empty())
+ {
+ const std::string shortcode(descr->ShortCodes.front());
+ if (LLEmojiDictionary::searchInShortCode(begin, end, shortcode, mFilterPattern))
+ {
+ emojis.emplace_back(descr->Character, shortcode, begin, end);
+ }
+ }
+ }
+ if (emojis.empty())
+ continue;
+ }
+
+ cats.emplace(std::make_pair(category, emojis));
+ }
+}
+
+void LLFloaterEmojiPicker::createGroupButton(LLButton::Params& params, const LLRect& rect, llwchar emoji)
+{
+ LLButton* button = LLUICtrlFactory::create<LLButton>(params);
+ button->setClickedCallback([this](LLUICtrl* ctrl, const LLSD&) { onGroupButtonClick(ctrl); });
+ button->setMouseEnterCallback([this](LLUICtrl* ctrl, const LLSD&) { onGroupButtonMouseEnter(ctrl); });
+ button->setMouseLeaveCallback([this](LLUICtrl* ctrl, const LLSD&) { onGroupButtonMouseLeave(ctrl); });
+
+ button->setRect(rect);
+ button->setTabStop(FALSE);
+ button->setLabel(LLUIString(LLWString(1, emoji)));
+ button->setUseFontColor(FALSE);
+
+ mGroupButtons.push_back(button);
+ mGroups->addChild(button);
+}
+
+void LLFloaterEmojiPicker::resizeGroupButtons()
+{
+ U32 groupCount = (U32)mGroupButtons.size();
+ if (!groupCount)
+ return;
+
+ S32 totalWidth = mGroups->getRect().getWidth();
+ S32 badgeWidth = totalWidth / groupCount;
+ S32 leftOffset = (totalWidth - badgeWidth * groupCount) / 2;
+
+ for (U32 i = 0; i < groupCount; ++i)
+ {
+ LLRect rect = mGroupButtons[i]->getRect();
+ rect.mLeft = leftOffset + badgeWidth * i;
+ rect.mRight = rect.mLeft + badgeWidth;
+ mGroupButtons[i]->setRect(rect);
+ }
+
+ LLRect rect = mBadge->getRect();
+ rect.mLeft = leftOffset + badgeWidth * mSelectedGroupIndex;
+ rect.mRight = rect.mLeft + badgeWidth;
+ mBadge->setRect(rect);
+}
+
+void LLFloaterEmojiPicker::selectEmojiGroup(U32 index)
+{
+ if (index == mSelectedGroupIndex || index >= mGroupButtons.size())
+ return;
+
+ if (mSelectedGroupIndex < mGroupButtons.size())
+ {
+ mGroupButtons[mSelectedGroupIndex]->setUseFontColor(FALSE);
+ mGroupButtons[mSelectedGroupIndex]->setToggleState(FALSE);
+ }
+
+ mSelectedGroupIndex = index;
+ mGroupButtons[mSelectedGroupIndex]->setToggleState(TRUE);
+ mGroupButtons[mSelectedGroupIndex]->setUseFontColor(TRUE);
+
+ LLButton* button = mGroupButtons[mSelectedGroupIndex];
+ LLRect rect = mBadge->getRect();
+ rect.mLeft = button->getRect().mLeft;
+ rect.mRight = button->getRect().mRight;
+ mBadge->setRect(rect);
+
+ fillEmojis();
+}
+
+void LLFloaterEmojiPicker::fillEmojis(bool fromResize)
+{
+ S32 scrollbar_size = mEmojiScroll->getSize();
+ if (scrollbar_size < 0)
+ {
+ static LLUICachedControl<S32> scrollbar_size_control("UIScrollbarSize", 0);
+ scrollbar_size = scrollbar_size_control;
+ }
+
+ const S32 scroll_width = mEmojiScroll->getRect().getWidth();
+ const S32 client_width = scroll_width - scrollbar_size - mEmojiScroll->getBorderWidth() * 2;
+ const S32 grid_padding = mEmojiGrid->getPadding();
+ const S32 icon_spacing = mEmojiGrid->getSpacing();
+ const S32 row_width = client_width - grid_padding * 2;
+ const S32 icon_size = 28; // icon width and height
+ const S32 max_icons = llmax(1, (row_width + icon_spacing) / (icon_size + icon_spacing));
+
+ // Optimization: don't rearrange for different widths with the same maxIcons
+ if (fromResize && (max_icons == mRecentMaxIcons))
+ return;
+
+ mRecentMaxIcons = max_icons;
+
+ mFocusedIconRow = 0;
+ mFocusedIconCol = 0;
+ mFocusedIcon = nullptr;
+ mHoveredIcon = nullptr;
+ mEmojiScroll->goToTop();
+ mEmojiGrid->clearPanels();
+ mPreview->setIcon(nullptr);
+
+ if (mEmojiGrid->getRect().getWidth() != client_width)
+ {
+ LLRect rect = mEmojiGrid->getRect();
+ rect.mRight = rect.mLeft + client_width;
+ mEmojiGrid->setRect(rect);
+ }
+
+ LLPanel::Params row_panel_params;
+ row_panel_params.rect = LLRect(0, icon_size, row_width, 0);
+
+ LLScrollingPanelList::Params row_list_params;
+ row_list_params.rect = row_panel_params.rect;
+ row_list_params.is_horizontal = TRUE;
+ row_list_params.padding = 0;
+ row_list_params.spacing = icon_spacing;
+
+ LLPanel::Params icon_params;
+ LLRect icon_rect(0, icon_size, icon_size, 0);
+
+ static LLColor4 default_color(0.75f, 0.75f, 0.75f, 1.0f);
+ LLColor4 bg_color = LLUIColorTable::instance().getColor("MenuItemHighlightBgColor", default_color);
+
+ if (!mSelectedGroupIndex)
+ {
+ // List all groups
+ for (const auto& group : mFilteredEmojis)
+ {
+ // List all categories in the group
+ for (const auto& category : group)
+ {
+ // List all emojis in the category
+ fillEmojisCategory(category.second, category.first, row_panel_params,
+ row_list_params, icon_params, icon_rect, max_icons, bg_color);
+ }
+ }
+ }
+ else
+ {
+ // List all categories in the selected group
+ const auto& group = mFilteredEmojis[mSelectedGroupIndex - 1];
+ for (const auto& category : group)
+ {
+ // List all emojis in the category
+ fillEmojisCategory(category.second, category.first, row_panel_params,
+ row_list_params, icon_params, icon_rect, max_icons, bg_color);
+ }
+ }
+
+ if (mEmojiGrid->getPanelList().empty())
+ {
+ showPreview(false);
+ mFocusedIconRow = -1;
+ mFocusedIconCol = -1;
+ if (!mHint.empty())
+ {
+ hideFloater();
+ }
+ }
+ else
+ {
+ showPreview(true);
+ mFocusedIconRow = 0;
+ mFocusedIconCol = 0;
+ moveFocusedIconNext();
+ }
+}
+
+void LLFloaterEmojiPicker::fillEmojisCategory(const std::vector<LLEmojiSearchResult>& emojis,
+ const std::string& category, const LLPanel::Params& row_panel_params, const LLUICtrl::Params& row_list_params,
+ const LLPanel::Params& icon_params, const LLRect& icon_rect, S32 max_icons, const LLColor4& bg)
+{
+ // Place the category title
+ std::string title =
+ category == RECENTLY_USED_CATEGORY ? getString("title_for_recently_used") :
+ category == FREQUENTLY_USED_CATEGORY ? getString("title_for_frequently_used") :
+ isupper(category.front()) ? category : LLStringUtil::capitalize(category);
+ LLEmojiGridDivider* div = new LLEmojiGridDivider(row_panel_params, title);
+ mEmojiGrid->addPanel(div, true);
+
+ int icon_index = 0;
+ LLEmojiGridRow* row = nullptr;
+
+ if (mFilterPattern.empty())
+ {
+ const LLEmojiDictionary::emoji2descr_map_t& emoji2descr = LLEmojiDictionary::instance().getEmoji2Descr();
+ LLEmojiSearchResult emoji { 0, "", 0, 0 };
+ if (category == RECENTLY_USED_CATEGORY)
+ {
+ for (llwchar code : sRecentlyUsed)
+ {
+ const LLEmojiDictionary::emoji2descr_map_t::const_iterator& e2d = emoji2descr.find(code);
+ if (e2d != emoji2descr.end() && !e2d->second->ShortCodes.empty())
+ {
+ emoji.Character = code;
+ emoji.String = e2d->second->ShortCodes.front();
+ createEmojiIcon(emoji, category, row_panel_params, row_list_params, icon_params,
+ icon_rect, max_icons, bg, row, icon_index);
+ }
+ }
+ }
+ else if (category == FREQUENTLY_USED_CATEGORY)
+ {
+ for (const auto& code : sFrequentlyUsed)
+ {
+ const LLEmojiDictionary::emoji2descr_map_t::const_iterator& e2d = emoji2descr.find(code.first);
+ if (e2d != emoji2descr.end() && !e2d->second->ShortCodes.empty())
+ {
+ emoji.Character = code.first;
+ emoji.String = e2d->second->ShortCodes.front();
+ createEmojiIcon(emoji, category, row_panel_params, row_list_params, icon_params,
+ icon_rect, max_icons, bg, row, icon_index);
+ }
+ }
+ }
+ else
+ {
+ const LLEmojiDictionary::cat2descrs_map_t& category2Descr = LLEmojiDictionary::instance().getCategory2Descrs();
+ const LLEmojiDictionary::cat2descrs_map_t::const_iterator& c2d = category2Descr.find(category);
+ if (c2d != category2Descr.end())
+ {
+ for (const LLEmojiDescriptor* descr : c2d->second)
+ {
+ emoji.Character = descr->Character;
+ emoji.String = descr->ShortCodes.front();
+ createEmojiIcon(emoji, category, row_panel_params, row_list_params, icon_params,
+ icon_rect, max_icons, bg, row, icon_index);
+ }
+ }
+ }
+ }
+ else
+ {
+ for (const LLEmojiSearchResult& emoji : emojis)
+ {
+ createEmojiIcon(emoji, category, row_panel_params, row_list_params, icon_params,
+ icon_rect, max_icons, bg, row, icon_index);
+ }
+ }
+}
+
+void LLFloaterEmojiPicker::createEmojiIcon(const LLEmojiSearchResult& emoji,
+ const std::string& category, const LLPanel::Params& row_panel_params, const LLUICtrl::Params& row_list_params,
+ const LLPanel::Params& icon_params, const LLRect& icon_rect, S32 max_icons, const LLColor4& bg,
+ LLEmojiGridRow*& row, int& icon_index)
+{
+ // Place a new row each (max_icons) icons
+ if (!(icon_index % max_icons))
+ {
+ row = new LLEmojiGridRow(row_panel_params, *(const LLScrollingPanelList::Params*)&row_list_params);
+ mEmojiGrid->addPanel(row, true);
+ }
+
+ // Place a new icon to the current row
+ LLEmojiGridIcon* icon = new LLEmojiGridIcon(icon_params, emoji);
+ icon->setMouseEnterCallback([this](LLUICtrl* ctrl, const LLSD&) { onEmojiMouseEnter(ctrl); });
+ icon->setMouseLeaveCallback([this](LLUICtrl* ctrl, const LLSD&) { onEmojiMouseLeave(ctrl); });
+ icon->setMouseDownCallback([this](LLUICtrl* ctrl, S32, S32, MASK) { onEmojiMouseDown(ctrl); });
+ icon->setMouseUpCallback([this](LLUICtrl* ctrl, S32, S32, MASK) { onEmojiMouseUp(ctrl); });
+ icon->setBackgroundColor(bg);
+ icon->setBackgroundOpaque(1);
+ icon->setRect(icon_rect);
+ row->mList->addPanel(icon, true);
+
+ icon_index++;
+}
+
+void LLFloaterEmojiPicker::showPreview(bool show)
+{
+ //mPreview->setIcon(nullptr);
+ mDummy->setVisible(show ? FALSE : TRUE);
+ mPreview->setVisible(show ? TRUE : FALSE);
+}
+
+void LLFloaterEmojiPicker::onGroupButtonClick(LLUICtrl* ctrl)
+{
+ if (LLButton* button = dynamic_cast<LLButton*>(ctrl))
+ {
+ if (button == mGroupButtons[mSelectedGroupIndex] || button->getToggleState())
+ return;
+
+ auto it = std::find(mGroupButtons.begin(), mGroupButtons.end(), button);
+ if (it == mGroupButtons.end())
+ return;
+
+ selectEmojiGroup(it - mGroupButtons.begin());
+ }
+}
+
+void LLFloaterEmojiPicker::onGroupButtonMouseEnter(LLUICtrl* ctrl)
+{
+ if (LLButton* button = dynamic_cast<LLButton*>(ctrl))
+ {
+ button->setUseFontColor(TRUE);
+ }
+}
+
+void LLFloaterEmojiPicker::onGroupButtonMouseLeave(LLUICtrl* ctrl)
+{
+ if (LLButton* button = dynamic_cast<LLButton*>(ctrl))
+ {
+ button->setUseFontColor(button->getToggleState());
+ }
+}
+
+void LLFloaterEmojiPicker::onEmojiMouseEnter(LLUICtrl* ctrl)
+{
+ if (LLEmojiGridIcon* icon = dynamic_cast<LLEmojiGridIcon*>(ctrl))
+ {
+ if (mFocusedIcon && mFocusedIcon != icon && mFocusedIcon->isBackgroundVisible())
+ {
+ unselectGridIcon(mFocusedIcon);
+ }
+
+ if (mHoveredIcon && mHoveredIcon != icon)
+ {
+ unselectGridIcon(mHoveredIcon);
+ }
+
+ selectGridIcon(icon);
+
+ mHoveredIcon = icon;
+ }
+}
+
+void LLFloaterEmojiPicker::onEmojiMouseLeave(LLUICtrl* ctrl)
+{
+ if (LLEmojiGridIcon* icon = dynamic_cast<LLEmojiGridIcon*>(ctrl))
+ {
+ if (icon == mHoveredIcon)
+ {
+ if (icon != mFocusedIcon)
+ {
+ unselectGridIcon(icon);
+ }
+ mHoveredIcon = nullptr;
+ }
+
+ if (!mHoveredIcon && mFocusedIcon && !mFocusedIcon->isBackgroundVisible())
+ {
+ selectGridIcon(mFocusedIcon);
+ }
+ }
+}
+
+void LLFloaterEmojiPicker::onEmojiMouseDown(LLUICtrl* ctrl)
+{
+ if (getSoundFlags() & MOUSE_DOWN)
+ {
+ make_ui_sound("UISndClick");
+ }
+}
+
+void LLFloaterEmojiPicker::onEmojiMouseUp(LLUICtrl* ctrl)
+{
+ if (getSoundFlags() & MOUSE_UP)
+ {
+ make_ui_sound("UISndClickRelease");
+ }
+
+ if (LLEmojiGridIcon* icon = dynamic_cast<LLEmojiGridIcon*>(ctrl))
+ {
+ LLSD value(wstring_to_utf8str(icon->getText()));
+ setValue(value);
+
+ onCommit();
+
+ if (!mHint.empty() || !(gKeyboard->currentMask(TRUE) & MASK_SHIFT))
+ {
+ hideFloater();
+ }
+ }
+}
+
+void LLFloaterEmojiPicker::selectFocusedIcon()
+{
+ if (mFocusedIcon && mFocusedIcon != mHoveredIcon)
+ {
+ unselectGridIcon(mFocusedIcon);
+ }
+
+ // Both mFocusedIconRow and mFocusedIconCol should be already verified
+ LLEmojiGridRow* row = dynamic_cast<LLEmojiGridRow*>(mEmojiGrid->getPanelList()[mFocusedIconRow]);
+ mFocusedIcon = row ? dynamic_cast<LLEmojiGridIcon*>(row->mList->getPanelList()[mFocusedIconCol]) : nullptr;
+
+ if (mFocusedIcon && !mHoveredIcon)
+ {
+ selectGridIcon(mFocusedIcon);
+ }
+}
+
+bool LLFloaterEmojiPicker::moveFocusedIconUp()
+{
+ for (S32 i = mFocusedIconRow - 1; i >= 0; --i)
+ {
+ LLScrollingPanel* panel = mEmojiGrid->getPanelList()[i];
+ LLEmojiGridRow* row = dynamic_cast<LLEmojiGridRow*>(panel);
+ if (row && row->mList->getPanelList().size() > mFocusedIconCol)
+ {
+ mEmojiScroll->scrollToShowRect(row->getBoundingRect());
+ mFocusedIconRow = i;
+ selectFocusedIcon();
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool LLFloaterEmojiPicker::moveFocusedIconDown()
+{
+ S32 rowCount = mEmojiGrid->getPanelList().size();
+ for (S32 i = mFocusedIconRow + 1; i < rowCount; ++i)
+ {
+ LLScrollingPanel* panel = mEmojiGrid->getPanelList()[i];
+ LLEmojiGridRow* row = dynamic_cast<LLEmojiGridRow*>(panel);
+ if (row && row->mList->getPanelList().size() > mFocusedIconCol)
+ {
+ mEmojiScroll->scrollToShowRect(row->getBoundingRect());
+ mFocusedIconRow = i;
+ selectFocusedIcon();
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool LLFloaterEmojiPicker::moveFocusedIconPrev()
+{
+ if (mHoveredIcon)
+ return false;
+
+ if (mFocusedIconCol > 0)
+ {
+ mFocusedIconCol--;
+ selectFocusedIcon();
+ return true;
+ }
+
+ for (S32 i = mFocusedIconRow - 1; i >= 0; --i)
+ {
+ LLScrollingPanel* panel = mEmojiGrid->getPanelList()[i];
+ LLEmojiGridRow* row = dynamic_cast<LLEmojiGridRow*>(panel);
+ if (row && row->mList->getPanelList().size())
+ {
+ mEmojiScroll->scrollToShowRect(row->getBoundingRect());
+ mFocusedIconCol = row->mList->getPanelList().size() - 1;
+ mFocusedIconRow = i;
+ selectFocusedIcon();
+ return true;
+ }
+ }
+
+ return false;
+}
+
+bool LLFloaterEmojiPicker::moveFocusedIconNext()
+{
+ if (mHoveredIcon)
+ return false;
+
+ LLScrollingPanel* panel = mEmojiGrid->getPanelList()[mFocusedIconRow];
+ LLEmojiGridRow* row = dynamic_cast<LLEmojiGridRow*>(panel);
+ S32 colCount = row ? row->mList->getPanelList().size() : 0;
+ if (mFocusedIconCol < colCount - 1)
+ {
+ mFocusedIconCol++;
+ selectFocusedIcon();
+ return true;
+ }
+
+ S32 rowCount = mEmojiGrid->getPanelList().size();
+ for (S32 i = mFocusedIconRow + 1; i < rowCount; ++i)
+ {
+ LLScrollingPanel* panel = mEmojiGrid->getPanelList()[i];
+ LLEmojiGridRow* row = dynamic_cast<LLEmojiGridRow*>(panel);
+ if (row && row->mList->getPanelList().size())
+ {
+ mEmojiScroll->scrollToShowRect(row->getBoundingRect());
+ mFocusedIconCol = 0;
+ mFocusedIconRow = i;
+ selectFocusedIcon();
+ return true;
+ }
+ }
+
+ return false;
+}
+
+void LLFloaterEmojiPicker::selectGridIcon(LLEmojiGridIcon* icon)
+{
+ icon->setBackgroundVisible(TRUE);
+ mPreview->setIcon(icon);
+}
+
+void LLFloaterEmojiPicker::unselectGridIcon(LLEmojiGridIcon* icon)
+{
+ icon->setBackgroundVisible(FALSE);
+ mPreview->setIcon(nullptr);
+}
+
+// virtual
+BOOL LLFloaterEmojiPicker::handleKey(KEY key, MASK mask, BOOL called_from_parent)
+{
+ if (mask == MASK_NONE)
+ {
+ switch (key)
+ {
+ case KEY_UP:
+ moveFocusedIconUp();
+ return TRUE;
+ case KEY_DOWN:
+ moveFocusedIconDown();
+ return TRUE;
+ case KEY_LEFT:
+ moveFocusedIconPrev();
+ return TRUE;
+ case KEY_RIGHT:
+ moveFocusedIconNext();
+ return TRUE;
+ case KEY_ESCAPE:
+ hideFloater();
+ return TRUE;
+ }
+ }
+
+ if (mask == MASK_ALT)
+ {
+ switch (key)
+ {
+ case KEY_LEFT:
+ selectEmojiGroup((mSelectedGroupIndex + mFilteredEmojis.size()) % mGroupButtons.size());
+ return TRUE;
+ case KEY_RIGHT:
+ selectEmojiGroup((mSelectedGroupIndex + 1) % mGroupButtons.size());
+ return TRUE;
+ }
+ }
+
+ if (key == KEY_RETURN)
+ {
+ U64 time = totalTime();
+ // <Shift+Return> comes twice for unknown reason
+ if (mFocusedIcon && (time - mRecentReturnPressedMs > 100000)) // Min interval 0.1 sec.
+ {
+ onEmojiMouseDown(mFocusedIcon);
+ onEmojiMouseUp(mFocusedIcon);
+ }
+ mRecentReturnPressedMs = time;
+ return TRUE;
+ }
+
+ if (mHint.empty())
+ {
+ if (key >= 0x20 && key < 0x80)
+ {
+ if (!mEmojiGrid->getPanelList().empty())
+ {
+ if (mFilterPattern.empty())
+ {
+ mFilterPattern = ":";
+ }
+ mFilterPattern += (char)key;
+ initialize();
+ }
+ return TRUE;
+ }
+ else if (key == KEY_BACKSPACE)
+ {
+ if (!mFilterPattern.empty())
+ {
+ mFilterPattern.pop_back();
+ if (mFilterPattern == ":")
+ {
+ mFilterPattern.clear();
+ }
+ initialize();
+ }
+ return TRUE;
+ }
+ }
+
+ return super::handleKey(key, mask, called_from_parent);
+}
+
+// virtual
+void LLFloaterEmojiPicker::goneFromFront()
+{
+ hideFloater();
+}
+
+void LLFloaterEmojiPicker::hideFloater() const
+{
+ LLEmojiHelper::instance().hideHelper(nullptr, true);
+}
+
+// static
+std::list<llwchar>& LLFloaterEmojiPicker::getRecentlyUsed()
+{
+ loadState();
+ return sRecentlyUsed;
+}
+
+// static
+void LLFloaterEmojiPicker::onEmojiUsed(llwchar emoji)
+{
+ // Update sRecentlyUsed
+ auto itr = std::find(sRecentlyUsed.begin(), sRecentlyUsed.end(), emoji);
+ if (itr == sRecentlyUsed.end())
+ {
+ sRecentlyUsed.push_front(emoji);
+ }
+ else if (itr != sRecentlyUsed.begin())
+ {
+ sRecentlyUsed.erase(itr);
+ sRecentlyUsed.push_front(emoji);
+ }
+
+ // Increment and reorder sFrequentlyUsed
+ auto itf = sFrequentlyUsed.begin();
+ while (itf != sFrequentlyUsed.end())
+ {
+ if (itf->first == emoji)
+ {
+ itf->second++;
+ while (itf != sFrequentlyUsed.begin())
+ {
+ auto prior = itf;
+ prior--;
+ if (prior->second > itf->second)
+ break;
+ prior->swap(*itf);
+ itf = prior;
+ }
+ break;
+ }
+ itf++;
+ }
+ // Append new if not found
+ if (itf == sFrequentlyUsed.end())
+ {
+ // Insert before others with count == 1
+ while (itf != sFrequentlyUsed.begin())
+ {
+ auto prior = itf;
+ prior--;
+ if (prior->second > 1)
+ break;
+ itf = prior;
+ }
+ sFrequentlyUsed.insert(itf, std::make_pair(emoji, 1));
+ }
+}
+
+// static
+void LLFloaterEmojiPicker::loadState()
+{
+ if (!sStateFileName.empty())
+ return; // Already loaded
+
+ sStateFileName = gDirUtilp->getExpandedFilename(LL_PATH_PER_SL_ACCOUNT, "emoji_floater_state.xml");
+
+ llifstream file;
+ file.open(sStateFileName.c_str());
+ if (!file.is_open())
+ {
+ LL_WARNS() << "Emoji floater state file is missing or inaccessible: " << sStateFileName << LL_ENDL;
+ return;
+ }
+
+ LLSD state;
+ LLSDSerialize::fromXML(state, file);
+ if (state.isUndefined())
+ {
+ LL_WARNS() << "Emoji floater state file is missing or ill-formed: " << sStateFileName << LL_ENDL;
+ return;
+ }
+
+ // Load and parse sRecentlyUsed
+ std::string recentlyUsed = state[sKeyRecentlyUsed];
+ std::vector<std::string> rtokens = LLStringUtil::getTokens(recentlyUsed, ",");
+ int maxCountR = 20;
+ for (const std::string& token : rtokens)
+ {
+ llwchar emoji = (llwchar)atoi(token.c_str());
+ if (std::find(sRecentlyUsed.begin(), sRecentlyUsed.end(), emoji) == sRecentlyUsed.end())
+ {
+ sRecentlyUsed.push_back(emoji);
+ if (!--maxCountR)
+ break;
+ }
+ }
+
+ // Load and parse sFrequentlyUsed
+ std::string frequentlyUsed = state[sKeyFrequentlyUsed];
+ std::vector<std::string> ftokens = LLStringUtil::getTokens(frequentlyUsed, ",");
+ int maxCountF = 20;
+ for (const std::string& token : ftokens)
+ {
+ std::vector<std::string> pair = LLStringUtil::getTokens(token, ":");
+ if (pair.size() == 2)
+ {
+ llwchar emoji = (llwchar)atoi(pair[0].c_str());
+ if (emoji)
+ {
+ U32 count = atoi(pair[1].c_str());
+ auto it = std::find_if(sFrequentlyUsed.begin(), sFrequentlyUsed.end(),
+ [emoji](std::pair<llwchar, U32>& it) { return it.first == emoji; });
+ if (it != sFrequentlyUsed.end())
+ {
+ it->second += count;
+ }
+ else
+ {
+ sFrequentlyUsed.push_back(std::make_pair(emoji, count));
+ if (!--maxCountF)
+ break;
+ }
+ }
+ }
+ }
+
+ // Normalize by minimum
+ if (!sFrequentlyUsed.empty())
+ {
+ U32 delta = sFrequentlyUsed.back().second - 1;
+ for (auto& it : sFrequentlyUsed)
+ {
+ it.second = std::max((U32)0, it.second - delta);
+ }
+ }
+}
+
+// static
+void LLFloaterEmojiPicker::saveState()
+{
+ if (sStateFileName.empty())
+ return; // Not loaded
+
+ if (LLAppViewer::instance()->isSecondInstance())
+ return; // Not allowed
+
+ LLSD state = LLSD::emptyMap();
+
+ if (!sRecentlyUsed.empty())
+ {
+ U32 maxCount = 20;
+ std::string recentlyUsed;
+ for (llwchar emoji : sRecentlyUsed)
+ {
+ if (!recentlyUsed.empty())
+ recentlyUsed += ",";
+ char buffer[32];
+ sprintf(buffer, "%u", (U32)emoji);
+ recentlyUsed += buffer;
+ if (!--maxCount)
+ break;
+ }
+ state[sKeyRecentlyUsed] = recentlyUsed;
+ }
+
+ if (!sFrequentlyUsed.empty())
+ {
+ U32 maxCount = 20;
+ std::string frequentlyUsed;
+ for (auto& it : sFrequentlyUsed)
+ {
+ if (!frequentlyUsed.empty())
+ frequentlyUsed += ",";
+ char buffer[32];
+ sprintf(buffer, "%u:%u", (U32)it.first, (U32)it.second);
+ frequentlyUsed += buffer;
+ if (!--maxCount)
+ break;
+ }
+ state[sKeyFrequentlyUsed] = frequentlyUsed;
+ }
+
+ llofstream stream(sStateFileName.c_str());
+ LLSDSerialize::toPrettyXML(state, stream);
+}