/** * @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 "llfiltereditor.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 state related variables static U32 sSelectedGroupIndex = 0; static std::string sFilterPattern; static std::list sRecentlyUsed; static std::list> sFrequentlyUsed; // State file related values static std::string sStateFileName; static const std::string sKeySelectedGroupIndex("SelectedGroupIndex"); static const std::string sKeyFilterPattern("FilterPattern"); 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 S32_MAX, // max_pixels nullptr, // right_x false, // use_ellipses true); // use_color } virtual void updatePanel(BOOL allow_modify) override {} private: const LLWString mText; }; class LLEmojiGridIcon : public LLScrollingPanel { public: LLEmojiGridIcon( const LLPanel::Params& panel_params , const LLEmojiDescriptor* descr , std::string category) : LLScrollingPanel(panel_params) , mDescr(descr) , mText(LLWString(1, descr->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 S32_MAX, // max_pixels nullptr, // right_x false, // use_ellipses true); // use_color } virtual void updatePanel(BOOL allow_modify) override {} const LLEmojiDescriptor* getDescr() const { return mDescr; } llwchar getEmoji() const { return mDescr->Character; } LLWString getText() const { return mText; } private: const LLEmojiDescriptor* mDescr; const LLWString mText; }; class LLEmojiPreviewPanel : public LLPanel { public: LLEmojiPreviewPanel() : LLPanel() { } void setEmoji(const LLEmojiDescriptor* descr) { mDescr = descr; if (!mDescr) return; mEmojiText = LLWString(1, descr->Character); } virtual void draw() override { LLPanel::draw(); if (!mDescr) return; 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; size_t count = mDescr->ShortCodes.size(); if (count == 1) { drawName(mDescr->ShortCodes.front(), iconWidth, centerY, max_pixels, textColor); } else if (count > 1) { F32 quarterY = 0.5f * centerY; drawName(mDescr->ShortCodes.front(), iconWidth, centerY + quarterY, max_pixels, textColor); drawName(*++mDescr->ShortCodes.begin(), iconWidth, quarterY, max_pixels, textColor); } } protected: void drawIcon(F32 x, F32 y, S32 max_pixels) { LLFontGL::getFontEmojiHuge()->render( mEmojiText, // 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 nullptr, // right_x false, // use_ellipses true); // use_color } void drawName(std::string name, F32 x, F32 y, S32 max_pixels, LLColor4& color) { LLFontGL::getFontEmoji()->renderUTF8( name, // wstr 0, // begin_offset x, // x y, // y color, // color LLFontGL::LEFT, // halign LLFontGL::VCENTER, // valign LLFontGL::NORMAL, // style LLFontGL::DROP_SHADOW_SOFT, // shadow -1, // max_chars max_pixels, // max_pixels nullptr, // right_x true, // use_ellipses false); // use_color } private: const LLEmojiDescriptor* mDescr { nullptr }; LLWString mEmojiText; }; LLFloaterEmojiPicker* LLFloaterEmojiPicker::getInstance() { LLFloaterEmojiPicker* floater = LLFloaterReg::getTypedInstance("emoji_picker"); if (!floater) LL_ERRS() << "Cannot instantiate emoji picker" << LL_ENDL; return floater; } LLFloaterEmojiPicker* LLFloaterEmojiPicker::showInstance(pick_callback_t pick_callback, close_callback_t close_callback) { LLFloaterEmojiPicker* floater = getInstance(); floater->show(pick_callback, close_callback); return floater; } void LLFloaterEmojiPicker::show(pick_callback_t pick_callback, close_callback_t close_callback) { mEmojiPickCallback = pick_callback; mFloaterCloseCallback = close_callback; openFloater(mKey); setFocus(TRUE); } LLFloaterEmojiPicker::LLFloaterEmojiPicker(const LLSD& key) : LLFloater(key) { loadState(); } BOOL LLFloaterEmojiPicker::postBuild() { // Should be initialized first mPreview = new LLEmojiPreviewPanel(); mPreview->setVisible(FALSE); addChild(mPreview); mDummy = getChild("Dummy"); mGroups = getChild("Groups"); mBadge = getChild("Badge"); mFilter = getChild("Filter"); mFilter->setKeystrokeCallback([this](LLUICtrl*, const LLSD&) { onFilterChanged(); }); mFilter->setTextChangedCallback([this](LLUICtrl*, const LLSD&) { onFilterChanged(); }); mFilter->setText(sFilterPattern); mEmojiScroll = getChild("EmojiGridContainer"); mEmojiScroll->setMouseEnterCallback([this](LLUICtrl*, const LLSD&) { onGridMouseEnter(); }); mEmojiScroll->setMouseLeaveCallback([this](LLUICtrl*, const LLSD&) { onGridMouseLeave(); }); mEmojiGrid = getChild("EmojiGrid"); fillGroups(); fillEmojis(); return TRUE; } void LLFloaterEmojiPicker::dirtyRect() { super::dirtyRect(); if (!mFilter) 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 && mEmojiScroll->getRect().getWidth() != mRecentGridWidth) { moveGroups(); fillEmojis(true); } } LLFloaterEmojiPicker::~LLFloaterEmojiPicker() { gFocusMgr.releaseFocusIfNeeded( this ); } void LLFloaterEmojiPicker::fillGroups() { LLButton::Params params; params.font = LLFontGL::getFontEmoji(); LLRect rect; rect.mTop = mGroups->getRect().getHeight(); rect.mBottom = mBadge->getRect().getHeight(); const std::vector& groups = LLEmojiDictionary::instance().getGroups(); for (const LLEmojiGroup& group : groups) { 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->setLabel(LLUIString(LLWString(1, group.Character))); if (mGroupButtons.size() == sSelectedGroupIndex) { button->setToggleState(TRUE); button->setUseFontColor(TRUE); } mGroupButtons.push_back(button); mGroups->addChild(button); } moveGroups(); } void LLFloaterEmojiPicker::moveGroups() { const std::vector& groups = LLEmojiDictionary::instance().getGroups(); if (groups.empty()) return; int badgeWidth = mGroups->getRect().getWidth() / groups.size(); if (badgeWidth == mRecentBadgeWidth) return; mRecentBadgeWidth = badgeWidth; for (int i = 0; i < mGroupButtons.size(); ++i) { LLRect rect = mGroupButtons[i]->getRect(); rect.mLeft = badgeWidth * i; rect.mRight = rect.mLeft + badgeWidth; mGroupButtons[i]->setRect(rect); } LLRect rect = mBadge->getRect(); rect.mLeft = badgeWidth * sSelectedGroupIndex; rect.mRight = rect.mLeft + badgeWidth; mBadge->setRect(rect); } void LLFloaterEmojiPicker::fillEmojis(bool fromResize) { mRecentGridWidth = mEmojiScroll->getRect().getWidth(); S32 scrollbarSize = mEmojiScroll->getSize(); if (scrollbarSize < 0) { static LLUICachedControl scrollbar_size_control("UIScrollbarSize", 0); scrollbarSize = scrollbar_size_control; } const S32 clientWidth = mRecentGridWidth - scrollbarSize - mEmojiScroll->getBorderWidth() * 2; const S32 gridPadding = mEmojiGrid->getPadding(); const S32 iconSpacing = mEmojiGrid->getSpacing(); const S32 rowWidth = clientWidth - gridPadding * 2; const S32 iconSize = 28; // icon width and height const S32 maxIcons = llmax(1, (rowWidth + iconSpacing) / (iconSize + iconSpacing)); // Optimization: don't rearrange for different widths with the same maxIcons if (fromResize && (maxIcons == mRecentMaxIcons)) return; mRecentMaxIcons = maxIcons; mHoveredIcon = nullptr; mEmojiGrid->clearPanels(); mPreview->setEmoji(nullptr); if (mEmojiGrid->getRect().getWidth() != clientWidth) { LLRect rect = mEmojiGrid->getRect(); rect.mRight = rect.mLeft + clientWidth; mEmojiGrid->setRect(rect); } LLPanel::Params row_panel_params; row_panel_params.rect = LLRect(0, iconSize, rowWidth, 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 = iconSpacing; LLPanel::Params icon_params; LLRect icon_rect(0, iconSize, iconSize, 0); static LLColor4 defaultColor(0.75f, 0.75f, 0.75f, 1.0f); LLColor4 bgColor = LLUIColorTable::instance().getColor("MenuItemHighlightBgColor", defaultColor); auto matchesPattern = [](const LLEmojiDescriptor* descr) -> bool { for (const std::string& shortCode : descr->ShortCodes) if (shortCode.find(sFilterPattern) != std::string::npos) return true; return false; }; auto listCategory = [&](std::string category, const std::vector& emojis, int maxRows = 0) { int rowCount = 0; int iconIndex = 0; bool showDivider = true; bool mixedFolder = maxRows; LLEmojiGridRow* row = nullptr; if (!mixedFolder && !isupper(category.front())) { LLStringUtil::capitalize(category); } for (const LLEmojiDescriptor* descr : emojis) { if (sFilterPattern.empty() || matchesPattern(descr)) { // Place a category title if needed if (showDivider) { LLEmojiGridDivider* div = new LLEmojiGridDivider(row_panel_params, category); mEmojiGrid->addPanel(div, true); showDivider = false; } // Place a new row each (maxIcons) icons if (!(iconIndex % maxIcons)) { if (maxRows && ++rowCount > maxRows) break; row = new LLEmojiGridRow(row_panel_params, row_list_params); mEmojiGrid->addPanel(row, true); } // Place a new icon to the current row LLEmojiGridIcon* icon = new LLEmojiGridIcon(icon_params, descr, mixedFolder ? LLStringUtil::capitalize(descr->Category) : category); 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(bgColor); icon->setBackgroundOpaque(1); icon->setRect(icon_rect); row->mList->addPanel(icon, true); iconIndex++; } } }; const std::vector& groups = LLEmojiDictionary::instance().getGroups(); const LLEmojiDictionary::emoji2descr_map_t& emoji2descr = LLEmojiDictionary::instance().getEmoji2Descr(); const LLEmojiDictionary::cat2descrs_map_t& category2Descr = LLEmojiDictionary::instance().getCategory2Descrs(); if (!sSelectedGroupIndex) { std::vector recentlyUsed; for (llwchar emoji : sRecentlyUsed) { auto it = emoji2descr.find(emoji); if (it != emoji2descr.end()) { recentlyUsed.push_back(it->second); } } listCategory(getString("title_for_recently_used"), recentlyUsed, 1); std::vector frequentlyUsed; for (auto& emoji : sFrequentlyUsed) { auto it = emoji2descr.find(emoji.first); if (it != emoji2descr.end()) { frequentlyUsed.push_back(it->second); } } listCategory(getString("title_for_frequently_used"), frequentlyUsed, 1); // List all groups for (const LLEmojiGroup& group : groups) { // List all categories in group for (const std::string& category : group.Categories) { // List all emojis in category const LLEmojiDictionary::cat2descrs_map_t::const_iterator& item = category2Descr.find(category); if (item != category2Descr.end()) { listCategory(category, item->second); } } } } else { // List all categories in the selected group for (const std::string& category : groups[sSelectedGroupIndex].Categories) { // List all emojis in category const LLEmojiDictionary::cat2descrs_map_t::const_iterator& item = category2Descr.find(category); if (item != category2Descr.end()) { listCategory(category, item->second); } } } } void LLFloaterEmojiPicker::onGroupButtonClick(LLUICtrl* ctrl) { if (LLButton* button = dynamic_cast(ctrl)) { if (button == mGroupButtons[sSelectedGroupIndex] || button->getToggleState()) return; auto it = std::find(mGroupButtons.begin(), mGroupButtons.end(), button); if (it == mGroupButtons.end()) return; mGroupButtons[sSelectedGroupIndex]->setUseFontColor(FALSE); mGroupButtons[sSelectedGroupIndex]->setToggleState(FALSE); sSelectedGroupIndex = it - mGroupButtons.begin(); mGroupButtons[sSelectedGroupIndex]->setToggleState(TRUE); mGroupButtons[sSelectedGroupIndex]->setUseFontColor(TRUE); LLRect rect = mBadge->getRect(); rect.mLeft = button->getRect().mLeft; rect.mRight = button->getRect().mRight; mBadge->setRect(rect); mFilter->setFocus(TRUE); fillEmojis(); } } void LLFloaterEmojiPicker::onFilterChanged() { sFilterPattern = mFilter->getText(); fillEmojis(); } void LLFloaterEmojiPicker::onGridMouseEnter() { mDummy->setVisible(FALSE); mPreview->setEmoji(nullptr); mPreview->setVisible(TRUE); } void LLFloaterEmojiPicker::onGridMouseLeave() { mPreview->setVisible(FALSE); mDummy->setVisible(TRUE); } 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 (ctrl) { if (mHoveredIcon && mHoveredIcon != ctrl) { unselectGridIcon(mHoveredIcon); } selectGridIcon(ctrl); mHoveredIcon = ctrl; } } void LLFloaterEmojiPicker::onEmojiMouseLeave(LLUICtrl* ctrl) { if (ctrl) { if (ctrl == mHoveredIcon) { unselectGridIcon(ctrl); } } } 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 (mEmojiPickCallback) { if (LLEmojiGridIcon* icon = dynamic_cast(ctrl)) { if (mEmojiPickCallback) { mEmojiPickCallback(icon->getEmoji()); } } } } void LLFloaterEmojiPicker::selectGridIcon(LLUICtrl* ctrl) { if (LLEmojiGridIcon* icon = dynamic_cast(ctrl)) { icon->setBackgroundVisible(TRUE); mPreview->setEmoji(icon->getDescr()); } } void LLFloaterEmojiPicker::unselectGridIcon(LLUICtrl* ctrl) { if (LLEmojiGridIcon* icon = dynamic_cast(ctrl)) { icon->setBackgroundVisible(FALSE); mPreview->setEmoji(nullptr); } } // virtual BOOL LLFloaterEmojiPicker::handleKeyHere(KEY key, MASK mask) { if (mask == MASK_NONE) { switch (key) { case KEY_ESCAPE: closeFloater(); return TRUE; } } return LLFloater::handleKeyHere(key, mask); } // virtual void LLFloaterEmojiPicker::closeFloater(bool app_quitting) { saveState(); LLFloater::closeFloater(app_quitting); if (mFloaterCloseCallback) { mFloaterCloseCallback(); } } // 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()) sFrequentlyUsed.push_back(std::make_pair(emoji, 1)); } // static void LLFloaterEmojiPicker::onRecentlyUsedChanged() { if (sSelectedGroupIndex) return; if (LLFloaterEmojiPicker* picker = getInstance()) { picker->fillEmojis(); } } // static void LLFloaterEmojiPicker::loadState() { if (!sStateFileName.empty()) return; // Already loaded sStateFileName = gDirUtilp->getExpandedFilename(LL_PATH_USER_SETTINGS, "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; } sSelectedGroupIndex = state[sKeySelectedGroupIndex].asInteger(); sFilterPattern = state[sKeyFilterPattern].asString(); // 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; 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 (sSelectedGroupIndex) { state[sKeySelectedGroupIndex] = (int)sSelectedGroupIndex; } if (!sFilterPattern.empty()) { state[sKeyFilterPattern] = sFilterPattern; } 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); }