diff options
| author | gwigz <gwigz@users.noreply.github.com> | 2026-06-18 22:04:45 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-06-19 00:04:45 +0300 |
| commit | 2c591b1429f44795aceebd5c6f5ea6907b2a08ea (patch) | |
| tree | 89f6416ea920acfc381a3f35f23de45ad3b4c98f /indra/newview | |
| parent | fc2e96135d0a55df5f3b668298fa72505d39413b (diff) | |
#5928 Gesture auto-complete, similar to emojis/mentions
Diffstat (limited to 'indra/newview')
| -rw-r--r-- | indra/newview/CMakeLists.txt | 2 | ||||
| -rw-r--r-- | indra/newview/llfloatergestureautocompletepicker.cpp | 165 | ||||
| -rw-r--r-- | indra/newview/llfloatergestureautocompletepicker.h | 47 | ||||
| -rw-r--r-- | indra/newview/llfloaterimnearbychat.cpp | 100 | ||||
| -rw-r--r-- | indra/newview/llviewerfloaterreg.cpp | 2 | ||||
| -rw-r--r-- | indra/newview/skins/default/xui/en/floater_gesture_autocomplete_picker.xml | 39 |
6 files changed, 354 insertions, 1 deletions
diff --git a/indra/newview/CMakeLists.txt b/indra/newview/CMakeLists.txt index 663b932f3b..3e0c343913 100644 --- a/indra/newview/CMakeLists.txt +++ b/indra/newview/CMakeLists.txt @@ -233,6 +233,7 @@ set(viewer_SOURCE_FILES llfloaterfonttest.cpp llfloaterforgetuser.cpp llfloatergesture.cpp + llfloatergestureautocompletepicker.cpp llfloatergltfasseteditor.cpp llfloatergodtools.cpp llfloatergotoline.cpp @@ -912,6 +913,7 @@ set(viewer_HEADER_FILES llfloaterfonttest.h llfloaterforgetuser.h llfloatergesture.h + llfloatergestureautocompletepicker.h llfloatergltfasseteditor.h llfloatergodtools.h llfloatergotoline.h diff --git a/indra/newview/llfloatergestureautocompletepicker.cpp b/indra/newview/llfloatergestureautocompletepicker.cpp new file mode 100644 index 0000000000..14d2065b5b --- /dev/null +++ b/indra/newview/llfloatergestureautocompletepicker.cpp @@ -0,0 +1,165 @@ +/** + * @file llfloatergestureautocompletepicker.cpp + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2026, 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 "llfloatergestureautocompletepicker.h" + +#include "llgestureautocompletehelper.h" +#include "llscrolllistctrl.h" +#include "llscrolllistitem.h" + +LLFloaterGestureAutocompletePicker::LLFloaterGestureAutocompletePicker(const LLSD& key) +: LLFloater(key), mGestureList(NULL) +{ + setFocusStealsFrontmost(false); + setBackgroundVisible(false); + setAutoFocus(false); +} + +bool LLFloaterGestureAutocompletePicker::postBuild() +{ + mGestureList = getChild<LLScrollListCtrl>("gesture_list"); + mGestureList->setCommitOnKeyboardMovement(false); + mGestureList->setCommitCallback(boost::bind(&LLFloaterGestureAutocompletePicker::commitSelected, this)); + + return LLFloater::postBuild(); +} + +void LLFloaterGestureAutocompletePicker::onOpen(const LLSD& key) +{ + LLGestureAutocompleteHelper& helper = LLGestureAutocompleteHelper::instance(); + mGestureList->clearRows(); + + const std::vector<LLGestureAutocompleteHelper::Row>& rows = helper.rows(); + + for (const auto& row : rows) + { + LLSD element; + element["value"] = row.value; + element["columns"][0]["column"] = "trigger"; + element["columns"][0]["value"] = row.trigger; + element["columns"][1]["column"] = "name"; + element["columns"][1]["value"] = row.name; + mGestureList->addElement(element); + } + + if (rows.empty() && !helper.emptyText().empty()) + { + LLSD element; + element["enabled"] = false; + element["columns"][0]["column"] = "trigger"; + element["columns"][0]["value"] = helper.emptyText(); + element["columns"][1]["column"] = "name"; + element["columns"][1]["value"] = LLStringUtil::null; + mGestureList->addElement(element); + } + + if (helper.total() > rows.size()) + { + LLSD element; + element["enabled"] = false; + element["columns"][0]["column"] = "trigger"; + element["columns"][0]["value"] = LLStringUtil::null; + element["columns"][1]["column"] = "name"; + + LLStringUtil::format_map_t args; + args["[COUNT]"] = llformat("%d", (S32)rows.size()); + args["[TOTAL]"] = llformat("%d", (S32)helper.total()); + element["columns"][1]["value"] = getString("showing_count", args); + + mGestureList->addElement(element); + } + + mGestureList->selectFirstItem(); + gFloaterView->adjustToFitScreen(this, false); +} + +bool LLFloaterGestureAutocompletePicker::handleKey(KEY key, MASK mask, bool called_from_parent) +{ + if (mask == MASK_NONE) + { + switch (key) + { + case KEY_UP: + mGestureList->selectPrevItem(); + mGestureList->scrollToShowSelected(); + return true; + case KEY_DOWN: + mGestureList->selectNextItem(); + mGestureList->scrollToShowSelected(); + return true; + case KEY_RETURN: + case KEY_TAB: + commitSelected(); + return true; + case KEY_ESCAPE: + LLGestureAutocompleteHelper::instance().hideHelper(); + return true; + case KEY_LEFT: + case KEY_RIGHT: + return true; + default: + break; + } + } + + return LLFloater::handleKey(key, mask, called_from_parent); +} + +void LLFloaterGestureAutocompletePicker::onClose(bool app_quitting) +{ + if (!app_quitting) + { + LLGestureAutocompleteHelper::instance().hideHelper(); + } +} + +void LLFloaterGestureAutocompletePicker::goneFromFront() +{ + LLGestureAutocompleteHelper::instance().hideHelper(); +} + +bool LLFloaterGestureAutocompletePicker::commitSelected() +{ + LLScrollListItem* item = mGestureList->getFirstSelected(); + + if (!item || !item->getEnabled()) + { + return false; + } + + const std::string value = mGestureList->getSelectedValue().asString(); + + if (value.empty()) + { + return false; + } + + setValue(value); + onCommit(); + + return true; +} diff --git a/indra/newview/llfloatergestureautocompletepicker.h b/indra/newview/llfloatergestureautocompletepicker.h new file mode 100644 index 0000000000..71f754138d --- /dev/null +++ b/indra/newview/llfloatergestureautocompletepicker.h @@ -0,0 +1,47 @@ +/** + * @file llfloatergestureautocompletepicker.h + * + * $LicenseInfo:firstyear=2026&license=viewerlgpl$ + * Second Life Viewer Source Code + * Copyright (C) 2026, 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$ + */ + +#pragma once + +#include "llfloater.h" + +class LLScrollListCtrl; + +class LLFloaterGestureAutocompletePicker : public LLFloater +{ +public: + LLFloaterGestureAutocompletePicker(const LLSD& key); + + bool postBuild() override; + void onOpen(const LLSD& key) override; + bool handleKey(KEY key, MASK mask, bool called_from_parent) override; + void onClose(bool app_quitting) override; + void goneFromFront() override; + +private: + bool commitSelected(); + + LLScrollListCtrl* mGestureList; +}; diff --git a/indra/newview/llfloaterimnearbychat.cpp b/indra/newview/llfloaterimnearbychat.cpp index f0d696361a..8fa183b180 100644 --- a/indra/newview/llfloaterimnearbychat.cpp +++ b/indra/newview/llfloaterimnearbychat.cpp @@ -55,6 +55,7 @@ #include "llfloaterimnearbychatlistener.h" #include "llagent.h" // gAgent #include "llgesturemgr.h" +#include "llgestureautocompletehelper.h" #include "llmultigesture.h" #include "llkeyboard.h" #include "llanimationstates.h" @@ -70,6 +71,8 @@ #include "llautoreplace.h" #include "lluiusage.h" +#include <map> + S32 LLFloaterIMNearbyChat::sLastSpecialChatChannel = 0; static LLFloaterIMNearbyChatListener sChatListener; @@ -77,6 +80,72 @@ static LLFloaterIMNearbyChatListener sChatListener; constexpr S32 EXPANDED_HEIGHT = 266; constexpr S32 COLLAPSED_HEIGHT = 60; constexpr S32 EXPANDED_MIN_HEIGHT = 150; +constexpr size_t MAX_GESTURE_AUTOCOMPLETE_ROWS = 50; + +namespace +{ +bool buildGestureAutocompleteRows( + const std::string& prefix, + std::vector<LLGestureAutocompleteHelper::Row>& rows, + size_t& total, + std::string& empty_text) +{ + rows.clear(); + total = 0; + empty_text.clear(); + + // Wait for at least one character after the slash before offering matches. + if (prefix.size() < 2 || prefix[0] != '/' || prefix.find_first_of(" \t") != std::string::npos) + { + return false; + } + + std::string lower_prefix = prefix; + LLStringUtil::toLower(lower_prefix); + + std::map<std::string, std::string> unique; + const LLGestureMgr::item_map_t& active = LLGestureMgr::instance().getActiveGestures(); + + for (const auto& entry : active) + { + LLMultiGesture* gesture = entry.second; + + if (!gesture || gesture->getTrigger().empty() || gesture->getTrigger()[0] != '/') + { + continue; + } + + std::string lower_trigger = gesture->getTrigger(); + LLStringUtil::toLower(lower_trigger); + + if (lower_trigger.compare(0, lower_prefix.size(), lower_prefix) != 0) + { + continue; + } + + unique.emplace( + gesture->getTrigger(), + gesture->mName); + } + + for (const auto& gesture : unique) + { + ++total; + + if (rows.size() < MAX_GESTURE_AUTOCOMPLETE_ROWS) + { + rows.push_back({ gesture.first, gesture.first, gesture.second }); + } + } + + if (rows.empty()) + { + empty_text = "No matching gestures"; + } + + return total > 0; +} +} // legacy callback glue void send_chat_from_viewer(const std::string& utf8_out_text, EChatType type, S32 channel); @@ -501,8 +570,34 @@ void LLFloaterIMNearbyChat::onChatBoxKeystroke() KEY key = gKeyboard->currentKey(); + static LLCachedControl<bool> autocomplete_gestures(gSavedSettings, "ChatAutocompleteGestures", true); + + if (autocomplete_gestures) + { + std::vector<LLGestureAutocompleteHelper::Row> rows; + size_t total = 0; + std::string empty_text; + const std::string utf8_trigger = wstring_to_utf8str(raw_text); + + if (buildGestureAutocompleteRows(utf8_trigger, rows, total, empty_text)) + { + LLGestureAutocompleteHelper::instance().showHelper( + mInputEditor, + rows, + total, + empty_text, + [this](std::string trigger) + { + mInputEditor->setText(trigger + " "); + mInputEditor->endOfDoc(); + }); + return; + } + + LLGestureAutocompleteHelper::instance().hideHelper(mInputEditor); + } // Ignore "special" keys, like backspace, arrows, etc. - if (gSavedSettings.getBOOL("ChatAutocompleteGestures") + if (autocomplete_gestures && length > 1 && raw_text[0] == '/' && key < KEY_SPECIAL) @@ -589,6 +684,9 @@ void LLFloaterIMNearbyChat::sendChat( EChatType type ) LLWString text = mInputEditor->getConvertedText(); LLWStringUtil::trim(text); LLWStringUtil::replaceChar(text,182,'\n'); // Convert paragraph symbols back into newlines. + + LLGestureAutocompleteHelper::instance().hideHelper(); + if (!text.empty()) { // Check if this is destined for another channel diff --git a/indra/newview/llviewerfloaterreg.cpp b/indra/newview/llviewerfloaterreg.cpp index eef31c7048..b034a934de 100644 --- a/indra/newview/llviewerfloaterreg.cpp +++ b/indra/newview/llviewerfloaterreg.cpp @@ -79,6 +79,7 @@ #include "llfloaterfonttest.h" #include "llfloaterforgetuser.h" #include "llfloatergesture.h" +#include "llfloatergestureautocompletepicker.h" #include "llfloatergltfasseteditor.h" #include "llfloatergodtools.h" #include "llfloatergridstatus.h" @@ -382,6 +383,7 @@ void LLViewerFloaterReg::registerFloaters() LLFloaterReg::add("font_test", "floater_font_test.xml", (LLFloaterBuildFunc)&LLFloaterReg::build<LLFloaterFontTest>); LLFloaterReg::add("forget_username", "floater_forget_user.xml", (LLFloaterBuildFunc)&LLFloaterReg::build<LLFloaterForgetUser>); + LLFloaterReg::add("gesture_autocomplete_picker", "floater_gesture_autocomplete_picker.xml", (LLFloaterBuildFunc)&LLFloaterReg::build<LLFloaterGestureAutocompletePicker>); LLFloaterReg::add("gestures", "floater_gesture.xml", (LLFloaterBuildFunc)&LLFloaterReg::build<LLFloaterGesture>); LLFloaterReg::add("gltf_asset_editor", "floater_gltf_asset_editor.xml", (LLFloaterBuildFunc)&LLFloaterReg::build<LLFloaterGLTFAssetEditor>); LLFloaterReg::add("god_tools", "floater_god_tools.xml", (LLFloaterBuildFunc)&LLFloaterReg::build<LLFloaterGodTools>); diff --git a/indra/newview/skins/default/xui/en/floater_gesture_autocomplete_picker.xml b/indra/newview/skins/default/xui/en/floater_gesture_autocomplete_picker.xml new file mode 100644 index 0000000000..cc4a3133cb --- /dev/null +++ b/indra/newview/skins/default/xui/en/floater_gesture_autocomplete_picker.xml @@ -0,0 +1,39 @@ +<?xml version="1.0" encoding="utf-8" standalone="yes" ?> +<floater + name="gesture_autocomplete_picker" + title="CHOOSE GESTURE" + single_instance="true" + can_minimize="false" + can_tear_off="false" + can_resize="true" + auto_close="true" + layout="topleft" + min_width="250" + chrome="true" + sound_flags="0" + height="136" + width="340"> + <floater.string + name="showing_count"> + Showing [COUNT] of [TOTAL] + </floater.string> + <scroll_list + follows="all" + height="130" + width="336" + layout="topleft" + left="3" + top="3" + name="gesture_list" + draw_heading="false" + multi_select="false"> + <scroll_list.columns + name="trigger" + label="Trigger" + width="128" /> + <scroll_list.columns + name="name" + label="Gesture" + dynamic_width="true" /> + </scroll_list> +</floater> |
