diff --git a/config_32_p.cmd b/config_32_p.cmd new file mode 100644 index 0000000000..2e4ecaa630 --- /dev/null +++ b/config_32_p.cmd @@ -0,0 +1,6 @@ +@ECHO OFF +REM call c:\python27\autobuild11\scripts\activate.bat +autobuild --version +autobuild configure -c ReleaseFS -v -- --chan private +REM autobuild build --no-configure -c ReleaseFS +pause \ No newline at end of file diff --git a/indra/llcommon/llstring.h b/indra/llcommon/llstring.h index 23cb48e531..345fc5106b 100644 --- a/indra/llcommon/llstring.h +++ b/indra/llcommon/llstring.h @@ -366,6 +366,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); @@ -1610,6 +1611,15 @@ void LLStringUtilBase::replaceTabsWithSpaces( string_type& str, size_type spa str = out_str; } +//static +template +std::basic_string LLStringUtilBase::capitalize(const string_type& str) +{ + string_type result(str); + capitalize(result); + return result; +} + //static template void LLStringUtilBase::capitalize(string_type& str) diff --git a/indra/llrender/llfontfreetype.cpp b/indra/llrender/llfontfreetype.cpp index 47444d82df..b529ae2aba 100644 --- a/indra/llrender/llfontfreetype.cpp +++ b/indra/llrender/llfontfreetype.cpp @@ -558,6 +558,8 @@ LLFontGlyphInfo* LLFontFreetype::addGlyph(llwchar wch, EFontGlyphType glyph_type // Fallback fonts with a functor have precedence over everything else fallback_font_vector_t::const_iterator it_fallback = mFallbackFonts.cbegin(); + /* This leads to a bug SL-19831 "Check marks in the menu are less visible." + ** Also, LLFontRegistry::createFont() says: "Fallback fonts don't render" for (; it_fallback != mFallbackFonts.cend() && it_fallback->second; ++it_fallback) { if (it_fallback->second(wch)) @@ -569,6 +571,7 @@ LLFontGlyphInfo* LLFontFreetype::addGlyph(llwchar wch, EFontGlyphType glyph_type } } } + */ // Initialize char to glyph map glyph_index = FT_Get_Char_Index(mFTFace, wch); diff --git a/indra/llui/llemojidictionary.cpp b/indra/llui/llemojidictionary.cpp index 179c5d25bf..cdcf5a93d6 100644 --- a/indra/llui/llemojidictionary.cpp +++ b/indra/llui/llemojidictionary.cpp @@ -38,7 +38,12 @@ // Constants // -constexpr char SKINNED_EMOJI_FILENAME[] = "emoji_characters.xml"; +static const std::string SKINNED_EMOJI_FILENAME("emoji_characters.xml"); +static const std::string SKINNED_CATEGORY_FILENAME("emoji_categories.xml"); +static const std::string COMMON_GROUP_FILENAME("emoji_groups.xml"); +static const std::string GROUP_NAME_ALL("all"); +static const std::string GROUP_NAME_OTHERS("others"); +static const std::string GROUP_NAME_SKIP("skip"); // ============================================================================ // Helper functions @@ -50,82 +55,68 @@ std::list llsd_array_to_list(const LLSD& sd, std::function mutator template<> std::list llsd_array_to_list(const LLSD& sd, std::function mutator) { - std::list result; - for (LLSD::array_const_iterator it = sd.beginArray(), end = sd.endArray(); it != end; ++it) - { - const LLSD& entry = *it; - if (!entry.isString()) - continue; + std::list result; + for (LLSD::array_const_iterator it = sd.beginArray(), end = sd.endArray(); it != end; ++it) + { + const LLSD& entry = *it; + if (!entry.isString()) + continue; - result.push_back(entry.asStringRef()); - if (mutator) - { - mutator(result.back()); - } - } - return result; -} - -LLEmojiDescriptor::LLEmojiDescriptor(const LLSD& descriptor_sd) -{ - Name = descriptor_sd["Name"].asStringRef(); - - const LLWString emoji_string = utf8str_to_wstring(descriptor_sd["Character"].asString()); - Character = (1 == emoji_string.size()) ? emoji_string[0] : L'\0'; // We don't currently support character composition - - auto toLower = [](std::string& str) { LLStringUtil::toLower(str); }; - ShortCodes = llsd_array_to_list(descriptor_sd["ShortCodes"], toLower); - Categories = llsd_array_to_list(descriptor_sd["Categories"], toLower); - - if (Name.empty()) - { - Name = ShortCodes.front(); - } -} - -bool LLEmojiDescriptor::isValid() const -{ - return - Character && - !ShortCodes.empty() && - !Categories.empty(); + result.push_back(entry.asStringRef()); + if (mutator) + { + mutator(result.back()); + } + } + return result; } struct emoji_filter_base { - emoji_filter_base(const std::string& needle) - { - // Search without the colon (if present) so the user can type ':food' and see all emojis in the 'Food' category - mNeedle = (boost::starts_with(needle, ":")) ? needle.substr(1) : needle; - LLStringUtil::toLower(mNeedle); - } + emoji_filter_base(const std::string& needle) + { + // Search without the colon (if present) so the user can type ':food' and see all emojis in the 'Food' category + mNeedle = (boost::starts_with(needle, ":")) ? needle.substr(1) : needle; + LLStringUtil::toLower(mNeedle); + } protected: - std::string mNeedle; + std::string mNeedle; }; struct emoji_filter_shortcode_or_category_contains : public emoji_filter_base { - emoji_filter_shortcode_or_category_contains(const std::string& needle) : emoji_filter_base(needle) {} + emoji_filter_shortcode_or_category_contains(const std::string& needle) : emoji_filter_base(needle) {} - bool operator()(const LLEmojiDescriptor& descr) const - { - for (const auto& short_code : descr.ShortCodes) - { - if (boost::icontains(short_code, mNeedle)) - return true; - } + bool operator()(const LLEmojiDescriptor& descr) const + { + for (const auto& short_code : descr.ShortCodes) + { + if (boost::icontains(short_code, mNeedle)) + return true; + } - for (const auto& category : descr.Categories) - { - if (boost::icontains(category, mNeedle)) - return true; - } + if (boost::icontains(descr.Category, mNeedle)) + return true; - return false; - } + return false; + } }; +std::string LLEmojiDescriptor::getShortCodes() const +{ + std::string result; + for (const std::string& shortCode : ShortCodes) + { + if (!result.empty()) + { + result += ", "; + } + result += shortCode; + } + return result; +} + // ============================================================================ // LLEmojiDictionary class // @@ -137,91 +128,278 @@ LLEmojiDictionary::LLEmojiDictionary() // static void LLEmojiDictionary::initClass() { - LLEmojiDictionary* pThis = &LLEmojiDictionary::initParamSingleton(); + LLEmojiDictionary* pThis = &LLEmojiDictionary::initParamSingleton(); - LLSD data; - - auto filenames = gDirUtilp->findSkinnedFilenames(LLDir::XUI, SKINNED_EMOJI_FILENAME, LLDir::CURRENT_SKIN); - if (filenames.empty()) - { - LL_WARNS() << "Emoji file characters not found" << LL_ENDL; - return; - } - const std::string filename = filenames.back(); - llifstream file(filename.c_str()); - if (file.is_open()) - { - LL_DEBUGS() << "Loading emoji characters file at " << filename << LL_ENDL; - LLSDSerialize::fromXML(data, file); - } - - if (data.isUndefined()) - { - LL_WARNS() << "Emoji file characters missing or ill-formed" << LL_ENDL; - return; - } - - for (LLSD::array_const_iterator descriptor_it = data.beginArray(), descriptor_end = data.endArray(); descriptor_it != descriptor_end; ++descriptor_it) - { - LLEmojiDescriptor descriptor(*descriptor_it); - if (!descriptor.isValid()) - { - LL_WARNS() << "Skipping invalid emoji descriptor " << descriptor.Character << LL_ENDL; - continue; - } - pThis->addEmoji(std::move(descriptor)); - } + pThis->loadTranslations(); + pThis->loadGroups(); + pThis->loadEmojis(); } LLWString LLEmojiDictionary::findMatchingEmojis(const std::string& needle) const { - LLWString result; - boost::transform(mEmojis | boost::adaptors::filtered(emoji_filter_shortcode_or_category_contains(needle)), - std::back_inserter(result), [](const auto& descr) { return descr.Character; }); - return result; + LLWString result; + boost::transform(mEmojis | boost::adaptors::filtered(emoji_filter_shortcode_or_category_contains(needle)), + std::back_inserter(result), [](const auto& descr) { return descr.Character; }); + return result; } const LLEmojiDescriptor* LLEmojiDictionary::getDescriptorFromEmoji(llwchar emoji) const { - const auto it = mEmoji2Descr.find(emoji); - return (mEmoji2Descr.end() != it) ? it->second : nullptr; + const auto it = mEmoji2Descr.find(emoji); + return (mEmoji2Descr.end() != it) ? it->second : nullptr; } const LLEmojiDescriptor* LLEmojiDictionary::getDescriptorFromShortCode(const std::string& short_code) const { - const auto it = mShortCode2Descr.find(short_code); - return (mShortCode2Descr.end() != it) ? it->second : nullptr; + const auto it = mShortCode2Descr.find(short_code); + return (mShortCode2Descr.end() != it) ? it->second : nullptr; } std::string LLEmojiDictionary::getNameFromEmoji(llwchar ch) const { - const auto it = mEmoji2Descr.find(ch); - return (mEmoji2Descr.end() != it) ? it->second->Name : LLStringUtil::null; + const auto it = mEmoji2Descr.find(ch); + return (mEmoji2Descr.end() != it) ? it->second->ShortCodes.front() : LLStringUtil::null; } bool LLEmojiDictionary::isEmoji(llwchar ch) const { - // Currently used codes: A9,AE,203C,2049,2122,...,2B55,3030,303D,3297,3299,1F004,...,1FAF6 - if (ch == 0xA9 || ch == 0xAE || (ch >= 0x2000 && ch < 0x3300) || (ch >= 0x1F000 && ch < 0x20000)) - { - return mEmoji2Descr.find(ch) != mEmoji2Descr.end(); - } + // Currently used codes: A9,AE,203C,2049,2122,...,2B55,3030,303D,3297,3299,1F004,...,1FAF6 + if (ch == 0xA9 || ch == 0xAE || (ch >= 0x2000 && ch < 0x3300) || (ch >= 0x1F000 && ch < 0x20000)) + { + return mEmoji2Descr.find(ch) != mEmoji2Descr.end(); + } - return false; + return false; } -void LLEmojiDictionary::addEmoji(LLEmojiDescriptor&& descr) +void LLEmojiDictionary::loadTranslations() { - mEmojis.push_back(descr); - mEmoji2Descr.insert(std::make_pair(descr.Character, &mEmojis.back())); - for (const std::string& shortCode : descr.ShortCodes) - { - mShortCode2Descr.insert(std::make_pair(shortCode, &mEmojis.back())); - } - for (const std::string& category : descr.Categories) - { - mCategory2Descrs[category].push_back(&mEmojis.back()); - } + std::vector filenames = gDirUtilp->findSkinnedFilenames(LLDir::XUI, SKINNED_CATEGORY_FILENAME, LLDir::CURRENT_SKIN); + if (filenames.empty()) + { + LL_WARNS() << "Emoji file categories not found" << LL_ENDL; + return; + } + + const std::string filename = filenames.back(); + llifstream file(filename.c_str()); + if (!file.is_open()) + { + LL_WARNS() << "Emoji file categories failed to open" << LL_ENDL; + return; + } + + LL_DEBUGS() << "Loading emoji categories file at " << filename << LL_ENDL; + + LLSD data; + LLSDSerialize::fromXML(data, file); + if (data.isUndefined()) + { + LL_WARNS() << "Emoji file categories missing or ill-formed" << LL_ENDL; + return; + } + + // Register translations for all categories + for (LLSD::array_const_iterator it = data.beginArray(), end = data.endArray(); it != end; ++it) + { + const LLSD& sd = *it; + const std::string& name = sd["Name"].asStringRef(); + const std::string& category = sd["Category"].asStringRef(); + if (!name.empty() && !category.empty()) + { + mTranslations[name] = category; + } + else + { + LL_WARNS() << "Skipping invalid emoji category '" << name << "' => '" << category << "'" << LL_ENDL; + } + } +} + +void LLEmojiDictionary::loadGroups() +{ + const std::string filename = gDirUtilp->getExpandedFilename(LL_PATH_APP_SETTINGS, COMMON_GROUP_FILENAME); + llifstream file(filename.c_str()); + if (!file.is_open()) + { + LL_WARNS() << "Emoji file groups failed to open" << LL_ENDL; + return; + } + + LL_DEBUGS() << "Loading emoji groups file at " << filename << LL_ENDL; + + LLSD data; + LLSDSerialize::fromXML(data, file); + if (data.isUndefined()) + { + LL_WARNS() << "Emoji file groups missing or ill-formed" << LL_ENDL; + return; + } + + mGroups.clear(); + // Add group "all" + mGroups.emplace_back(); + // https://www.compart.com/en/unicode/U+1F50D + mGroups.front().Character = 0x1F50D; + // https://www.compart.com/en/unicode/U+1F302 + llwchar iconOthers = 0x1F302; + + // Register all groups + for (LLSD::array_const_iterator it = data.beginArray(), end = data.endArray(); it != end; ++it) + { + const LLSD& sd = *it; + const std::string& name = sd["Name"].asStringRef(); + if (name == GROUP_NAME_ALL) + { + mGroups.front().Character = loadIcon(sd); + } + else if (name == GROUP_NAME_OTHERS) + { + iconOthers = loadIcon(sd); + } + else if (name == GROUP_NAME_SKIP) + { + mSkipCategories = loadCategories(sd); + translateCategories(mSkipCategories); + } + else + { + // Add new group + mGroups.emplace_back(); + LLEmojiGroup& group = mGroups.back(); + group.Character = loadIcon(sd); + group.Categories = loadCategories(sd); + translateCategories(group.Categories); + + for (const std::string& category : group.Categories) + { + mCategory2Group.insert(std::make_pair(category, &group)); + } + } + } + + // Add group "others" + mGroups.emplace_back(); + mGroups.back().Character = iconOthers; +} + +void LLEmojiDictionary::loadEmojis() +{ + std::vector filenames = gDirUtilp->findSkinnedFilenames(LLDir::XUI, SKINNED_EMOJI_FILENAME, LLDir::CURRENT_SKIN); + if (filenames.empty()) + { + LL_WARNS() << "Emoji file characters not found" << LL_ENDL; + return; + } + + const std::string filename = filenames.back(); + llifstream file(filename.c_str()); + if (!file.is_open()) + { + LL_WARNS() << "Emoji file characters failed to open" << LL_ENDL; + return; + } + + LL_DEBUGS() << "Loading emoji characters file at " << filename << LL_ENDL; + + LLSD data; + LLSDSerialize::fromXML(data, file); + if (data.isUndefined()) + { + LL_WARNS() << "Emoji file characters missing or ill-formed" << LL_ENDL; + return; + } + + for (LLSD::array_const_iterator it = data.beginArray(), end = data.endArray(); it != end; ++it) + { + const LLSD& sd = *it; + + llwchar icon = loadIcon(sd); + if (!icon) + { + LL_WARNS() << "Skipping invalid emoji descriptor (no icon)" << LL_ENDL; + continue; + } + + std::list categories = loadCategories(sd); + if (categories.empty()) + { + LL_WARNS() << "Skipping invalid emoji descriptor (no categories)" << LL_ENDL; + continue; + } + + std::string category = categories.front(); + + if (std::find(mSkipCategories.begin(), mSkipCategories.end(), category) != mSkipCategories.end()) + { + // This category is listed for skip + continue; + } + + std::list shortCodes = loadShortCodes(sd); + if (shortCodes.empty()) + { + LL_WARNS() << "Skipping invalid emoji descriptor (no shortCodes)" << LL_ENDL; + continue; + } + + if (mCategory2Group.find(category) == mCategory2Group.end()) + { + // Add unknown category to "others" group + mGroups.back().Categories.push_back(category); + mCategory2Group.insert(std::make_pair(category, &mGroups.back())); + } + + mEmojis.emplace_back(); + LLEmojiDescriptor& emoji = mEmojis.back(); + emoji.Character = icon; + emoji.Category = category; + emoji.ShortCodes = std::move(shortCodes); + + mEmoji2Descr.insert(std::make_pair(icon, &emoji)); + mCategory2Descrs[category].push_back(&emoji); + for (const std::string& shortCode : emoji.ShortCodes) + { + mShortCode2Descr.insert(std::make_pair(shortCode, &emoji)); + } + } +} + +llwchar LLEmojiDictionary::loadIcon(const LLSD& sd) +{ + // We don't currently support character composition + const LLWString icon = utf8str_to_wstring(sd["Character"].asString()); + return (1 == icon.size()) ? icon[0] : L'\0'; +} + +static inline std::list loadStrings(const LLSD& sd, const std::string key) +{ + auto toLower = [](std::string& str) { LLStringUtil::toLower(str); }; + return llsd_array_to_list(sd[key], toLower); +} + +std::list LLEmojiDictionary::loadCategories(const LLSD& sd) +{ + static const std::string categoriesKey("Categories"); + return loadStrings(sd, categoriesKey); +} + +std::list LLEmojiDictionary::loadShortCodes(const LLSD& sd) +{ + static const std::string shortCodesKey("ShortCodes"); + return loadStrings(sd, shortCodesKey); +} + +void LLEmojiDictionary::translateCategories(std::list& categories) +{ + for (std::string& category : categories) + { + auto it = mTranslations.find(category); + if (it != mTranslations.end()) + { + category = it->second; + } + } } // ============================================================================ diff --git a/indra/llui/llemojidictionary.h b/indra/llui/llemojidictionary.h index 1507ebfad3..f6442684a7 100644 --- a/indra/llui/llemojidictionary.h +++ b/indra/llui/llemojidictionary.h @@ -36,14 +36,20 @@ struct LLEmojiDescriptor { - LLEmojiDescriptor(const LLSD& descriptor_sd); + llwchar Character; + std::string Category; + std::list ShortCodes; + std::string getShortCodes() const; +}; - bool isValid() const; +// ============================================================================ +// LLEmojiGroup class +// - std::string Name; - llwchar Character; - std::list ShortCodes; - std::list Categories; +struct LLEmojiGroup +{ + llwchar Character; + std::list Categories; }; // ============================================================================ @@ -52,36 +58,48 @@ struct LLEmojiDescriptor class LLEmojiDictionary : public LLParamSingleton, public LLInitClass { - LLSINGLETON(LLEmojiDictionary); - ~LLEmojiDictionary() override {}; + LLSINGLETON(LLEmojiDictionary); + ~LLEmojiDictionary() override {}; public: - typedef std::map emoji2descr_map_t; - typedef emoji2descr_map_t::value_type emoji2descr_item_t; - typedef std::map code2descr_map_t; - typedef code2descr_map_t::value_type code2descr_item_t; - typedef std::map> cat2descrs_map_t; - typedef cat2descrs_map_t::value_type cat2descrs_item_t; + typedef std::map cat2cat_map_t; + typedef std::map cat2group_map_t; + typedef std::map emoji2descr_map_t; + typedef std::map code2descr_map_t; + typedef std::map> cat2descrs_map_t; - static void initClass(); - LLWString findMatchingEmojis(const std::string& needle) const; - const LLEmojiDescriptor* getDescriptorFromEmoji(llwchar emoji) const; - const LLEmojiDescriptor* getDescriptorFromShortCode(const std::string& short_code) const; - std::string getNameFromEmoji(llwchar ch) const; - bool isEmoji(llwchar ch) const; + static void initClass(); + LLWString findMatchingEmojis(const std::string& needle) const; + const LLEmojiDescriptor* getDescriptorFromEmoji(llwchar emoji) const; + const LLEmojiDescriptor* getDescriptorFromShortCode(const std::string& short_code) const; + std::string getNameFromEmoji(llwchar ch) const; + bool isEmoji(llwchar ch) const; - const emoji2descr_map_t& getEmoji2Descr() const { return mEmoji2Descr; } - const code2descr_map_t& getShortCode2Descr() const { return mShortCode2Descr; } - const cat2descrs_map_t& getCategory2Descrs() const { return mCategory2Descrs; } + const std::vector& getGroups() const { return mGroups; } + const emoji2descr_map_t& getEmoji2Descr() const { return mEmoji2Descr; } + const cat2descrs_map_t& getCategory2Descrs() const { return mCategory2Descrs; } + const code2descr_map_t& getShortCode2Descr() const { return mShortCode2Descr; } private: - void addEmoji(LLEmojiDescriptor&& descr); + void loadTranslations(); + void loadGroups(); + void loadEmojis(); + + static llwchar loadIcon(const LLSD& sd); + static std::list loadCategories(const LLSD& sd); + static std::list loadShortCodes(const LLSD& sd); + void translateCategories(std::list& categories); private: - std::list mEmojis; - emoji2descr_map_t mEmoji2Descr; - code2descr_map_t mShortCode2Descr; - cat2descrs_map_t mCategory2Descrs; + std::vector mGroups; + std::list mEmojis; + std::list mSkipCategories; + + cat2cat_map_t mTranslations; + cat2group_map_t mCategory2Group; + emoji2descr_map_t mEmoji2Descr; + cat2descrs_map_t mCategory2Descrs; + code2descr_map_t mShortCode2Descr; }; // ============================================================================ diff --git a/indra/llui/llscrollingpanellist.cpp b/indra/llui/llscrollingpanellist.cpp index b6f2eb8ba2..3a819e7d06 100644 --- a/indra/llui/llscrollingpanellist.cpp +++ b/indra/llui/llscrollingpanellist.cpp @@ -37,53 +37,44 @@ static LLDefaultChildRegistry::Register r("scrolling_panel // This could probably be integrated with LLScrollContainer -SJB +LLScrollingPanelList::Params::Params() + : is_horizontal("is_horizontal") + , padding("padding") + , spacing("spacing") +{ +} + +LLScrollingPanelList::LLScrollingPanelList(const Params& p) + : LLUICtrl(p) + , mIsHorizontal(p.is_horizontal) + , mPadding(p.padding.isProvided() ? p.padding : DEFAULT_PADDING) + , mSpacing(p.spacing.isProvided() ? p.spacing : DEFAULT_SPACING) +{ +} + void LLScrollingPanelList::clearPanels() { deleteAllChildren(); mPanelList.clear(); - - LLRect rc = getRect(); - rc.setLeftTopAndSize(rc.mLeft, rc.mTop, 1, 1); - setRect(rc); - - notifySizeChanged(rc.getHeight()); + rearrange(); } -S32 LLScrollingPanelList::addPanel( LLScrollingPanel* panel ) +S32 LLScrollingPanelList::addPanel(LLScrollingPanel* panel, bool back) { - addChildInBack( panel ); - mPanelList.push_front( panel ); - - // Resize this view - S32 total_height = 0; - S32 max_width = 0; - S32 cur_gap = 0; - for (std::deque::iterator iter = mPanelList.begin(); - iter != mPanelList.end(); ++iter) + if (back) { - LLScrollingPanel *childp = *iter; - total_height += childp->getRect().getHeight() + cur_gap; - max_width = llmax( max_width, childp->getRect().getWidth() ); - cur_gap = GAP_BETWEEN_PANELS; + addChild(panel); + mPanelList.push_back(panel); } - LLRect rc = getRect(); - rc.setLeftTopAndSize(rc.mLeft, rc.mTop, max_width, total_height); - setRect(rc); - - notifySizeChanged(rc.getHeight()); - - // Reposition each of the child views - S32 cur_y = total_height; - for (std::deque::iterator iter = mPanelList.begin(); - iter != mPanelList.end(); ++iter) + else { - LLScrollingPanel *childp = *iter; - cur_y -= childp->getRect().getHeight(); - childp->translate( -childp->getRect().mLeft, cur_y - childp->getRect().mBottom); - cur_y -= GAP_BETWEEN_PANELS; + addChildInBack(panel); + mPanelList.push_front(panel); } - return total_height; + rearrange(); + + return mIsHorizontal ? getRect().getWidth() : getRect().getHeight(); } void LLScrollingPanelList::removePanel(LLScrollingPanel* panel) @@ -100,7 +91,7 @@ void LLScrollingPanelList::removePanel(LLScrollingPanel* panel) break; } } - if(iter != mPanelList.end()) + if (iter != mPanelList.end()) { removePanel(index); } @@ -120,36 +111,7 @@ void LLScrollingPanelList::removePanel( U32 panel_index ) mPanelList.erase( mPanelList.begin() + panel_index ); } - const S32 GAP_BETWEEN_PANELS = 6; - - // Resize this view - S32 total_height = 0; - S32 max_width = 0; - S32 cur_gap = 0; - for (std::deque::iterator iter = mPanelList.begin(); - iter != mPanelList.end(); ++iter) - { - LLScrollingPanel *childp = *iter; - total_height += childp->getRect().getHeight() + cur_gap; - max_width = llmax( max_width, childp->getRect().getWidth() ); - cur_gap = GAP_BETWEEN_PANELS; - } - LLRect rc = getRect(); - rc.setLeftTopAndSize(rc.mLeft, rc.mTop, max_width, total_height); - setRect(rc); - - notifySizeChanged(rc.getHeight()); - - // Reposition each of the child views - S32 cur_y = total_height; - for (std::deque::iterator iter = mPanelList.begin(); - iter != mPanelList.end(); ++iter) - { - LLScrollingPanel *childp = *iter; - cur_y -= childp->getRect().getHeight(); - childp->translate( -childp->getRect().mLeft, cur_y - childp->getRect().mBottom); - cur_y -= GAP_BETWEEN_PANELS; - } + rearrange(); } void LLScrollingPanelList::updatePanels(BOOL allow_modify) @@ -162,20 +124,91 @@ void LLScrollingPanelList::updatePanels(BOOL allow_modify) } } +void LLScrollingPanelList::rearrange() +{ + // Resize this view + S32 new_width, new_height; + if (!mPanelList.empty()) + { + new_width = new_height = mPadding * 2; + for (std::deque::iterator iter = mPanelList.begin(); + iter != mPanelList.end(); ++iter) + { + LLScrollingPanel* childp = *iter; + const LLRect& rect = childp->getRect(); + if (mIsHorizontal) + { + new_width += rect.getWidth() + mSpacing; + new_height = llmax(new_height, rect.getHeight()); + } + else + { + new_height += rect.getHeight() + mSpacing; + new_width = llmax(new_width, rect.getWidth()); + } + } + + if (mIsHorizontal) + { + new_width -= mSpacing; + } + else + { + new_height -= mSpacing; + } + } + else + { + new_width = new_height = 1; + } + + LLRect rc = getRect(); + if (mIsHorizontal || !followsRight()) + { + rc.mRight = rc.mLeft + new_width; + } + if (!mIsHorizontal || !followsBottom()) + { + rc.mBottom = rc.mTop - new_height; + } + + if (rc.mRight != getRect().mRight || rc.mBottom != getRect().mBottom) + { + setRect(rc); + notifySizeChanged(); + } + + // Reposition each of the child views + S32 pos = mIsHorizontal ? mPadding : rc.getHeight() - mPadding; + for (std::deque::iterator iter = mPanelList.begin(); + iter != mPanelList.end(); ++iter) + { + LLScrollingPanel* childp = *iter; + const LLRect& rect = childp->getRect(); + if (mIsHorizontal) + { + childp->translate(pos - rect.mLeft, rc.getHeight() - mPadding - rect.mTop); + pos += rect.getWidth() + mSpacing; + } + else + { + childp->translate(mPadding - rect.mLeft, pos - rect.mTop); + pos -= rect.getHeight() + mSpacing; + } + } +} + void LLScrollingPanelList::updatePanelVisiblilty() { // Determine visibility of children. - S32 BORDER_WIDTH = 2; // HACK - LLRect parent_local_rect = getParent()->getRect(); - parent_local_rect.stretch( -BORDER_WIDTH ); - LLRect parent_screen_rect; - getParent()->localPointToScreen( - BORDER_WIDTH, 0, + getParent()->localPointToScreen( + mPadding, mPadding, &parent_screen_rect.mLeft, &parent_screen_rect.mBottom ); - getParent()->localPointToScreen( - parent_local_rect.getWidth() - BORDER_WIDTH, parent_local_rect.getHeight() - BORDER_WIDTH, + getParent()->localPointToScreen( + getParent()->getRect().getWidth() - mPadding, + getParent()->getRect().getHeight() - mPadding, &parent_screen_rect.mRight, &parent_screen_rect.mTop ); for (std::deque::iterator iter = mPanelList.begin(); @@ -207,11 +240,12 @@ void LLScrollingPanelList::draw() LLUICtrl::draw(); } -void LLScrollingPanelList::notifySizeChanged(S32 height) +void LLScrollingPanelList::notifySizeChanged() { LLSD info; info["action"] = "size_changes"; - info["height"] = height; + info["height"] = getRect().getHeight(); + info["width"] = getRect().getWidth(); notifyParent(info); } diff --git a/indra/llui/llscrollingpanellist.h b/indra/llui/llscrollingpanellist.h index e8df176ec3..9dc65dabb5 100644 --- a/indra/llui/llscrollingpanellist.h +++ b/indra/llui/llscrollingpanellist.h @@ -51,12 +51,18 @@ class LLScrollingPanelList : public LLUICtrl { public: struct Params : public LLInitParam::Block - {}; - LLScrollingPanelList(const Params& p) - : LLUICtrl(p) - {} + { + Optional is_horizontal; + Optional padding; + Optional spacing; + + Params(); + }; + + LLScrollingPanelList(const Params& p); - static const S32 GAP_BETWEEN_PANELS = 6; + static const S32 DEFAULT_SPACING = 6; + static const S32 DEFAULT_PADDING = 2; typedef std::deque panel_list_t; @@ -65,11 +71,18 @@ public: virtual void draw(); void clearPanels(); - S32 addPanel( LLScrollingPanel* panel ); - void removePanel( LLScrollingPanel* panel ); - void removePanel( U32 panel_index ); + S32 addPanel(LLScrollingPanel* panel, bool back = false); + void removePanel(LLScrollingPanel* panel); + void removePanel(U32 panel_index); void updatePanels(BOOL allow_modify); - const panel_list_t& getPanelList() { return mPanelList; } + void rearrange(); + + const panel_list_t& getPanelList() const { return mPanelList; } + bool getIsHorizontal() const { return mIsHorizontal; } + void setPadding(S32 padding) { mPadding = padding; rearrange(); } + void setSpacing(S32 spacing) { mSpacing = spacing; rearrange(); } + S32 getPadding() const { return mPadding; } + S32 getSpacing() const { return mSpacing; } private: void updatePanelVisiblilty(); @@ -77,7 +90,11 @@ private: /** * Notify parent about size change, makes sense when used inside accordion */ - void notifySizeChanged(S32 height); + void notifySizeChanged(); + + bool mIsHorizontal; + S32 mPadding; + S32 mSpacing; panel_list_t mPanelList; }; diff --git a/indra/newview/app_settings/emoji_groups.xml b/indra/newview/app_settings/emoji_groups.xml new file mode 100644 index 0000000000..b433927f91 --- /dev/null +++ b/indra/newview/app_settings/emoji_groups.xml @@ -0,0 +1,82 @@ + + + + + Name + all + Character + 🔍 + + + Character + 😀 + Categories + + smileys and emotion + people and body + + + + Character + 🥬 + Categories + + animals and nature + + + + Character + 🍔 + Categories + + food and drink + + + + Character + 🛩 + Categories + + travel and places + + + + Character + 🏈 + Categories + + activities + + + + Character + 💡 + Categories + + objects + + + + Character + + Categories + + symbols + + + + Name + others + Character + 🌂 + + + Name + skip + Categories + + components + + + + diff --git a/indra/newview/llfloateremojipicker.cpp b/indra/newview/llfloateremojipicker.cpp index 7fbaaaaa89..08929b05f5 100644 --- a/indra/newview/llfloateremojipicker.cpp +++ b/indra/newview/llfloateremojipicker.cpp @@ -27,275 +27,844 @@ #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" -std::string LLFloaterEmojiPicker::mSelectedCategory; -std::string LLFloaterEmojiPicker::mSearchPattern; -int LLFloaterEmojiPicker::mSelectedEmojiIndex; +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 sRecentlyUsed; +static std::list> 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 { public: - LLEmojiScrollListItem(const llwchar emoji, const LLScrollListItem::Params& params) - : LLScrollListItem(params) - , mEmoji(emoji) - { - } + LLEmojiScrollListItem(const llwchar emoji, const LLScrollListItem::Params& params) + : LLScrollListItem(params) + , mEmoji(emoji) + { + } - llwchar getEmoji() const { return mEmoji; } + 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); + 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 - } + 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; + 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* 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; + 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); + 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 - mPreviewEmoji = getChild("PreviewEmoji"); - mPreviewEmoji->setClickedCallback([this](LLUICtrl*, const LLSD&) { onPreviewEmojiClick(); }); + // Should be initialized first + mPreviewEmoji = getChild("PreviewEmoji"); + mPreviewEmoji->setClickedCallback([this](LLUICtrl*, const LLSD&) { onPreviewEmojiClick(); }); - mCategory = getChild("Category"); - mCategory->setCommitCallback([this](LLUICtrl*, const LLSD&) { onCategoryCommit(); }); - const LLEmojiDictionary::cat2descrs_map_t& cat2Descrs = LLEmojiDictionary::instance().getCategory2Descrs(); - mCategory->clearRows(); - for (const LLEmojiDictionary::cat2descrs_item_t& item : cat2Descrs) - { - std::string value = item.first; - std::string name = value; - LLStringUtil::capitalize(name); - mCategory->add(name, value); - } - mCategory->setSelectedByValue(mSelectedCategory, true); + mDescription = getChild("Description"); + mDescription->setVisible(FALSE); - mSearch = getChild("Search"); - mSearch->setKeystrokeCallback([this](LLLineEditor*, void*) { onSearchKeystroke(); }, NULL); - mSearch->setFont(LLViewerChat::getChatFont()); - mSearch->setText(mSearchPattern); + mGroups = getChild("Groups"); + mBadge = getChild("Badge"); - mEmojis = getChild("Emojis"); - mEmojis->setCommitCallback([this](LLUICtrl*, const LLSD&) { onEmojiSelect(); }); - mEmojis->setDoubleClickCallback([this]() { onEmojiPick(); }); - fillEmojis(); + mSearch = getChild("Search"); + mSearch->setKeystrokeCallback([this](LLLineEditor*, void*) { onSearchKeystroke(); }, NULL); + mSearch->setFont(LLViewerChat::getChatFont()); + mSearch->setText(sSearchPattern); - return TRUE; + 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 ); + gFocusMgr.releaseFocusIfNeeded( this ); } -void LLFloaterEmojiPicker::fillEmojis() +void LLFloaterEmojiPicker::fillGroups() { - mEmojis->clearRows(); + LLButton::Params params; + params.font = LLFontGL::getFontEmoji(); - const LLEmojiDictionary::emoji2descr_map_t& emoji2Descr = LLEmojiDictionary::instance().getEmoji2Descr(); - for (const LLEmojiDictionary::emoji2descr_item_t& item : emoji2Descr) - { - const LLEmojiDescriptor* descr = item.second; + LLRect rect; + rect.mTop = mGroups->getRect().getHeight(); + rect.mBottom = mBadge->getRect().getHeight(); - if (!mSelectedCategory.empty() && !matchesCategory(descr)) - continue; + 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); }); - if (!mSearchPattern.empty() && !matchesPattern(descr)) - continue; + button->setRect(rect); - LLScrollListItem::Params params; - // The following line adds default monochrome view of the emoji (is shown as an example) - //params.columns.add().column("look").value(wstring_to_utf8str(LLWString(1, it.first))); - params.columns.add().column("name").value(descr->Name); - mEmojis->addRow(new LLEmojiScrollListItem(item.first, params), params); - } + LLUIString text; + text.insert(0, LLWString(1, group.Character)); + button->setLabel(text); - if (mEmojis->getItemCount()) - { - if (mSelectedEmojiIndex > 0 && mSelectedEmojiIndex < mEmojis->getItemCount()) - mEmojis->selectNthItem(mSelectedEmojiIndex); - else - mEmojis->selectFirstItem(); + if (mGroupButtons.size() == sSelectedGroupIndex) + { + button->setToggleState(TRUE); + button->setUseFontColor(TRUE); + } - mEmojis->scrollToShowSelected(); - } - else - { - onEmojiEmpty(); - } + mGroupButtons.push_back(button); + mGroups->addChild(button); + } + + moveGroups(); } -bool LLFloaterEmojiPicker::matchesCategory(const LLEmojiDescriptor* descr) +void LLFloaterEmojiPicker::moveGroups() { - return std::find(descr->Categories.begin(), descr->Categories.end(), mSelectedCategory) != descr->Categories.end(); + 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 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 (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)) + { + 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->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::emoji2descr_map_t& emoji2descr = LLEmojiDictionary::instance().getEmoji2Descr(); + const LLEmojiDictionary::cat2descrs_map_t& category2Descr = LLEmojiDictionary::instance().getCategory2Descrs(); + if (!sSelectedGroupIndex) + { + std::vector 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 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); + } + } + } } bool LLFloaterEmojiPicker::matchesPattern(const LLEmojiDescriptor* descr) { - if (descr->Name.find(mSearchPattern) != std::string::npos) - return true; - for (const std::string& shortCode : descr->ShortCodes) - if (shortCode.find(mSearchPattern) != std::string::npos) - return true; - for (const std::string& category : descr->Categories) - if (category.find(mSearchPattern) != std::string::npos) - return true; - return false; + for (const std::string& shortCode : descr->ShortCodes) + if (shortCode.find(sSearchPattern) != std::string::npos) + return true; + return false; } -void LLFloaterEmojiPicker::onCategoryCommit() +void LLFloaterEmojiPicker::onGroupButtonClick(LLUICtrl* ctrl) { - mSelectedCategory = mCategory->getSelectedValue().asString(); - mSelectedEmojiIndex = 0; - fillEmojis(); + 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() { - mSearchPattern = mSearch->getText(); - mSelectedEmojiIndex = 0; - fillEmojis(); + sSearchPattern = mSearch->getText(); + fillEmojis(); } void LLFloaterEmojiPicker::onPreviewEmojiClick() { - if (mEmojiPickCallback) - { - if (LLEmojiScrollListItem* item = dynamic_cast(mEmojis->getFirstSelected())) - { - mEmojiPickCallback(item->getEmoji()); - } - } + if (mEmojiPickCallback) + { + if (LLEmojiGridIcon* icon = dynamic_cast(mHoveredIcon)) + { + mEmojiPickCallback(icon->getEmoji()); + } + } } -void LLFloaterEmojiPicker::onEmojiSelect() +void LLFloaterEmojiPicker::onGridMouseEnter() { - const LLEmojiScrollListItem* item = dynamic_cast(mEmojis->getFirstSelected()); - if (item) - { - mSelectedEmojiIndex = mEmojis->getFirstSelectedIndex(); - LLUIString text; - text.insert(0, LLWString(1, item->getEmoji())); - mPreviewEmoji->setLabel(text); - return; - } - - onEmojiEmpty(); + mSearch->setVisible(FALSE); + mDescription->setText(LLStringExplicit(""), LLStyle::Params()); + mDescription->setVisible(TRUE); } -void LLFloaterEmojiPicker::onEmojiEmpty() +void LLFloaterEmojiPicker::onGridMouseLeave() { - mSelectedEmojiIndex = 0; - mPreviewEmoji->setLabel(LLUIString()); + mDescription->setVisible(FALSE); + mDescription->setText(LLStringExplicit(""), LLStyle::Params()); + mSearch->setVisible(TRUE); + mSearch->setFocus(TRUE); } -void LLFloaterEmojiPicker::onEmojiPick() +void LLFloaterEmojiPicker::onGroupButtonMouseEnter(LLUICtrl* ctrl) { - if (mEmojiPickCallback) - { - if (LLEmojiScrollListItem* item = dynamic_cast(mEmojis->getFirstSelected())) - { - mEmojiPickCallback(item->getEmoji()); - closeFloater(); - } - } + 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)) + { + onEmojiUsed(icon->getEmoji()); + 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_RETURN: - if (mCategory->hasFocus()) - break; - onEmojiPick(); - return TRUE; - case KEY_ESCAPE: - closeFloater(); - return TRUE; - case KEY_UP: - mEmojis->selectPrevItem(); - mEmojis->scrollToShowSelected(); - return TRUE; - case KEY_DOWN: - mEmojis->selectNextItem(); - mEmojis->scrollToShowSelected(); - return TRUE; - } - } + if (mask == MASK_NONE) + { + switch (key) + { + case KEY_ESCAPE: + closeFloater(); + return TRUE; + } + } - return LLFloater::handleKeyHere(key, mask); + return LLFloater::handleKeyHere(key, mask); } // virtual void LLFloaterEmojiPicker::closeFloater(bool app_quitting) { - LLFloater::closeFloater(app_quitting); - if (mFloaterCloseCallback) - mFloaterCloseCallback(); + 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 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 ftokens = LLStringUtil::getTokens(frequentlyUsed, ","); + int maxCountF = 20; + for (const std::string& token : ftokens) + { + std::vector 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& 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 7327fb945e..d00391c61f 100644 --- a/indra/newview/llfloateremojipicker.h +++ b/indra/newview/llfloateremojipicker.h @@ -33,49 +33,71 @@ struct LLEmojiDescriptor; class LLFloaterEmojiPicker : public LLFloater { + using super = LLFloater; + public: - // The callback function will be called with an emoji char. - typedef boost::function pick_callback_t; - typedef boost::function close_callback_t; + // The callback function will be called with an emoji char. + typedef boost::function pick_callback_t; + typedef boost::function close_callback_t; - // Call this to select an emoji. - static LLFloaterEmojiPicker* getInstance(); - static LLFloaterEmojiPicker* showInstance(pick_callback_t pick_callback = nullptr, close_callback_t close_callback = nullptr); + // Call this to select an emoji. + static LLFloaterEmojiPicker* getInstance(); + static LLFloaterEmojiPicker* showInstance(pick_callback_t pick_callback = nullptr, close_callback_t close_callback = nullptr); - LLFloaterEmojiPicker(const LLSD& key); - virtual ~LLFloaterEmojiPicker(); + LLFloaterEmojiPicker(const LLSD& key); + virtual ~LLFloaterEmojiPicker(); - virtual BOOL postBuild(); + virtual BOOL postBuild() override; + virtual void dirtyRect() override; - void show(pick_callback_t pick_callback = nullptr, close_callback_t close_callback = nullptr); + void show(pick_callback_t pick_callback = nullptr, close_callback_t close_callback = nullptr); - virtual void closeFloater(bool app_quitting = false); + virtual void closeFloater(bool app_quitting = false) override; private: - void fillEmojis(); - bool matchesCategory(const LLEmojiDescriptor* descr); - bool matchesPattern(const LLEmojiDescriptor* descr); + void fillGroups(); + void moveGroups(); + void fillEmojis(bool fromResize = false); - void onCategoryCommit(); - void onSearchKeystroke(); - void onPreviewEmojiClick(); - void onEmojiSelect(); - void onEmojiEmpty(); - void onEmojiPick(); + bool matchesPattern(const LLEmojiDescriptor* descr); - virtual BOOL handleKeyHere(KEY key, MASK mask); + void onGroupButtonClick(LLUICtrl* ctrl); + void onSearchKeystroke(); + void onPreviewEmojiClick(); + void onGridMouseEnter(); + void onGridMouseLeave(); + void onGroupButtonMouseEnter(LLUICtrl* ctrl); + void onGroupButtonMouseLeave(LLUICtrl* ctrl); + void onEmojiMouseEnter(LLUICtrl* ctrl); + void onEmojiMouseLeave(LLUICtrl* ctrl); + void onEmojiMouseClick(LLUICtrl* ctrl, MASK mask); - class LLComboBox* mCategory { nullptr }; - class LLLineEditor* mSearch { nullptr }; - class LLScrollListCtrl* mEmojis { nullptr }; - class LLButton* mPreviewEmoji { nullptr }; + void selectGridIcon(LLUICtrl* ctrl); + void unselectGridIcon(LLUICtrl* ctrl); - pick_callback_t mEmojiPickCallback; - close_callback_t mFloaterCloseCallback; + virtual BOOL handleKeyHere(KEY key, MASK mask) override; - static std::string mSelectedCategory; - static std::string mSearchPattern; - static int mSelectedEmojiIndex; + void onEmojiUsed(llwchar emoji); + void loadState(); + void saveState(); + + class LLPanel* mGroups { nullptr }; + class LLPanel* mBadge { nullptr }; + class LLLineEditor* mSearch { nullptr }; + class LLScrollContainer* mEmojiScroll { nullptr }; + class LLScrollingPanelList* mEmojiGrid { nullptr }; + class LLButton* mPreviewEmoji { nullptr }; + class LLTextBox* mDescription { nullptr }; + + pick_callback_t mEmojiPickCallback; + close_callback_t mFloaterCloseCallback; + + std::vector mGroupButtons; + + S32 mRecentBadgeWidth { 0 }; + S32 mRecentGridWidth { 0 }; + S32 mRecentMaxIcons { 0 }; + LLUICtrl* mHoveredIcon { nullptr }; }; #endif diff --git a/indra/newview/skins/default/xui/da/emoji_categories.xml b/indra/newview/skins/default/xui/da/emoji_categories.xml new file mode 100644 index 0000000000..456b18e4e2 --- /dev/null +++ b/indra/newview/skins/default/xui/da/emoji_categories.xml @@ -0,0 +1,59 @@ + + + + + Name + smileys and emotion + Category + smileys and følelser + + + Name + people and body + Category + mennesker and krop + + + Name + components + Category + komponenter + + + Name + animals and nature + Category + dyr and natur + + + Name + food and drink + Category + mad and drikke + + + Name + travel and places + Category + rejser and steder + + + Name + activities + Category + oplevelser + + + Name + objects + Category + objekter + + + Name + symbols + Category + symboler + + + diff --git a/indra/newview/skins/default/xui/de/emoji_categories.xml b/indra/newview/skins/default/xui/de/emoji_categories.xml new file mode 100644 index 0000000000..ed63d0bac9 --- /dev/null +++ b/indra/newview/skins/default/xui/de/emoji_categories.xml @@ -0,0 +1,59 @@ + + + + + Name + smileys and emotion + Category + Smileys and Emotionen + + + Name + people and body + Category + Menschen and Körper + + + Name + components + Category + Komponenten + + + Name + animals and nature + Category + Tiere and Natur + + + Name + food and drink + Category + Essen and Trinken + + + Name + travel and places + Category + Reisen and Orte + + + Name + activities + Category + Aktivitäten + + + Name + objects + Category + Gegenstände + + + Name + symbols + Category + Symbole + + + diff --git a/indra/newview/skins/default/xui/de/floater_emoji_picker.xml b/indra/newview/skins/default/xui/de/floater_emoji_picker.xml index 7e9adaa76b..00301c19d8 100644 --- a/indra/newview/skins/default/xui/de/floater_emoji_picker.xml +++ b/indra/newview/skins/default/xui/de/floater_emoji_picker.xml @@ -1,9 +1,6 @@ + + - - - - - diff --git a/indra/newview/skins/default/xui/en/emoji_categories.xml b/indra/newview/skins/default/xui/en/emoji_categories.xml new file mode 100644 index 0000000000..0315d0c43a --- /dev/null +++ b/indra/newview/skins/default/xui/en/emoji_categories.xml @@ -0,0 +1,59 @@ + + + + + Name + smileys and emotion + Category + smileys and emotion + + + Name + people and body + Category + people and body + + + Name + components + Category + components + + + Name + animals and nature + Category + animals and nature + + + Name + food and drink + Category + food and drink + + + Name + travel and places + Category + travel and places + + + Name + activities + Category + activities + + + Name + objects + Category + objects + + + Name + symbols + Category + symbols + + + diff --git a/indra/newview/skins/default/xui/en/floater_activeim.xml b/indra/newview/skins/default/xui/en/floater_activeim.xml index 62c6b8c29e..357c9c38c9 100644 --- a/indra/newview/skins/default/xui/en/floater_activeim.xml +++ b/indra/newview/skins/default/xui/en/floater_activeim.xml @@ -23,7 +23,7 @@ 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 ada065213e..dacfee5bc9 100644 --- a/indra/newview/skins/default/xui/en/floater_emoji_picker.xml +++ b/indra/newview/skins/default/xui/en/floater_emoji_picker.xml @@ -7,59 +7,77 @@ legacy_header_height="0" can_resize="true" layout="topleft" + min_width="250" height="400" - width="200"> + width="250"> + + + width="212" /> +