summaryrefslogtreecommitdiff
path: root/indra
diff options
context:
space:
mode:
Diffstat (limited to 'indra')
-rw-r--r--indra/llcommon/llstring.h10
-rw-r--r--indra/newview/llfloateremojipicker.cpp245
-rw-r--r--indra/newview/llfloateremojipicker.h7
-rw-r--r--indra/newview/skins/default/xui/en/floater_emoji_picker.xml2
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"