fix(server): 翻页扩展时保留混输候选并同步 Emoji 末尾排序
This commit is contained in:
1 parent
6489d4be17
commit
c2bd59d448
10 files changed
+248
-23
No files matched your search
@@ -228,7 +228,7 @@ Windows 端的全部一方源码都在本仓,一次 clone 就能拿到完整
|
||||
全拼 / 双拼输入中文时,可以把英文、emoji、颜文字、快捷短语和日期时间直接混进候选,不用切换到对应的快捷模式(五笔不混输)。这些开关集中在设置 → 实用功能的「候选混输」里:总开关关闭后全部不混入,展开后可逐项开关,各项的设置在总开关关闭期间保留。
|
||||
|
||||
- **中英混输**:根据字母串补充英文候选。触发字符数可选(1~8),达到该长度后才出现英文候选项。
|
||||
- **emoji 混输**:在英文候选之后插入匹配的 emoji;**颜文字混输**排在 emoji 之后。若同时开启云候选或 AI 联想,这些附加项会相应顺移。
|
||||
- **emoji 混输**:匹配的 emoji 排在候选项列表最后,不挤占正常汉字候选的位置;**颜文字混输**排在末尾的 emoji 之后。翻页扩展候选时仍保留在列表末尾。
|
||||
- **快捷短语混入候选**:见下文「实用功能快捷模式」。
|
||||
- **日期时间混入候选**(默认关闭):输入 `rq` / `riqi` / `date`、`sj` / `shijian` / `time`、`xq` / `xingqi` / `week` 时,当前日期、时间或星期出现在首个候选之后,其余格式排在列表末尾。双拼按当前方案换算后的整拼同样能触发,例如小鹤 `uijm` → `shijian`。
|
||||
|
||||
|
||||
@@ -183,8 +183,6 @@ std::vector<WordItem> CandidateQueries::mixed(std::vector<WordItem> candidates,
|
||||
++priority_slot;
|
||||
};
|
||||
insert_leading(english_candidates);
|
||||
insert_leading(emoji_candidates);
|
||||
insert_leading(kaomoji_candidates);
|
||||
|
||||
for (auto *source : {&english_candidates, &emoji_candidates, &kaomoji_candidates})
|
||||
{
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
#include "../../core/data_path.h"
|
||||
#include "../../core/candidate_queries.h"
|
||||
#include "../../core/input_session.h"
|
||||
#include "../../include/metasequoia/session.h"
|
||||
#include "../../user_dictionary/user_dictionary_journal.h"
|
||||
#include "test_directory_cleanup.h"
|
||||
|
||||
@@ -161,21 +163,48 @@ int run_test()
|
||||
require(mixed.set_english_input_options(english_options), "Valid mixed-English options were rejected.");
|
||||
mixed.set_mixed_expressive_options(expressive_options);
|
||||
type(mixed, "ni");
|
||||
const std::vector<std::string> expected_words{"你", "Ni", "😀", "(^_^)", "倪", "Ninja", "😁", "(T_T)"};
|
||||
const std::vector<std::string> expected_words{"你", "Ni", "倪", "Ninja", "😀", "😁", "(^_^)", "(T_T)"};
|
||||
require(mixed.candidates().size() == expected_words.size(),
|
||||
"Mixed candidate deduplication produced the wrong candidate count.");
|
||||
for (std::size_t index = 0; index < expected_words.size(); ++index)
|
||||
{
|
||||
require(mixed.candidates()[index].word == expected_words[index],
|
||||
"Mixed candidates did not keep the Windows-compatible stable ordering.");
|
||||
"Mixed expressive candidates did not stay after local Chinese and English candidates.");
|
||||
}
|
||||
require(sources(mixed) == std::vector<CandidateSource>{CandidateSource::Database,
|
||||
CandidateSource::EnglishDictionary, CandidateSource::Emoji,
|
||||
CandidateSource::Kaomoji, CandidateSource::Database,
|
||||
CandidateSource::EnglishDictionary, CandidateSource::Emoji,
|
||||
CandidateSource::Kaomoji},
|
||||
require(sources(mixed) ==
|
||||
std::vector<CandidateSource>{CandidateSource::Database, CandidateSource::EnglishDictionary,
|
||||
CandidateSource::Database, CandidateSource::EnglishDictionary,
|
||||
CandidateSource::Emoji, CandidateSource::Emoji, CandidateSource::Kaomoji,
|
||||
CandidateSource::Kaomoji},
|
||||
"Mixed candidate sources did not retain their priority groups.");
|
||||
|
||||
metasequoia::SessionOptions public_options;
|
||||
public_options.paths = metasequoia::RuntimePaths::legacy();
|
||||
public_options.english = english_options;
|
||||
public_options.expressive = expressive_options;
|
||||
public_options.helpcode = false;
|
||||
metasequoia::Session public_session(public_options);
|
||||
require(public_session.character('n').handled && public_session.character('i').handled,
|
||||
"The public Session facade did not handle mixed-expression input.");
|
||||
const auto public_candidates = public_session.snapshot().candidates;
|
||||
require(public_candidates.size() == expected_words.size(), "The public Session lost mixed candidates.");
|
||||
for (std::size_t index = 0; index < expected_words.size(); ++index)
|
||||
require(public_candidates[index].word == expected_words[index],
|
||||
"The public Session facade did not keep expressive candidates at the end.");
|
||||
|
||||
// Cloud/AI slots affect English only; all expressive candidates still follow the local list.
|
||||
metasequoia::CandidateQueries queries(metasequoia::RuntimePaths::legacy(), GetXiaoheShuangpinProfile());
|
||||
const auto with_online = queries.mixed(
|
||||
{WordItem("ni", "你", 200), WordItem("ni", "cloud", 0, CandidateSource::CloudSuggestion),
|
||||
WordItem("ni", "ai", 0, CandidateSource::AiSuggestion), WordItem("ni", "倪", 100)},
|
||||
"ni", SchemeType::Quanpin, english_options, expressive_options, false, metasequoia::LocalInputMode::None);
|
||||
const std::vector<std::string> expected_online{"你", "cloud", "ai", "Ni", "倪",
|
||||
"Ninja", "😀", "😁", "(^_^)", "(T_T)"};
|
||||
require(with_online.size() == expected_online.size(), "Online candidates changed mixed deduplication.");
|
||||
for (std::size_t index = 0; index < expected_online.size(); ++index)
|
||||
require(with_online[index].word == expected_online[index],
|
||||
"Cloud or AI candidates moved expressive candidates ahead of local candidates.");
|
||||
|
||||
metasequoia::InputSession below_threshold(SchemeType::Quanpin);
|
||||
require(below_threshold.set_english_input_options(english_options),
|
||||
"Valid mixed-English options were rejected below the expressive threshold.");
|
||||
@@ -194,6 +223,11 @@ int run_test()
|
||||
!has_source(emoji_only, CandidateSource::EnglishDictionary) &&
|
||||
!has_source(emoji_only, CandidateSource::Kaomoji),
|
||||
"The mixed Emoji toggle was not independent.");
|
||||
require(emoji_only.candidates().size() >= 3, "Mixed Emoji candidates were incomplete.");
|
||||
require(emoji_only.candidates()[0].word == "你" && emoji_only.candidates()[1].word == "倪" &&
|
||||
emoji_only.candidates()[2].source == CandidateSource::Emoji &&
|
||||
emoji_only.candidates().back().source == CandidateSource::Emoji,
|
||||
"Mixed Emoji candidates displaced Chinese candidates with mixed English disabled.");
|
||||
|
||||
metasequoia::MixedExpressiveOptions kaomoji_only_options;
|
||||
kaomoji_only_options.kaomoji_candidates = true;
|
||||
@@ -204,6 +238,11 @@ int run_test()
|
||||
!has_source(kaomoji_only, CandidateSource::EnglishDictionary) &&
|
||||
!has_source(kaomoji_only, CandidateSource::Emoji),
|
||||
"The mixed kaomoji toggle was not independent.");
|
||||
require(kaomoji_only.candidates().size() >= 3, "Mixed kaomoji candidates were incomplete.");
|
||||
require(kaomoji_only.candidates()[0].word == "你" && kaomoji_only.candidates()[1].word == "倪" &&
|
||||
kaomoji_only.candidates()[2].source == CandidateSource::Kaomoji &&
|
||||
kaomoji_only.candidates().back().source == CandidateSource::Kaomoji,
|
||||
"Mixed kaomoji candidates displaced Chinese candidates with mixed English disabled.");
|
||||
|
||||
const std::filesystem::path missing_emoji_table = root / "missing-emoji-table";
|
||||
prepare_main_database(missing_emoji_table);
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
|
||||
#include "global/globals.h"
|
||||
#include "ipc/candidate_selection_policy.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
namespace FanyImeIpc
|
||||
{
|
||||
// Session expansion rebuilds local candidates; asynchronous results only live in the UI list.
|
||||
// Quick phrases are rebuilt by the caller, so carry over only asynchronous sources, deduplicating
|
||||
// against the expanded list before applying the same ordering policy as worker callbacks.
|
||||
inline void SetExpandedCandidatesKeepingPagePosition(Global::CandidateUiState &ui, std::vector<WordItem> expanded,
|
||||
const EnglishPlacement &english_placement = {})
|
||||
{
|
||||
for (const auto &candidate : ui.items)
|
||||
{
|
||||
switch (candidate.source)
|
||||
{
|
||||
case CandidateSource::CloudSuggestion:
|
||||
case CandidateSource::AiSuggestion:
|
||||
case CandidateSource::EnglishDictionary:
|
||||
case CandidateSource::Emoji:
|
||||
case CandidateSource::Kaomoji:
|
||||
case CandidateSource::DateTime:
|
||||
if (std::none_of(expanded.begin(), expanded.end(),
|
||||
[&](const WordItem &item) { return item.word == candidate.word; }))
|
||||
expanded.push_back(candidate);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
NormalizeMixedCandidateOrder(expanded, 1, english_placement);
|
||||
const int current_page = ui.page_index;
|
||||
const int current_selection = ui.selected_index_in_page;
|
||||
ui.set_items(std::move(expanded));
|
||||
ui.page_index = current_page;
|
||||
ui.selected_index_in_page = current_selection;
|
||||
}
|
||||
} // namespace FanyImeIpc
|
||||
@@ -313,6 +313,12 @@ std::vector<WordItem> ReplaceSourceAndDeduplicate(std::vector<WordItem> &items,
|
||||
return unique_candidates;
|
||||
}
|
||||
|
||||
// Merge query results; NormalizeMixedCandidateOrder determines their final positions.
|
||||
void AppendCandidates(std::vector<WordItem> &items, std::vector<WordItem> candidates)
|
||||
{
|
||||
items.insert(items.end(), std::make_move_iterator(candidates.begin()), std::make_move_iterator(candidates.end()));
|
||||
}
|
||||
|
||||
// 首个插到 insert_index(最终位置由 NormalizeMixedCandidateOrder 按来源统一摆),其余追加到末尾。
|
||||
void InsertFirstAndAppendRest(std::vector<WordItem> &items, std::vector<WordItem> candidates, size_t insert_index)
|
||||
{
|
||||
@@ -379,16 +385,14 @@ void ApplyMixedCandidates(MixedCandidates::Result result, const std::string &inp
|
||||
{
|
||||
auto emoji = ReplaceSourceAndDeduplicate(items, CandidateSource::Emoji, std::move(result.emoji));
|
||||
changed = changed || !emoji.empty();
|
||||
for (auto &candidate : emoji)
|
||||
items.push_back(std::move(candidate));
|
||||
AppendCandidates(items, std::move(emoji));
|
||||
}
|
||||
|
||||
if (GetConfiguredKaomojiMixedInputEnabled())
|
||||
{
|
||||
auto kaomoji = ReplaceSourceAndDeduplicate(items, CandidateSource::Kaomoji, std::move(result.kaomoji));
|
||||
changed = changed || !kaomoji.empty();
|
||||
for (auto &candidate : kaomoji)
|
||||
items.push_back(std::move(candidate));
|
||||
AppendCandidates(items, std::move(kaomoji));
|
||||
}
|
||||
|
||||
// 全被去重掉、列表也没少东西时,屏幕上的那页就是对的,不必重画。
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
#include <cstdint>
|
||||
#include <utility>
|
||||
#include "ipc.h"
|
||||
#include "ipc/candidate_expansion_policy.h"
|
||||
#include "ipc/candidate_ui_owner.h"
|
||||
#include "ipc/input_key_policy.h"
|
||||
#include "utils/common_utils.h"
|
||||
@@ -528,17 +529,13 @@ bool ExpandCandidatesKeepingPagePosition()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const int current_page = ui.page_index;
|
||||
const int current_selection = ui.selected_index_in_page;
|
||||
auto expanded = g_inputSession->get_candidates();
|
||||
user_dictionary::apply_fixed_positions(
|
||||
user_dictionary::default_user_db_path(), CurrentRankingContextKey(), expanded, true,
|
||||
[](const std::string &key, const std::string &value) { return g_inputSession->find_candidate(key, value); },
|
||||
g_inputSession->has_active_helpcode());
|
||||
PlaceQuickPhrases(expanded, g_inputSession->get_pinyin_sequence_with_cases());
|
||||
ui.set_items(std::move(expanded));
|
||||
ui.page_index = current_page;
|
||||
ui.selected_index_in_page = current_selection;
|
||||
FanyImeIpc::SetExpandedCandidatesKeepingPagePosition(ui, std::move(expanded), CurrentEnglishPlacement(ui.items));
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -44,6 +44,7 @@ set(TEST_SOURCE_FILES
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src/test_candidate_ui_action_policy.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src/test_candidate_translation_policy.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src/test_candidate_ui_state.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src/test_candidate_expansion_policy.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src/test_composition_selection_history.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src/test_candidate_render_sync.cpp"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/src/test_selection_replay_guard.cpp"
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
#include "ipc/candidate_expansion_policy.h"
|
||||
#include "tests/includes/test_framework.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace
|
||||
{
|
||||
std::vector<WordItem> LocalCandidates(int count)
|
||||
{
|
||||
std::vector<WordItem> items;
|
||||
for (int index = 0; index < count; ++index)
|
||||
items.emplace_back("ni", "local-" + std::to_string(index), count - index);
|
||||
return items;
|
||||
}
|
||||
|
||||
std::vector<WordItem> MixedCandidates()
|
||||
{
|
||||
return {WordItem("ni", "Ni", 100, CandidateSource::EnglishDictionary),
|
||||
WordItem("ninja", "Ninja", 50, CandidateSource::EnglishDictionary),
|
||||
WordItem("ni", "😀", 0, CandidateSource::Emoji),
|
||||
WordItem("ni", "😁", 0, CandidateSource::Emoji),
|
||||
WordItem("ni", "(^_^)", 0, CandidateSource::Kaomoji),
|
||||
WordItem("ni", "(T_T)", 0, CandidateSource::Kaomoji),
|
||||
WordItem("date:ymd_dash", "2026-10-08", 0, CandidateSource::DateTime),
|
||||
metasequoia::local_modes::date_time_menu_item("rq"),
|
||||
WordItem("ni", "cloud", 0, CandidateSource::CloudSuggestion),
|
||||
WordItem("ni", "ai", 0, CandidateSource::AiSuggestion)};
|
||||
}
|
||||
|
||||
void RequireExpressiveTail(const std::vector<WordItem> &items)
|
||||
{
|
||||
REQUIRE(items.size() >= 4);
|
||||
const size_t start = items.size() - 4;
|
||||
REQUIRE_EQ(items[start].word, std::string("😀"));
|
||||
REQUIRE_EQ(items[start + 1].word, std::string("😁"));
|
||||
REQUIRE_EQ(items[start + 2].word, std::string("(^_^)"));
|
||||
REQUIRE_EQ(items[start + 3].word, std::string("(T_T)"));
|
||||
}
|
||||
} // namespace
|
||||
|
||||
TEST_CASE(candidate_expansion_preserves_mixed_results_before_entering_a_partial_last_page)
|
||||
{
|
||||
Global::CandidateUiState ui;
|
||||
ui.page_size = 8;
|
||||
auto initial = LocalCandidates(24);
|
||||
const auto mixed = MixedCandidates();
|
||||
initial.insert(initial.end(), mixed.begin(), mixed.end());
|
||||
FanyImeIpc::NormalizeMixedCandidateOrder(initial);
|
||||
ui.set_items(std::move(initial));
|
||||
ui.page_index = 3;
|
||||
ui.selected_index_in_page = 2;
|
||||
REQUIRE(ui.is_next_page_partial_last_page());
|
||||
|
||||
// This is the replacement step used by ExpandCandidatesKeepingPagePosition after the
|
||||
// session has loaded its next batch. The old set_items(expanded) loses every mixed result.
|
||||
FanyImeIpc::SetExpandedCandidatesKeepingPagePosition(ui, LocalCandidates(60));
|
||||
REQUIRE_EQ(ui.page_index, 3);
|
||||
REQUIRE_EQ(ui.selected_index_in_page, 2);
|
||||
REQUIRE_EQ(ui.item_total_count, 70);
|
||||
REQUIRE_EQ(ui.items[0].word, std::string("local-0"));
|
||||
REQUIRE_EQ(ui.items[1].source, CandidateSource::DateTime);
|
||||
REQUIRE(metasequoia::local_modes::is_date_time_menu_item(ui.items[2]));
|
||||
REQUIRE_EQ(ui.items[3].source, CandidateSource::CloudSuggestion);
|
||||
REQUIRE_EQ(ui.items[4].source, CandidateSource::AiSuggestion);
|
||||
REQUIRE_EQ(ui.items[5].source, CandidateSource::EnglishDictionary);
|
||||
RequireExpressiveTail(ui.items);
|
||||
|
||||
// The final page remains reachable and still contains Emoji followed by kaomoji.
|
||||
while (ui.has_next_page())
|
||||
++ui.page_index;
|
||||
REQUIRE_EQ(ui.page_index, 8);
|
||||
REQUIRE_EQ(ui.current_page_count(), 6);
|
||||
REQUIRE_EQ(ui.items[ui.item_total_count - 1].source, CandidateSource::Kaomoji);
|
||||
}
|
||||
|
||||
TEST_CASE(candidate_expansion_refills_the_current_last_page_and_keeps_learned_english_placement)
|
||||
{
|
||||
Global::CandidateUiState ui;
|
||||
ui.page_size = 8;
|
||||
auto initial = LocalCandidates(24);
|
||||
const auto mixed = MixedCandidates();
|
||||
initial.insert(initial.end(), mixed.begin(), mixed.end());
|
||||
ui.set_items(std::move(initial));
|
||||
ui.page_index = 4;
|
||||
ui.selected_index_in_page = 1;
|
||||
REQUIRE(!ui.has_next_page());
|
||||
REQUIRE(!ui.is_current_page_full());
|
||||
FanyImeIpc::EnglishPlacement placement;
|
||||
placement.slot = 6;
|
||||
placement.input = "ni";
|
||||
placement.page_size = 8;
|
||||
|
||||
FanyImeIpc::SetExpandedCandidatesKeepingPagePosition(ui, LocalCandidates(60), placement);
|
||||
REQUIRE_EQ(ui.page_index, 4);
|
||||
REQUIRE_EQ(ui.selected_index_in_page, 1);
|
||||
REQUIRE(ui.is_current_page_full());
|
||||
const auto english_index = FanyImeIpc::SlottedEnglishIndex(ui.items);
|
||||
REQUIRE(english_index.has_value());
|
||||
REQUIRE_EQ(*english_index, size_t{6});
|
||||
RequireExpressiveTail(ui.items);
|
||||
}
|
||||
|
||||
TEST_CASE(candidate_expansion_deduplicates_results_and_preserves_fixed_english_metadata)
|
||||
{
|
||||
Global::CandidateUiState ui;
|
||||
auto initial = LocalCandidates(24);
|
||||
auto mixed = MixedCandidates();
|
||||
mixed.front().fixed_position = 1;
|
||||
mixed.front().corrected_from = "metadata";
|
||||
initial.insert(initial.end(), mixed.begin(), mixed.end());
|
||||
ui.set_items(std::move(initial));
|
||||
|
||||
auto expanded = LocalCandidates(60);
|
||||
expanded.push_back(mixed[1]);
|
||||
expanded.emplace_back("ni", "cloud", 1);
|
||||
expanded.emplace_back("ni", "local-quick-phrase", 0, CandidateSource::QuickPhrase);
|
||||
FanyImeIpc::SetExpandedCandidatesKeepingPagePosition(ui, expanded);
|
||||
REQUIRE_EQ(ui.item_total_count, 71);
|
||||
REQUIRE_EQ(ui.items[0].word, std::string("Ni"));
|
||||
REQUIRE_EQ(ui.items[0].fixed_position, 1);
|
||||
REQUIRE_EQ(ui.items[0].corrected_from, std::string("metadata"));
|
||||
REQUIRE_EQ(
|
||||
std::count_if(ui.items.begin(), ui.items.end(), [](const WordItem &item) { return item.word == "cloud"; }), 1);
|
||||
RequireExpressiveTail(ui.items);
|
||||
|
||||
FanyImeIpc::SetExpandedCandidatesKeepingPagePosition(ui, std::move(expanded));
|
||||
REQUIRE_EQ(ui.item_total_count, 71);
|
||||
RequireExpressiveTail(ui.items);
|
||||
}
|
||||
|
||||
TEST_CASE(candidate_expansion_rebuilds_local_candidates_without_carrying_stale_fallbacks_or_phrases)
|
||||
{
|
||||
Global::CandidateUiState ui;
|
||||
ui.set_items({WordItem("ni", "old-local", 1), WordItem("ni", "old-fallback", 0, CandidateSource::Fallback),
|
||||
WordItem("ni", "old-phrase", 0, CandidateSource::QuickPhrase)});
|
||||
auto expanded = LocalCandidates(60);
|
||||
FanyImeIpc::SetExpandedCandidatesKeepingPagePosition(ui, expanded);
|
||||
REQUIRE_EQ(ui.item_total_count, 60);
|
||||
for (size_t index = 0; index < expanded.size(); ++index)
|
||||
REQUIRE_EQ(ui.items[index].word, expanded[index].word);
|
||||
}
|
||||
@@ -429,7 +429,7 @@ TEST_CASE(FixedEnglishCandidateKeepsItsMixedCandidatePosition)
|
||||
REQUIRE_EQ(items[2].word, std::string("GitHub"));
|
||||
}
|
||||
|
||||
TEST_CASE(EmojiMixedCandidateFollowsEnglishAndShiftsWithCloudAndAi)
|
||||
TEST_CASE(EmojiMixedCandidatesStayAtTheEndWithCloudAndAi)
|
||||
{
|
||||
const auto local = [](std::string word) { return WordItem("ni", std::move(word), 100); };
|
||||
const auto english = [] { return WordItem("ni", "nice", 1, CandidateSource::EnglishDictionary); };
|
||||
@@ -470,7 +470,7 @@ TEST_CASE(EmojiMixedCandidateFollowsEnglishAndShiftsWithCloudAndAi)
|
||||
REQUIRE_EQ(items[4].source, CandidateSource::Emoji);
|
||||
}
|
||||
|
||||
TEST_CASE(KaomojiMixedCandidateSitsRightAfterEmoji)
|
||||
TEST_CASE(KaomojiMixedCandidatesFollowEmojiAtTheEnd)
|
||||
{
|
||||
const auto local = [](std::string word) { return WordItem("ni", std::move(word), 100); };
|
||||
const auto english = [] { return WordItem("ni", "nice", 1, CandidateSource::EnglishDictionary); };
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
<div class="section-header">
|
||||
<div>
|
||||
<div class="section-title">emoji 混输</div>
|
||||
<div class="input-setting-description">在候选项中加入匹配的 emoji(位于英文候选之后;云候选与 AI 联想会使其相应顺移)</div>
|
||||
<div class="input-setting-description">在候选项中加入匹配的 emoji(排在候选项列表最后,不挤占正常汉字候选的位置)</div>
|
||||
</div>
|
||||
<div class="ftb-toggle-btn active" id="emojiMixedInputToggleBtn" role="switch" aria-checked="true" tabindex="0">
|
||||
<div class="ftb-toggle-knob"></div>
|
||||
@@ -69,7 +69,7 @@
|
||||
<div class="section-header">
|
||||
<div>
|
||||
<div class="section-title">颜文字混输</div>
|
||||
<div class="input-setting-description">在候选项中加入匹配的颜文字(排在 emoji 之后)</div>
|
||||
<div class="input-setting-description">在候选项中加入匹配的颜文字(排在列表末尾的 emoji 之后)</div>
|
||||
</div>
|
||||
<div class="ftb-toggle-btn active" id="kaomojiMixedInputToggleBtn" role="switch" aria-checked="true" tabindex="0">
|
||||
<div class="ftb-toggle-knob"></div>
|
||||
|
||||
Reference in new issue
Block a user