SL-19951 Collect used icons in a special group 'Recently used'

meow-7.2.2
Alexander Gavriliuk 2023-07-07 23:35:01 +02:00 committed by Guru
parent e7f2fbf2a4
commit 16f0329d18
4 changed files with 254 additions and 10 deletions

View File

@ -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);
@ -1596,6 +1597,15 @@ void LLStringUtilBase<T>::replaceTabsWithSpaces( string_type& str, size_type spa
str = out_str;
}
//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)

View File

@ -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);
}

View File

@ -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

View File

@ -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"