907 lines
27 KiB
C++
907 lines
27 KiB
C++
/**
|
|
* @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 "llfloaterreg.h"
|
|
#include "llkeyboard.h"
|
|
#include "lllineeditor.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<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 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, 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<LLFloaterEmojiPicker>("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);
|
|
|
|
mGroups = getChild<LLPanel>("Groups");
|
|
mBadge = getChild<LLPanel>("Badge");
|
|
|
|
mFilter = getChild<LLLineEditor>("Filter");
|
|
mFilter->setKeystrokeCallback([this](LLLineEditor*, void*) { onSearchKeystroke(); }, NULL);
|
|
mFilter->setFont(LLViewerChat::getChatFont());
|
|
mFilter->setText(sFilterPattern);
|
|
|
|
mEmojiScroll = getChild<LLScrollContainer>("EmojiGridContainer");
|
|
mEmojiScroll->setMouseEnterCallback([this](LLUICtrl*, const LLSD&) { onGridMouseEnter(); });
|
|
mEmojiScroll->setMouseLeaveCallback([this](LLUICtrl*, const LLSD&) { onGridMouseLeave(); });
|
|
|
|
mEmojiGrid = getChild<LLScrollingPanelList>("EmojiGrid");
|
|
|
|
fillGroups();
|
|
fillEmojis();
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
void LLFloaterEmojiPicker::dirtyRect()
|
|
{
|
|
super::dirtyRect();
|
|
|
|
if (!mFilter)
|
|
return;
|
|
|
|
const S32 PADDING = 4;
|
|
LLRect rect(PADDING, mFilter->getRect().mTop, getRect().getWidth() - PADDING * 2, PADDING);
|
|
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<LLEmojiGroup>& groups = LLEmojiDictionary::instance().getGroups();
|
|
for (const LLEmojiGroup& group : groups)
|
|
{
|
|
LLButton* button = LLUICtrlFactory::create<LLButton>(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);
|
|
|
|
LLUIString text;
|
|
text.insert(0, LLWString(1, group.Character));
|
|
button->setLabel(text);
|
|
|
|
if (mGroupButtons.size() == sSelectedGroupIndex)
|
|
{
|
|
button->setToggleState(TRUE);
|
|
button->setUseFontColor(TRUE);
|
|
}
|
|
|
|
mGroupButtons.push_back(button);
|
|
mGroups->addChild(button);
|
|
}
|
|
|
|
moveGroups();
|
|
}
|
|
|
|
void LLFloaterEmojiPicker::moveGroups()
|
|
{
|
|
const std::vector<LLEmojiGroup>& 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<S32> 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<const LLEmojiDescriptor*>& emojis, int maxRows = 0)
|
|
{
|
|
int rowCount = 0;
|
|
int iconIndex = 0;
|
|
bool showDivider = true;
|
|
bool mixedFolder = maxRows;
|
|
LLEmojiGridRow* row = nullptr;
|
|
if (!mixedFolder)
|
|
{
|
|
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<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)
|
|
{
|
|
// 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<LLButton*>(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::onSearchKeystroke()
|
|
{
|
|
sFilterPattern = mFilter->getText();
|
|
fillEmojis();
|
|
}
|
|
|
|
void LLFloaterEmojiPicker::onGridMouseEnter()
|
|
{
|
|
mFilter->setVisible(FALSE);
|
|
mPreview->setEmoji(nullptr);
|
|
mPreview->setVisible(TRUE);
|
|
}
|
|
|
|
void LLFloaterEmojiPicker::onGridMouseLeave()
|
|
{
|
|
mPreview->setVisible(FALSE);
|
|
mFilter->setVisible(TRUE);
|
|
mFilter->setFocus(TRUE);
|
|
}
|
|
|
|
void LLFloaterEmojiPicker::onGroupButtonMouseEnter(LLUICtrl* ctrl)
|
|
{
|
|
if (LLButton* button = dynamic_cast<LLButton*>(ctrl))
|
|
{
|
|
button->setUseFontColor(TRUE);
|
|
}
|
|
}
|
|
|
|
void LLFloaterEmojiPicker::onGroupButtonMouseLeave(LLUICtrl* ctrl)
|
|
{
|
|
if (LLButton* button = dynamic_cast<LLButton*>(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<LLEmojiGridIcon*>(ctrl))
|
|
{
|
|
onEmojiUsed(icon->getEmoji());
|
|
if (mEmojiPickCallback)
|
|
{
|
|
mEmojiPickCallback(icon->getEmoji());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void LLFloaterEmojiPicker::selectGridIcon(LLUICtrl* ctrl)
|
|
{
|
|
if (LLEmojiGridIcon* icon = dynamic_cast<LLEmojiGridIcon*>(ctrl))
|
|
{
|
|
icon->setBackgroundVisible(TRUE);
|
|
mPreview->setEmoji(icon->getDescr());
|
|
}
|
|
}
|
|
|
|
void LLFloaterEmojiPicker::unselectGridIcon(LLUICtrl* ctrl)
|
|
{
|
|
if (LLEmojiGridIcon* icon = dynamic_cast<LLEmojiGridIcon*>(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();
|
|
}
|
|
}
|
|
|
|
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();
|
|
|
|
sFilterPattern = state[sKeyFilterPattern].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 (!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);
|
|
}
|