diff options
| -rw-r--r-- | indra/llcommon/llstring.h | 10 | ||||
| -rw-r--r-- | indra/newview/llfloateremojipicker.cpp | 245 | ||||
| -rw-r--r-- | indra/newview/llfloateremojipicker.h | 7 | ||||
| -rw-r--r-- | indra/newview/skins/default/xui/en/floater_emoji_picker.xml | 2 | 
4 files changed, 254 insertions, 10 deletions
| diff --git a/indra/llcommon/llstring.h b/indra/llcommon/llstring.h index bdb90335e1..62403969e4 100644 --- a/indra/llcommon/llstring.h +++ b/indra/llcommon/llstring.h @@ -357,6 +357,7 @@ public:  	static void	replaceNonstandardASCII( string_type& string, T replacement );  	static void	replaceChar( string_type& string, T target, T replacement );  	static void replaceString( string_type& string, string_type target, string_type replacement ); +	static string_type capitalize(const string_type& str);  	static void capitalize(string_type& str);  	static BOOL	containsNonprintable(const string_type& string); @@ -1598,6 +1599,15 @@ void LLStringUtilBase<T>::replaceTabsWithSpaces( string_type& str, size_type spa  //static  template<class T> +std::basic_string<T> LLStringUtilBase<T>::capitalize(const string_type& str) +{ +	string_type result(str); +	capitalize(result); +	return result; +} + +//static +template<class T>  void LLStringUtilBase<T>::capitalize(string_type& str)  {  	if (str.size()) diff --git a/indra/newview/llfloateremojipicker.cpp b/indra/newview/llfloateremojipicker.cpp index 02982bf63a..08929b05f5 100644 --- a/indra/newview/llfloateremojipicker.cpp +++ b/indra/newview/llfloateremojipicker.cpp @@ -27,6 +27,7 @@  #include "llfloateremojipicker.h" +#include "llappviewer.h"  #include "llbutton.h"  #include "llcombobox.h"  #include "llemojidictionary.h" @@ -37,11 +38,27 @@  #include "llscrollingpanellist.h"  #include "llscrolllistctrl.h"  #include "llscrolllistitem.h" +#include "llsdserialize.h"  #include "lltextbox.h"   #include "llviewerchat.h"  -size_t LLFloaterEmojiPicker::sSelectedGroupIndex; -std::string LLFloaterEmojiPicker::sSearchPattern; +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 sSearchPattern; +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 sKeySelectedGroupIndex("SelectedGroupIndex"); +static const std::string sKeySearchPattern("SearchPattern"); +static const std::string sKeyRecentlyUsed("RecentlyUsed"); +static const std::string sKeyFrequentlyUsed("FrequentlyUsed"); +}  class LLEmojiScrollListItem : public LLScrollListItem  { @@ -146,7 +163,10 @@ private:  class LLEmojiGridIcon : public LLScrollingPanel  {  public: -    LLEmojiGridIcon(const LLPanel::Params& panel_params, const LLEmojiDescriptor* descr, std::string category) +    LLEmojiGridIcon( +        const LLPanel::Params& panel_params +        , const LLEmojiDescriptor* descr +        , std::string category)          : LLScrollingPanel(panel_params)          , mEmoji(descr->Character)          , mText(LLWString(1, mEmoji)) @@ -218,6 +238,7 @@ void LLFloaterEmojiPicker::show(pick_callback_t pick_callback, close_callback_t  LLFloaterEmojiPicker::LLFloaterEmojiPicker(const LLSD& key)  : LLFloater(key)  { +    loadState();  }  BOOL LLFloaterEmojiPicker::postBuild() @@ -269,7 +290,6 @@ void LLFloaterEmojiPicker::fillGroups()  {      LLButton::Params params;      params.font = LLFontGL::getFontEmoji(); -    //params.use_font_color = true;      LLRect rect;      rect.mTop = mGroups->getRect().getHeight(); @@ -387,12 +407,17 @@ void LLFloaterEmojiPicker::fillEmojis(bool fromResize)      static constexpr U32 bgcolorCount = sizeof(bgcolors) / sizeof(*bgcolors); -    auto listCategory = [&](std::string category, const std::vector<const LLEmojiDescriptor*>& emojis) +    auto listCategory = [&](std::string category, const std::vector<const LLEmojiDescriptor*>& emojis, int maxRows = 0)      { +        int rowCount = 0;          int iconIndex = 0;          bool showDivider = true; +        bool mixedFolder = maxRows;          LLEmojiGridRow* row = nullptr; -        LLStringUtil::capitalize(category); +        if (!mixedFolder) +        { +            LLStringUtil::capitalize(category); +        }          for (const LLEmojiDescriptor* descr : emojis)          {              if (sSearchPattern.empty() || matchesPattern(descr)) @@ -408,12 +433,14 @@ void LLFloaterEmojiPicker::fillEmojis(bool fromResize)                  // 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, category); +                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->setMouseUpCallback([this](LLUICtrl* ctrl, S32, S32, MASK mask) { onEmojiMouseClick(ctrl, mask); }); @@ -428,9 +455,32 @@ void LLFloaterEmojiPicker::fillEmojis(bool fromResize)      };      const std::vector<LLEmojiGroup>& 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<const LLEmojiDescriptor*> 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<const LLEmojiDescriptor*> 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)          { @@ -577,6 +627,7 @@ void LLFloaterEmojiPicker::onEmojiMouseClick(LLUICtrl* ctrl, MASK mask)      {          if (LLEmojiGridIcon* icon = dynamic_cast<LLEmojiGridIcon*>(ctrl))          { +            onEmojiUsed(icon->getEmoji());              mPreviewEmoji->handleAnyMouseClick(0, 0, 0, EMouseClickType::CLICK_LEFT, TRUE);              mPreviewEmoji->handleAnyMouseClick(0, 0, 0, EMouseClickType::CLICK_LEFT, FALSE);              if (!(mask & 4)) @@ -631,9 +682,189 @@ BOOL LLFloaterEmojiPicker::handleKeyHere(KEY key, MASK mask)  // virtual  void LLFloaterEmojiPicker::closeFloater(bool app_quitting)  { +    saveState();      LLFloater::closeFloater(app_quitting);      if (mFloaterCloseCallback)      {          mFloaterCloseCallback();      }  } + +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)); +} + +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(); + +    sSearchPattern = state[sKeySearchPattern].asString(); + +    // 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; +        for (auto& it : sFrequentlyUsed) +        { +            it.second = std::max((U32)0, it.second - delta); +        } +    } +} + +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 (!sSearchPattern.empty()) +    { +        state[sKeySearchPattern] = sSearchPattern; +    } + +    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); +} diff --git a/indra/newview/llfloateremojipicker.h b/indra/newview/llfloateremojipicker.h index 7fa6ea46b0..d00391c61f 100644 --- a/indra/newview/llfloateremojipicker.h +++ b/indra/newview/llfloateremojipicker.h @@ -77,6 +77,10 @@ private:      virtual BOOL handleKeyHere(KEY key, MASK mask) override; +    void onEmojiUsed(llwchar emoji); +    void loadState(); +    void saveState(); +      class LLPanel* mGroups { nullptr };      class LLPanel* mBadge { nullptr };      class LLLineEditor* mSearch { nullptr }; @@ -94,9 +98,6 @@ private:      S32 mRecentGridWidth { 0 };      S32 mRecentMaxIcons { 0 };      LLUICtrl* mHoveredIcon { nullptr }; - -    static size_t sSelectedGroupIndex; -    static std::string sSearchPattern;  };  #endif diff --git a/indra/newview/skins/default/xui/en/floater_emoji_picker.xml b/indra/newview/skins/default/xui/en/floater_emoji_picker.xml index 2a534b23b9..4b4bf0c5a3 100644 --- a/indra/newview/skins/default/xui/en/floater_emoji_picker.xml +++ b/indra/newview/skins/default/xui/en/floater_emoji_picker.xml @@ -10,6 +10,8 @@      min_width="250"      height="400"      width="250"> +  <floater.string name="title_for_recently_used" value="Recently used"/> +  <floater.string name="title_for_frequently_used" value="Frequently used"/>    <line_editor        name="Search"        label="Type to search" | 
