/** * @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 "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 "lltextbox.h" #include "llviewerchat.h" size_t LLFloaterEmojiPicker::sSelectedGroupIndex; std::string LLFloaterEmojiPicker::sSearchPattern; class LLEmojiScrollListItem : public LLScrollListItem { public: LLEmojiScrollListItem(const llwchar emoji, const LLScrollListItem::Params& params) : LLScrollListItem(params) , mEmoji(emoji) { } llwchar getEmoji() const { return mEmoji; } virtual void draw(const LLRect& rect, const LLColor4& fg_color, const LLColor4& hover_color, // highlight/hover selection of whole item or cell const LLColor4& select_color, // highlight/hover selection of whole item or cell const LLColor4& highlight_color, // highlights contents of cells (ex: text) S32 column_padding) override { LLScrollListItem::draw(rect, fg_color, hover_color, select_color, highlight_color, column_padding); LLWString wstr(1, mEmoji); S32 width = getColumn(0)->getWidth(); F32 x = rect.mLeft + width / 2; F32 y = rect.getCenterY(); LLFontGL::getFontEmoji()->render( wstr, // 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 } private: llwchar mEmoji; }; 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::getFontSansSerifBold()->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) , mEmoji(descr->Character) , mText(LLWString(1, mEmoji)) , mDescr(descr->getShortCodes()) , mCategory(category) { } 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 {} llwchar getEmoji() const { return mEmoji; } LLWString getText() const { return mText; } std::string getDescr() const { return mDescr; } std::string getCategory() const { return mCategory; } private: const llwchar mEmoji; const LLWString mText; const std::string mDescr; const std::string mCategory; }; 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) { } BOOL LLFloaterEmojiPicker::postBuild() { // Should be initialized first mPreviewEmoji = getChild("PreviewEmoji"); mPreviewEmoji->setClickedCallback([this](LLUICtrl*, const LLSD&) { onPreviewEmojiClick(); }); mDescription = getChild("Description"); mDescription->setVisible(FALSE); mGroups = getChild("Groups"); mBadge = getChild("Badge"); mSearch = getChild("Search"); mSearch->setKeystrokeCallback([this](LLLineEditor*, void*) { onSearchKeystroke(); }, NULL); mSearch->setFont(LLViewerChat::getChatFont()); mSearch->setText(sSearchPattern); 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 (mEmojiScroll && mEmojiScroll->getRect().getWidth() != mRecentGridWidth) { moveGroups(); fillEmojis(true); } } LLFloaterEmojiPicker::~LLFloaterEmojiPicker() { gFocusMgr.releaseFocusIfNeeded( this ); } void LLFloaterEmojiPicker::fillGroups() { LLButton::Params params; params.font = LLFontGL::getFontEmoji(); //params.use_font_color = true; 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); 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& 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(); mPreviewEmoji->setLabel(LLUIString()); 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 const LLColor4 bgcolors[] = { LLColor4(0.8f, 0.6f, 0.8f, 1.0f), LLColor4(0.8f, 0.8f, 0.4f, 1.0f), LLColor4(0.6f, 0.6f, 0.8f, 1.0f), LLColor4(0.4f, 0.7f, 0.4f, 1.0f), LLColor4(0.5f, 0.7f, 0.9f, 1.0f), LLColor4(0.7f, 0.8f, 0.2f, 1.0f) }; static constexpr U32 bgcolorCount = sizeof(bgcolors) / sizeof(*bgcolors); auto listCategory = [&](std::string category, const std::vector& emojis) { int iconIndex = 0; bool showDivider = true; LLEmojiGridRow* row = nullptr; LLStringUtil::capitalize(category); for (const LLEmojiDescriptor* descr : emojis) { if (sSearchPattern.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)) { 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); 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); }); icon->setBackgroundColor(bgcolors[iconIndex % bgcolorCount]); icon->setBackgroundOpaque(1); icon->setRect(icon_rect); row->mList->addPanel(icon, true); iconIndex++; } } }; const std::vector& groups = LLEmojiDictionary::instance().getGroups(); const LLEmojiDictionary::cat2descrs_map_t& category2Descr = LLEmojiDictionary::instance().getCategory2Descrs(); if (!sSelectedGroupIndex) { // 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); } } } } bool LLFloaterEmojiPicker::matchesPattern(const LLEmojiDescriptor* descr) { for (const std::string& shortCode : descr->ShortCodes) if (shortCode.find(sSearchPattern) != std::string::npos) return true; return false; } 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); mSearch->setFocus(TRUE); fillEmojis(); } } void LLFloaterEmojiPicker::onSearchKeystroke() { sSearchPattern = mSearch->getText(); fillEmojis(); } void LLFloaterEmojiPicker::onPreviewEmojiClick() { if (mEmojiPickCallback) { if (LLEmojiGridIcon* icon = dynamic_cast(mHoveredIcon)) { mEmojiPickCallback(icon->getEmoji()); } } } void LLFloaterEmojiPicker::onGridMouseEnter() { mSearch->setVisible(FALSE); mDescription->setText(LLStringExplicit(""), LLStyle::Params()); mDescription->setVisible(TRUE); } void LLFloaterEmojiPicker::onGridMouseLeave() { mDescription->setVisible(FALSE); mDescription->setText(LLStringExplicit(""), LLStyle::Params()); mSearch->setVisible(TRUE); mSearch->setFocus(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::onEmojiMouseClick(LLUICtrl* ctrl, MASK mask) { if (mEmojiPickCallback) { if (LLEmojiGridIcon* icon = dynamic_cast(ctrl)) { mPreviewEmoji->handleAnyMouseClick(0, 0, 0, EMouseClickType::CLICK_LEFT, TRUE); mPreviewEmoji->handleAnyMouseClick(0, 0, 0, EMouseClickType::CLICK_LEFT, FALSE); if (!(mask & 4)) { closeFloater(); } } } } void LLFloaterEmojiPicker::selectGridIcon(LLUICtrl* ctrl) { if (LLEmojiGridIcon* icon = dynamic_cast(ctrl)) { icon->setBackgroundVisible(TRUE); LLUIString text; text.insert(0, icon->getText()); mPreviewEmoji->setLabel(text); std::string descr = icon->getDescr() + "\n" + icon->getCategory(); mDescription->setText(LLStringExplicit(descr), LLStyle::Params()); } } void LLFloaterEmojiPicker::unselectGridIcon(LLUICtrl* ctrl) { if (LLEmojiGridIcon* icon = dynamic_cast(ctrl)) { icon->setBackgroundVisible(FALSE); mPreviewEmoji->setLabel(LLUIString()); mDescription->setText(LLStringExplicit(""), LLStyle::Params()); } } // 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) { LLFloater::closeFloater(app_quitting); if (mFloaterCloseCallback) { mFloaterCloseCallback(); } }