/** * @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 "lltrans.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 sRecentlyUsed; static std::list> 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 = (F32)(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 static_cast(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) , mChar(LLWString(1, emoji.Character)) { } virtual void draw() override { LLScrollingPanel::draw(); F32 x = (F32)(getRect().getWidth() / 2); F32 y = (F32)(getRect().getHeight() / 2); LLFontGL::getFontEmojiLarge()->render( mChar, // 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; } const LLWString& getChar() const { return mChar; } private: const LLEmojiSearchResult mData; const LLWString mChar; }; 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 = utf8str_to_wstring(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); static LLUIColor textColor = LLUIColorTable::instance().getColor("MenuItemEnabledColor", defaultColor); S32 max_pixels = clientWidth - iconWidth; drawName((F32)iconWidth, centerY, max_pixels, textColor.get()); } 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, const LLColor4& color) { F32 x0 = x; F32 x1 = (F32)max_pixels; LLFontGL* font = LLFontGL::getFontEmojiLarge(); if (mBegin) { LLWString text = mTitle.substr(0, mBegin); font->render( text.c_str(), // text 0, // begin_offset x0, // x y, // y color, // color LLFontGL::LEFT, // halign LLFontGL::VCENTER, // valign LLFontGL::NORMAL, // style LLFontGL::DROP_SHADOW_SOFT, // shadow static_cast(text.size()), // max_chars (S32)x1); // max_pixels F32 dx = font->getWidthF32(text.c_str()); x0 += dx; x1 -= dx; } if (x1 > 0 && mEnd > mBegin) { LLWString text = mTitle.substr(mBegin, mEnd - mBegin); font->render( 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 static_cast(text.size()), // max_chars (S32)x1); // max_pixels F32 dx = font->getWidthF32(text.c_str()); x0 += dx; x1 -= dx; } if (x1 > 0 && mEnd < mTitle.size()) { LLWString text = mEnd ? mTitle.substr(mEnd) : mTitle; font->render( 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 static_cast(text.size()), // max_chars (S32)x1); // max_pixels } } private: llwchar mEmoji; LLWString mWStr; LLWString 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("Groups"); mBadge = getChild("Badge"); mEmojiScroll = getChild("EmojiGridContainer"); mEmojiGrid = getChild("EmojiGrid"); mDummy = getChild("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::onClose(bool app_quitting) { if (!app_quitting) { LLEmojiHelper::instance().hideHelper(nullptr, true); } } 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 { std::size_t begin, end; LLStringUtil::format_map_t args; args["[FILTER]"] = mFilterPattern.substr(1); std::string title(getString("text_no_emoji_for_filter", args)); LLEmojiDictionary::searchInShortCode(begin, end, title, mFilterPattern); mPreview->setData(EMPTY_LIST_IMAGE_INDEX, title, begin, end); showPreview(true); } return; } mGroups->setVisible(true); mPreview->setIcon(nullptr); showPreview(true); mSelectedGroupIndex = groupIndex == ALL_EMOJIS_GROUP_INDEX ? 0 : static_cast((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); button->die(); } mFilteredEmojiGroups.clear(); mFilteredEmojis.clear(); mGroupButtons.clear(); LLButton::Params params; params.font = LLFontGL::getFontEmojiLarge(); LLRect rect; rect.mTop = mGroups->getRect().getHeight(); rect.mBottom = mBadge->getRect().getHeight(); // Create button for "All categories" params.name = "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> cats; fillCategoryRecentlyUsed(cats); fillCategoryFrequentlyUsed(cats); if (!cats.empty()) { mFilteredEmojiGroups.push_back(USED_EMOJIS_GROUP_INDEX); mFilteredEmojis.emplace_back(cats); params.name = "used_categories"; createGroupButton(params, rect, USED_EMOJIS_IMAGE_INDEX); } } const std::vector& groups = LLEmojiDictionary::instance().getGroups(); // List all categories in the dictionary for (U32 i = 0; i < groups.size(); ++i) { std::map> cats; fillGroupEmojis(cats, i); if (!cats.empty()) { mFilteredEmojiGroups.push_back(i); mFilteredEmojis.emplace_back(cats); params.name = "group_" + std::to_string(i); createGroupButton(params, rect, groups[i].Character); } } resizeGroupButtons(); } void LLFloaterEmojiPicker::fillCategoryRecentlyUsed(std::map>& cats) { if (sRecentlyUsed.empty()) return; std::vector 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()) { for (const std::string& shortcode : e2d->second->ShortCodes) { 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>& cats) { if (sFrequentlyUsed.empty()) return; std::vector 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()) { for (const std::string& shortcode : e2d->second->ShortCodes) { 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>& cats, U32 index) { const std::vector& 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 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()) { for (const std::string& shortcode : descr->ShortCodes) { 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(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 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& 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) { mDummy->setVisible(!show); mPreview->setVisible(show); } void LLFloaterEmojiPicker::onGroupButtonClick(LLUICtrl* ctrl) { if (LLButton* button = dynamic_cast(ctrl)) { if (button == mGroupButtons[mSelectedGroupIndex] || button->getToggleState()) return; auto it = std::find(mGroupButtons.begin(), mGroupButtons.end(), button); if (it == mGroupButtons.end()) return; selectEmojiGroup((U32)(it - mGroupButtons.begin())); } } void LLFloaterEmojiPicker::onGroupButtonMouseEnter(LLUICtrl* ctrl) { if (LLButton* button = dynamic_cast(ctrl)) { button->setUseFontColor(true); } } void LLFloaterEmojiPicker::onGroupButtonMouseLeave(LLUICtrl* ctrl) { if (LLButton* button = dynamic_cast(ctrl)) { button->setUseFontColor(button->getToggleState()); } } void LLFloaterEmojiPicker::onEmojiMouseEnter(LLUICtrl* ctrl) { if (LLEmojiGridIcon* icon = dynamic_cast(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(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(ctrl)) { LLSD value(wstring_to_utf8str(icon->getChar())); 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(mEmojiGrid->getPanelList()[mFocusedIconRow]); mFocusedIcon = row ? dynamic_cast(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(panel); if (row && row->mList->getPanelList().size() > mFocusedIconCol) { mEmojiScroll->scrollToShowRect(row->getBoundingRect()); mFocusedIconRow = i; selectFocusedIcon(); return true; } } return false; } bool LLFloaterEmojiPicker::moveFocusedIconDown() { auto rowCount = mEmojiGrid->getPanelList().size(); for (size_t i = mFocusedIconRow + 1; i < rowCount; ++i) { LLScrollingPanel* panel = mEmojiGrid->getPanelList()[i]; LLEmojiGridRow* row = dynamic_cast(panel); if (row && row->mList->getPanelList().size() > mFocusedIconCol) { mEmojiScroll->scrollToShowRect(row->getBoundingRect()); mFocusedIconRow = static_cast(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(panel); if (row && row->mList->getPanelList().size()) { mEmojiScroll->scrollToShowRect(row->getBoundingRect()); mFocusedIconCol = static_cast(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(panel); S32 colCount = row ? static_cast(row->mList->getPanelList().size()) : 0; if (mFocusedIconCol < colCount - 1) { mFocusedIconCol++; selectFocusedIcon(); return true; } auto rowCount = mEmojiGrid->getPanelList().size(); for (size_t i = mFocusedIconRow + 1; i < rowCount; ++i) { LLScrollingPanel* panel = mEmojiGrid->getPanelList()[i]; LLEmojiGridRow* row = dynamic_cast(panel); if (row && row->mList->getPanelList().size()) { mEmojiScroll->scrollToShowRect(row->getBoundingRect()); mFocusedIconCol = 0; mFocusedIconRow = static_cast(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(static_cast((mSelectedGroupIndex + mFilteredEmojis.size()) % mGroupButtons.size())); return true; case KEY_RIGHT: selectEmojiGroup(static_cast((mSelectedGroupIndex + 1) % mGroupButtons.size())); return true; } } if (key == KEY_RETURN) { U64 time = totalTime(); // 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& 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 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 ftokens = LLStringUtil::getTokens(frequentlyUsed, ","); int maxCountF = 20; for (const std::string& token : ftokens) { std::vector 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& 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); }