/* * SPDX-FileCopyrightText: 2017-2017 CSSlayer * * SPDX-License-Identifier: LGPL-2.1-or-later * */ #include "pinyin.h" // Use relative path so we don't need import export target. // We want to keep cloudpinyin logic but don't call it. #include "../../modules/cloudpinyin/cloudpinyin_public.h" #include "config.h" #include "customphrase.h" #include "notifications_public.h" #include "pinyincandidate.h" #include "pinyinhelper_public.h" #include "punctuation_public.h" #include "spell_public.h" #include "workerthread.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #ifdef FCITX_HAS_LUA #include #endif namespace fcitx { namespace { FCITX_DEFINE_LOG_CATEGORY(pinyin, "pinyin"); #define PINYIN_DEBUG() FCITX_LOGC(pinyin, Debug) #define PINYIN_ERROR() FCITX_LOGC(pinyin, Error) template std::unique_ptr predictCandidateList(PinyinEngine *engine, const std::vector &words) { if (words.empty()) { return nullptr; } auto candidateList = std::make_unique(); for (const auto &word : words) { if constexpr (std::is_same_v) { candidateList->append(engine, word); } else if constexpr (std::is_same_v< T, std::pair>) { if (word.second == libime::PinyinPredictionSource::Model) { candidateList->append(engine, word.first); } else if (word.second == libime::PinyinPredictionSource::Dictionary) { candidateList->append( engine, word.first); } } } candidateList->setSelectionKey(engine->selectionKeys()); candidateList->setPageSize(*engine->config().pageSize); if (!candidateList->empty()) { candidateList->setGlobalCursorIndex(0); } return candidateList; } std::tuple englishNess(const std::string &input, bool sp) { const auto pys = stringutils::split(input, " "); constexpr int fullWeight = -2; constexpr int shortWeight = 3; constexpr int invalidWeight = 6; constexpr int defaultWeight = shortWeight; int weight = 0; if (std::any_of(input.begin(), input.end(), charutils::isupper)) { return {true, std::max(1, (invalidWeight * pys.size() + 7) / 10)}; } for (const auto &py : pys) { if (sp) { if (py.size() == 2) { weight += fullWeight / 2; } else { weight += invalidWeight; } } else { if (py == "ng") { weight += fullWeight; } else { auto firstChr = py[0]; if (firstChr == '\'') { return {false, 0}; } if (firstChr == 'i' || firstChr == 'u' || firstChr == 'v') { weight += invalidWeight; } else if (py.size() <= 2) { weight += shortWeight; } else if (py.find_first_of("aeiou") != std::string::npos) { weight += fullWeight; } else { weight += defaultWeight; } } } } if (weight < 0) { return {false, 0}; } return {false, (weight + 7) / 10}; } bool isStroke(const std::string &input) { static const std::unordered_set py{'h', 'p', 's', 'z', 'n'}; return std::all_of(input.begin(), input.end(), [](char c) { return py.count(c); }); } std::string getEncodedPinyin(const libime::SentenceResult &result) { std::string encodedPinyin; bool validPinyin = std::all_of( result.sentence().begin(), result.sentence().end(), [](const libime::LatticeNode *node) { if (node->word().empty()) { return true; } const auto *pinyinNode = static_cast(node); return !pinyinNode->encodedPinyin().empty() && pinyinNode->encodedPinyin().size() % 2 == 0; }); if (validPinyin) { for (const auto *node : result.sentence()) { const auto *pinyinNode = static_cast(node); encodedPinyin.insert(encodedPinyin.end(), pinyinNode->encodedPinyin().begin(), pinyinNode->encodedPinyin().end()); } } return encodedPinyin; } } // namespace PinyinState::PinyinState(PinyinEngine *engine) : context_(engine->ime()) { context_.setMaxSentenceLength(35); } void PinyinEngine::initPredict(InputContext *inputContext) { auto *state = inputContext->propertyFor(&factory_); // clear state no matter what. state->predictWords_.reset(); auto &context = state->context_; auto lmState = context.state(); auto selected = context.selectedWords(); if (*config_.keepCurrentContext) { context.appendContextWords(selected); } if (selected.empty()) { return; } if (!*config_.predictionEnabled) { return; } const auto selectPinyin = context.selectedWordsWithPinyin(); const std::string lastEncodedPinyin = !selectPinyin.empty() ? selectPinyin.back().second : std::string(); auto words = prediction_.predict(lmState, selected, lastEncodedPinyin, *config_.predictionSize); if (auto candidateList = predictCandidateList(this, words)) { auto &inputPanel = inputContext->inputPanel(); state->predictWords_ = std::move(selected); inputPanel.setCandidateList(std::move(candidateList)); } else { return; } inputContext->updatePreedit(); inputContext->updateUserInterface(UserInterfaceComponent::InputPanel); } void PinyinEngine::updatePredict(InputContext *inputContext) { inputContext->inputPanel().reset(); auto *state = inputContext->propertyFor(&factory_); assert(state->predictWords_.has_value()); if (*config_.keepCurrentContext) { state->context_.setContextWords(*state->predictWords_); } auto words = prediction_.predict(*state->predictWords_, *config_.predictionSize); if (auto candidateList = predictCandidateList(this, words)) { auto &inputPanel = inputContext->inputPanel(); inputPanel.setCandidateList(std::move(candidateList)); } else { // Clear if we can't do predict. // This help other code to detect whether we are in predict. state->predictWords_.reset(); } inputContext->updatePreedit(); inputContext->updateUserInterface(UserInterfaceComponent::InputPanel); } #ifdef FCITX_HAS_LUA std::vector PinyinEngine::luaCandidateTrigger(InputContext *ic, const std::string &candidateString) { std::vector result; RawConfig arg; arg.setValue(candidateString); auto ret = imeapi()->call( ic, "candidateTrigger", arg); const auto *length = ret.valueByPath("Length"); try { if (length) { auto n = std::stoi(*length); for (int i = 0; i < n; i++) { const auto *candidate = ret.valueByPath(std::to_string(i)); if (candidate && !candidate->empty()) { result.push_back(*candidate); } } } } catch (...) { } return result; } #endif std::pair PinyinEngine::preedit(InputContext *inputContext) const { auto *state = inputContext->propertyFor(&factory_); PreeditMode mode = inputContext->capabilityFlags().test(CapabilityFlag::Preedit) ? *config_.preeditMode : PreeditMode::No; // Use const ref to avoid accidentally change anything. const auto &context = state->context_; auto preeditWithCursor = context.preeditWithCursor(); // client preedit can be empty/pinyin/preview depends on config Text clientPreedit; Text preedit; switch (mode) { case PreeditMode::ComposingPinyin: if (*config_.preeditCursorPositionAtBeginning) { clientPreedit.append( preeditWithCursor.first.substr(0, preeditWithCursor.second), {TextFormatFlag::HighLight, TextFormatFlag::Underline}); clientPreedit.append( preeditWithCursor.first.substr(preeditWithCursor.second), TextFormatFlag::Underline); clientPreedit.setCursor(0); } else { clientPreedit.append(preeditWithCursor.first, TextFormatFlag::Underline); clientPreedit.setCursor(preeditWithCursor.second); } break; case PreeditMode::CommitPreview: clientPreedit.append(context.sentence(), TextFormatFlag::Underline); if (*config_.preeditCursorPositionAtBeginning) { clientPreedit.setCursor(0); } else { clientPreedit.setCursor(context.selectedSentence().size()); } [[fallthrough]]; case PreeditMode::No: preedit.append(preeditWithCursor.first); preedit.setCursor(preeditWithCursor.second); break; } return {std::move(clientPreedit), std::move(preedit)}; } std::string PinyinEngine::preeditCommitString(InputContext *inputContext) const { auto *state = inputContext->propertyFor(&factory_); // Use const ref to avoid accidentally change anything. const auto &context = state->context_; const auto &userInput = context.userInput(); const auto selectedLength = context.selectedLength(); return context.selectedSentence() + userInput.substr(selectedLength, userInput.length() - selectedLength); } void PinyinEngine::updatePreedit(InputContext *inputContext) const { auto &inputPanel = inputContext->inputPanel(); auto [clientPreedit, preedit] = this->preedit(inputContext); inputPanel.setClientPreedit(clientPreedit); inputPanel.setPreedit(preedit); } void PinyinEngine::updatePuncPreedit(InputContext *inputContext) const { auto candidateList = inputContext->inputPanel().candidateList(); if (inputContext->capabilityFlags().test(CapabilityFlag::Preedit)) { if (candidateList->cursorIndex() >= 0) { Text preedit; const auto &candidate = candidateList->candidate(candidateList->cursorIndex()); if (const auto *puncCandidate = dynamic_cast( &candidate)) { preedit.append(puncCandidate->word()); } preedit.setCursor(preedit.textLength()); inputContext->inputPanel().setClientPreedit(preedit); } inputContext->updatePreedit(); } } void PinyinEngine::updatePuncCandidate( InputContext *inputContext, const std::string &original, const std::vector &candidates) const { inputContext->inputPanel().reset(); auto *state = inputContext->propertyFor(&factory_); auto puncCandidateList = std::make_unique(); puncCandidateList->setPageSize(*config_.pageSize); puncCandidateList->setCursorPositionAfterPaging( CursorPositionAfterPaging::ResetToFirst); for (const auto &result : candidates) { puncCandidateList->append( this, result, original == result); } puncCandidateList->setCursorIncludeUnselected(false); puncCandidateList->setCursorKeepInSamePage(false); puncCandidateList->setGlobalCursorIndex(0); puncCandidateList->setSelectionKey(selectionKeys_); state->mode_ = PinyinMode::Punctuation; inputContext->inputPanel().setCandidateList(std::move(puncCandidateList)); updatePuncPreedit(inputContext); inputContext->updateUserInterface(UserInterfaceComponent::InputPanel); } void PinyinEngine::updateUI(InputContext *inputContext) { inputContext->inputPanel().reset(); auto *state = inputContext->propertyFor(&factory_); // Use const ref to avoid accidentally change anything. const auto &context = state->context_; if (context.selected()) { auto sentence = context.sentence(); if (!inputContext->capabilityFlags().testAny( CapabilityFlag::PasswordOrSensitive)) { state->context_.learn(); } inputContext->commitString(sentence); inputContext->updatePreedit(); inputContext->updateUserInterface(UserInterfaceComponent::InputPanel); initPredict(inputContext); state->context_.clear(); return; } do { if (context.userInput().empty()) { break; } // Update Preedit. updatePreedit(inputContext); auto &inputPanel = inputContext->inputPanel(); // Update candidate const auto &pinyinCandidates = context.candidatesToCursor(); if (pinyinCandidates.empty()) { break; } // Setup candidate list. auto candidateList = std::make_unique(); candidateList->setPageSize(*config_.pageSize); candidateList->setCursorPositionAfterPaging( CursorPositionAfterPaging::ResetToFirst); // Precompute some values. const auto selectedLength = context.selectedLength(); const auto selectedSentence = context.selectedSentence(); const bool fullResult = (context.cursor() == context.size() || context.cursor() == context.selectedLength()); const auto pyLength = context.cursor() > selectedLength ? context.cursor() - selectedLength : std::string::npos; const auto pyBeforeCursor = context.userInput().substr(selectedLength, pyLength); std::vector> candidates; // Since symbol is by default, add some extra size for reservation. candidates.reserve(pinyinCandidates.size() + (*config_.pageSize * 2)); std::unordered_set customCandidateSet; /// Create custom phrase candidate {{{ do { const auto *results = customPhrase_.lookup(pyBeforeCursor); if (!results) { break; } for (const auto &result : *results) { if (result.order() <= 0) { continue; } auto phrase = result.evaluate([this, inputContext](std::string_view key) { return evaluateCustomPhrase(inputContext, key); }); if (customCandidateSet.contains(phrase)) { continue; } customCandidateSet.insert(phrase); std::string customPhrase = result.isDynamic() ? result.value() : phrase; candidates.push_back( std::make_unique( this, pyBeforeCursor.size(), result.order() - 1, std::move(phrase), std::move(customPhrase))); } } while (0); /// }}} /// Create cloud candidate. {{{ std::optional cloud; if (*config_.cloudPinyinEnabled && cloudpinyin() && !inputContext->capabilityFlags().testAny( CapabilityFlag::PasswordOrSensitive) && fullResult) { using namespace std::placeholders; auto fullPinyin = context.useShuangpin() ? context.candidateFullPinyin(0) : context.userInput().substr(selectedLength); auto cand = std::make_unique( this, fullPinyin, selectedSentence, inputContext, [this](InputContext *ic, const std::string &selected, const std::string &word) { cloudPinyinSelected(ic, selected, word); }, *config_.cloudPinyinIndex - 1); if (!cand->filled() || (!cand->word().empty() && !customCandidateSet.contains(cand->word()) && !context.candidatesToCursorSet().contains(cand->word()))) { customCandidateSet.insert(cand->word()); candidates.push_back(std::move(cand)); cloud = std::prev(candidates.end()); } } /// }}} /// Create spell candidate {{{ auto [parsedPy, parsedPyCursor] = state->context_.preeditWithCursor( libime::PinyinPreeditMode::RawText); if (*config_.spellEnabled && spell() && parsedPyCursor >= selectedSentence.size() && selectedLength <= context.cursor()) { auto [hasUpper, engNess] = englishNess(parsedPy, context.useShuangpin()); if (engNess) { parsedPyCursor -= selectedSentence.length(); parsedPy = parsedPy.substr( selectedSentence.size(), parsedPyCursor > selectedSentence.length() ? parsedPyCursor - selectedSentence.length() : std::string::npos); auto results = spell()->call( "en", SpellProvider::Custom, pyBeforeCursor, engNess); // Our hint doesn't work well with mixed case, so, always put a // word as is. if (hasUpper && !pyBeforeCursor.empty()) { if (std::find(results.begin(), results.end(), pyBeforeCursor) == results.end()) { if (!charutils::isupper(pyBeforeCursor[0])) { results.insert(results.begin(), pyBeforeCursor); } else { results.push_back(pyBeforeCursor); } } } int position = hasUpper ? 0 : 1; for (const auto &result : results) { if (customCandidateSet.contains(result)) { continue; } candidates.push_back(std::make_unique( this, result, pyBeforeCursor.size(), position++)); } } } /// }}} /// Create stroke candidate {{{ if (*config_.strokeCandidateEnabled && pinyinhelper() && context.selectedLength() == 0 && fullResult && isStroke(context.userInput())) { int limit = (context.userInput().size() + 4) / 5; limit = std::min(limit, 3); auto results = pinyinhelper()->call( context.userInput(), limit); int strokeCandsPos = *config_.pageSize - static_cast(results.size()); if (strokeCandsPos < 0) { strokeCandsPos = *config_.pageSize - 1; } for (auto &result : results) { utf8::getChar(result.first); auto py = pinyinhelper()->call( utf8::getChar(result.first)); auto pystr = stringutils::join(py, " "); candidates.push_back(std::make_unique( this, result.first, pystr, context.userInput().size(), strokeCandsPos++)); } } /// }}} const auto candidateCompare = [](const auto &lhs, const auto &rhs) { return lhs->order() < rhs->order(); }; // We expect stable sort here. // Real pinyin candidate is always in order, so we have small N here. std::ranges::stable_sort(candidates, candidateCompare); // Save the middle point for inplace_merge. const size_t middle = candidates.size(); for (size_t idx = 0; idx < pinyinCandidates.size(); ++idx) { const auto &candidate = pinyinCandidates[idx]; auto candidateString = candidate.toString(); if (customCandidateSet.contains(candidateString)) { continue; } candidates.push_back(std::make_unique( this, inputContext, Text(std::move(candidateString)), candidate.sentence().back()->to()->index(), idx)); } std::ranges::inplace_merge(candidates, std::next(candidates.begin(), middle), candidateCompare); // Apply the candidate to candidate generation. for (auto &candidatePtr : candidates) { // Candidate pointer shall still valid here. auto *candidate = candidatePtr.get(); auto candidateString = candidate->text().toString(); std::vector luaExtraCandidates; #ifdef FCITX_HAS_LUA // Only trigger lua for top N candidates to avoid too much overhead. if (candidate->order() < std::max(*config_.nbest, *config_.pageSize) && imeapi()) { luaExtraCandidates = luaCandidateTrigger(inputContext, candidateString); } #endif // use candidateString before it is moved. const std::vector *symbols = nullptr; if (*config_.symbolsEnabled) { symbols = symbols_.lookup(candidateString); } candidateList->append(std::move(candidatePtr)); for (auto &extraCandidate : luaExtraCandidates) { candidateList->append( this, candidate->selectLength(), std::move(extraCandidate)); } if (symbols) { std::string encodedPinyin; if (candidate->isPinyinCandidate()) { encodedPinyin = getEncodedPinyin(pinyinCandidates[candidate->order()]); } for (const auto &symbol : *symbols) { const bool isFull = candidate->selectLength() == pyBeforeCursor.size(); candidateList->append( this, symbol, encodedPinyin, candidate->selectLength(), isFull); } } } candidateList->setSelectionKey(selectionKeys_); if (!candidateList->empty()) { candidateList->setGlobalCursorIndex(0); } candidateList->setActionableImpl( std::make_unique(this, inputContext)); inputPanel.setCandidateList(std::move(candidateList)); } while (0); inputContext->updatePreedit(); inputContext->updateUserInterface(UserInterfaceComponent::InputPanel); } std::string PinyinEngine::evaluateCustomPhrase(InputContext *inputContext, std::string_view key) { FCITX_UNUSED(inputContext); auto result = CustomPhrase::builtinEvaluator(key); if (!result.empty()) { return result; } #ifdef FCITX_HAS_LUA if (key.starts_with("lua:")) { RawConfig config; auto ret = imeapi()->call( inputContext, std::string(key.substr(4)), config); if (!ret.value().empty()) { return ret.value(); } } #endif return ""; } PinyinEngine::PinyinEngine(Instance *instance) : instance_(instance), factory_([this](InputContext &) { return new PinyinState(this); }), worker_(instance->eventDispatcher()) { ime_ = std::make_unique( std::make_unique(), std::make_unique( libime::DefaultLanguageModelResolver::instance() .languageModelFileForLanguage("zh_CN"))); const auto &standardPath = StandardPaths::global(); auto systemDictFile = standardPath.open(StandardPathsType::Data, "libime/sc.dict"); if (systemDictFile.isValid()) { IFDStreamBuf buffer(systemDictFile.fd()); std::istream in(&buffer); ime_->dict()->load(libime::PinyinDictionary::SystemDict, in, libime::PinyinDictFormat::Binary); } else { ime_->dict()->load(libime::PinyinDictionary::SystemDict, LIBIME_INSTALL_PKGDATADIR "/sc.dict", libime::PinyinDictFormat::Binary); } prediction_.setUserLanguageModel(ime_->model()); prediction_.setPinyinDictionary(ime_->dict()); do { auto file = standardPath.open(StandardPathsType::PkgData, "pinyin/user.dict"); if (!file.isValid()) { break; } try { IFDStreamBuf buffer(file.fd()); std::istream in(&buffer); ime_->dict()->load(libime::PinyinDictionary::UserDict, in, libime::PinyinDictFormat::Binary); } catch (const std::exception &e) { PINYIN_ERROR() << "Failed to load pinyin dict: " << e.what(); } } while (0); do { auto file = standardPath.open(StandardPathsType::PkgData, "pinyin/user.history", StandardPathsMode::User); try { IFDStreamBuf buffer(file.fd()); std::istream in(&buffer); ime_->model()->load(in); } catch (const std::exception &e) { PINYIN_ERROR() << "Failed to load pinyin history: " << e.what(); } } while (0); ime_->setScoreFilter(1); loadBuiltInDict(); reloadConfig(); loadExtraDict(); loadCustomPhrase(); instance_->inputContextManager().registerProperty("pinyinState", &factory_); KeySym syms[] = { FcitxKey_1, FcitxKey_2, FcitxKey_3, FcitxKey_4, FcitxKey_5, FcitxKey_6, FcitxKey_7, FcitxKey_8, FcitxKey_9, FcitxKey_0, }; KeyStates states; for (auto sym : syms) { selectionKeys_.emplace_back(sym, states); } KeySym numpadsyms[] = { FcitxKey_KP_1, FcitxKey_KP_2, FcitxKey_KP_3, FcitxKey_KP_4, FcitxKey_KP_5, FcitxKey_KP_6, FcitxKey_KP_7, FcitxKey_KP_8, FcitxKey_KP_9, FcitxKey_KP_0, }; KeyStates numpadStates; for (auto sym : numpadsyms) { numpadSelectionKeys_.emplace_back(sym, numpadStates); } predictionAction_.setShortText(*config_.predictionEnabled ? _("Prediction Enabled") : _("Prediction Disabled")); predictionAction_.setLongText(_("Show prediction words")); predictionAction_.setIcon(*config_.predictionEnabled ? "fcitx-remind-active" : "fcitx-remind-inactive"); predictionAction_.connect( [this](InputContext *ic) { config_.predictionEnabled.setValue(!(*config_.predictionEnabled)); predictionAction_.setShortText(*config_.predictionEnabled ? _("Prediction Enabled") : _("Prediction Disabled")); predictionAction_.setIcon(*config_.predictionEnabled ? "fcitx-remind-active" : "fcitx-remind-inactive"); predictionAction_.update(ic); }); instance_->userInterfaceManager().registerAction("pinyin-prediction", &predictionAction_); event_ = instance_->watchEvent( EventType::InputContextKeyEvent, EventWatcherPhase::PreInputMethod, [this](Event &event) { auto &keyEvent = static_cast(event); auto *inputContext = keyEvent.inputContext(); const auto *entry = instance_->inputMethodEntry(inputContext); if (!entry || entry->addon() != "pinyin") { return; } handle2nd3rdSelection(keyEvent); }); pyConfig_.shuangpinProfile.annotation().setHidden(true); pyConfig_.showShuangpinMode.annotation().setHidden(true); pyConfig_.fuzzyConfig->partialSp.annotation().setHidden(true); deferredPreload_ = instance_->eventLoop().addDeferEvent([this]( EventSource *) { bool hasCloudPinyin = cloudpinyin() != nullptr; for (auto *configPtr : {&config_, &pyConfig_}) { configPtr->cloudPinyinEnabled.annotation().setHidden( !hasCloudPinyin); configPtr->cloudPinyinIndex.annotation().setHidden(!hasCloudPinyin); configPtr->cloudPinyinAnimation.annotation().setHidden( !hasCloudPinyin); configPtr->keepCloudPinyinPlaceHolder.annotation().setHidden( !hasCloudPinyin); configPtr->cloudpinyin.setHidden(!hasCloudPinyin); } deferredPreload_.reset(); return true; }); } PinyinEngine::~PinyinEngine() {} void PinyinEngine::loadSymbols(const UnixFD &file) { if (!file.isValid()) { return; } IFDStreamBuf buffer(file.fd()); std::istream in(&buffer); try { PINYIN_DEBUG() << "Loading symbol dict."; symbols_.load(in); } catch (const std::exception &e) { PINYIN_ERROR() << "Failed to load symbol dict: " << e.what(); } } void PinyinEngine::loadDict(const std::string &fullPath, std::list> &taskTokens) { if (fullPath.empty()) { return; } ime_->dict()->addEmptyDict(); PINYIN_DEBUG() << "Loading pinyin dict " << fullPath; std::packaged_task task([fullPath]() { std::ifstream in(fullPath, std::ios::in | std::ios::binary); auto trie = libime::PinyinDictionary::load( in, libime::PinyinDictFormat::Binary); return trie; }); taskTokens.push_back(worker_.addTask( std::move(task), [this, index = ime_->dict()->dictSize() - 1, fullPath]( std::shared_future &future) { try { PINYIN_DEBUG() << "Load pinyin dict " << fullPath << " finished."; ime_->dict()->setTrie(index, future.get()); } catch (const std::exception &e) { PINYIN_ERROR() << "Failed to load pinyin dict " << fullPath << ": " << e.what(); } })); } void PinyinEngine::loadBuiltInDict() { const auto &standardPath = StandardPaths::global(); { auto file = standardPath.open(StandardPathsType::PkgData, "pinyin/symbols"); loadSymbols(file); } { auto file = standardPath.locate(StandardPathsType::PkgData, "pinyin/chaizi.dict"); loadDict(file, persistentTask_); } { auto file = standardPath.locate(StandardPathsType::Data, "libime/extb.dict"); // Try again with absolute libime path. if (file.empty()) { file = standardPath.locate(StandardPathsType::Data, LIBIME_INSTALL_PKGDATADIR "/extb.dict"); } loadDict(file, persistentTask_); } if (ime_->dict()->dictSize() != libime::TrieDictionary::UserDict + 1 + NumBuiltInDict) { throw std::runtime_error("Failed to load built-in dictionary"); } } void PinyinEngine::loadExtraDict() { const auto &standardPath = StandardPaths::global(); auto files = standardPath.locate(StandardPathsType::PkgData, "pinyin/dictionaries", pathfilter::extension(".dict")); std::unordered_set disableFilesSet; for (const auto &item : standardPath.locate(StandardPathsType::PkgData, "pinyin/dictionaries", pathfilter::extension(".disable")) | std::views::transform( [](const auto &item) { return item.first.stem(); })) { disableFilesSet.insert(item); } FCITX_ASSERT(ime_->dict()->dictSize() >= libime::TrieDictionary::UserDict + NumBuiltInDict + 1) << "Dict size: " << ime_->dict()->dictSize(); tasks_.clear(); ime_->dict()->removeFrom(libime::TrieDictionary::UserDict + NumBuiltInDict + 1); for (auto &file : files) { if (disableFilesSet.contains(file.first)) { PINYIN_DEBUG() << "Dictionary: " << file.first << " is disabled."; continue; } PINYIN_DEBUG() << "Loading extra dictionary: " << file.first; loadDict(file.second, tasks_); } } void PinyinEngine::loadCustomPhrase() { const auto &standardPath = StandardPaths::global(); auto file = standardPath.open(StandardPathsType::PkgData, "pinyin/customphrase"); if (!file.isValid()) { customPhrase_.clear(); return; } try { IFDStreamBuf buffer(file.fd()); std::istream in(&buffer); customPhrase_.load(in, true); } catch (const std::exception &e) { PINYIN_ERROR() << e.what(); } } void PinyinEngine::populateConfig() { if (*config_.firstRun) { config_.firstRun.setValue(false); safeSaveAsIni(config_, "conf/pinyin.conf"); deferEvent_ = instance_->eventLoop().addDeferEvent([this]( EventSource *) { if (cloudpinyin() && !*config_.cloudPinyinEnabled && notifications()) { const auto &key = cloudpinyin()->call(); std::string msg; if (key.empty()) { msg = _("Do you want to enable cloudpinyin now for better " "prediction? You can always toggle it later in " "configuration."); } else { msg = _( "Do you want to enable cloudpinyin now for better " "prediction? You can always toggle it later in " "configuration or by pressing {}.", Key::keyListToString(key, KeyStringFormat::Localized)); } std::vector actions = {"yes", _("Yes"), "no", _("No")}; notifications()->call( _("Pinyin"), 0, "fcitx-pinyin", _("Enable Cloudpinyin"), msg, actions, -1, [this](const std::string &action) { if (action == "yes") { config_.cloudPinyinEnabled.setValue(true); save(); } }, nullptr); } deferEvent_.reset(); return true; }); } ime_->setNBest(*config_.nbest); ime_->setWordCandidateLimit(*config_.wordCandidateLimit); ime_->setPartialLongWordLimit(*config_.longWordLimit); ime_->setPreeditMode(*config_.showActualPinyinInPreedit ? libime::PinyinPreeditMode::Pinyin : libime::PinyinPreeditMode::RawText); // Setup fuzzy flags. libime::PinyinFuzzyFlags flags; const auto &fuzzyConfig = *config_.fuzzyConfig; #define SET_FUZZY_FLAG(VAR, ENUM) \ if (*fuzzyConfig.VAR) { \ flags |= libime::PinyinFuzzyFlag::ENUM; \ } SET_FUZZY_FLAG(ue, VE_UE) SET_FUZZY_FLAG(commonTypo, CommonTypo) SET_FUZZY_FLAG(commonTypo, AdvancedTypo) SET_FUZZY_FLAG(inner, Inner) SET_FUZZY_FLAG(innerShort, InnerShort) SET_FUZZY_FLAG(partialFinal, PartialFinal) SET_FUZZY_FLAG(partialSp, PartialSp) SET_FUZZY_FLAG(v, V_U) SET_FUZZY_FLAG(an, AN_ANG) SET_FUZZY_FLAG(en, EN_ENG) SET_FUZZY_FLAG(ian, IAN_IANG) SET_FUZZY_FLAG(in, IN_ING) SET_FUZZY_FLAG(ou, U_OU) SET_FUZZY_FLAG(uan, UAN_UANG) SET_FUZZY_FLAG(c, C_CH) SET_FUZZY_FLAG(f, F_H) SET_FUZZY_FLAG(l, L_N) SET_FUZZY_FLAG(r, L_R) SET_FUZZY_FLAG(s, S_SH) SET_FUZZY_FLAG(z, Z_ZH) { std::shared_ptr correctionProfile; switch (*fuzzyConfig.correction) { case CorrectionLayout::None: break; case CorrectionLayout::Qwerty: correctionProfile = std::make_shared( libime::BuiltinPinyinCorrectionProfile::Qwerty); break; } ime_->setCorrectionProfile(std::move(correctionProfile)); } if (ime_->correctionProfile()) { flags |= libime::PinyinFuzzyFlag::Correction; } ime_->setFuzzyFlags(flags); if (*config_.shuangpinProfile == ShuangpinProfileEnum::Custom) { auto file = StandardPaths::global().open(StandardPathsType::PkgConfig, "pinyin/sp.dat"); if (file.isValid()) { try { IFDStreamBuf buffer(file.fd()); std::istream in(&buffer); ime_->setShuangpinProfile( std::make_shared( in, ime_->correctionProfile().get())); } catch (const std::exception &e) { PINYIN_ERROR() << e.what(); } } else { PINYIN_ERROR() << "Failed to open shuangpin profile."; } } else { libime::ShuangpinBuiltinProfile profile; #define TRANS_SP_PROFILE(PROFILE) \ case ShuangpinProfileEnum::PROFILE: \ profile = libime::ShuangpinBuiltinProfile::PROFILE; \ break; switch (*config_.shuangpinProfile) { TRANS_SP_PROFILE(Ziranma) TRANS_SP_PROFILE(MS) TRANS_SP_PROFILE(Ziguang) TRANS_SP_PROFILE(ABC) TRANS_SP_PROFILE(Zhongwenzhixing) TRANS_SP_PROFILE(PinyinJiajia) TRANS_SP_PROFILE(Xiaohe) default: profile = libime::ShuangpinBuiltinProfile::Ziranma; break; } ime_->setShuangpinProfile(std::make_shared( profile, ime_->correctionProfile().get())); } // Always set a profile to avoid crash. if (!ime_->shuangpinProfile()) { ime_->setShuangpinProfile(std::make_shared( libime::ShuangpinBuiltinProfile::Ziranma, ime_->correctionProfile().get())); } quickphraseTriggerRegex_.clear(); for (const std::string ®Str : *config_.quickphraseTriggerRegex) { try { std::regex reg{regStr}; quickphraseTriggerRegex_.push_back(std::move(reg)); } catch (const std::exception &e) { PINYIN_DEBUG() << "Invalid regular expression: \"" << regStr << "\", " << e.what(); } } PINYIN_DEBUG() << "Quick Phrase Trigger Regex size: " << quickphraseTriggerRegex_.size(); ime_->dict()->setFlags(libime::TrieDictionary::UserDict + 1, *config_.chaiziEnabled ? libime::PinyinDictFlag::FullMatch : libime::PinyinDictFlag::Disabled); ime_->dict()->setFlags(libime::TrieDictionary::UserDict + 2, *config_.extBEnabled ? libime::PinyinDictFlag::NoFlag : libime::PinyinDictFlag::Disabled); pyConfig_ = config_; } void PinyinEngine::reloadConfig() { PINYIN_DEBUG() << "Reload pinyin config."; readAsIni(config_, "conf/pinyin.conf"); populateConfig(); } void PinyinEngine::activate(const fcitx::InputMethodEntry &entry, fcitx::InputContextEvent &event) { auto *inputContext = event.inputContext(); // Request addons that gonna be used but not listed in optional // dependencies. fullwidth(); chttrans(); if (*config_.spellEnabled) { spell(); } if (pinyinhelper()) { // Preload stroke data, since we gonna use it anyway. pinyinhelper()->call(); } for (const auto *actionName : {"chttrans", "punctuation", "fullwidth"}) { if (auto *action = instance_->userInterfaceManager().lookupAction(actionName)) { inputContext->statusArea().addAction(StatusGroup::InputMethod, action); } } inputContext->statusArea().addAction(StatusGroup::InputMethod, &predictionAction_); auto *state = inputContext->propertyFor(&factory_); state->context_.setUseShuangpin(entry.uniqueName() == "shuangpin"); // TODO: use surrouding to re-build context. state->context_.clearContextWords(); } void PinyinEngine::deactivate(const fcitx::InputMethodEntry &entry, fcitx::InputContextEvent &event) { auto *inputContext = event.inputContext(); do { if (event.type() != EventType::InputContextSwitchInputMethod) { break; } auto *state = inputContext->propertyFor(&factory_); if (state->mode_ == PinyinMode::Punctuation) { auto candidateList = inputContext->inputPanel().candidateList(); if (!candidateList) { break; } auto index = candidateList->cursorIndex(); if (index >= 0) { candidateList->candidate(index).select(inputContext); } break; } if (state->context_.empty()) { break; } switch (*config_.switchInputMethodBehavior) { case SwitchInputMethodBehavior::CommitPreedit: inputContext->commitString(preeditCommitString(inputContext)); break; case SwitchInputMethodBehavior::CommitDefault: { inputContext->commitString(state->context_.sentence()); break; } case SwitchInputMethodBehavior::Clear: break; } } while (0); reset(entry, event); } bool PinyinEngine::handlePuncCandidate(KeyEvent &event) { auto *inputContext = event.inputContext(); auto *state = inputContext->propertyFor(&factory_); if (state->mode_ != PinyinMode::Punctuation) { return false; } auto candidateList = inputContext->inputPanel().candidateList(); if (!candidateList) { doReset(inputContext); return false; } if (event.key().check(FcitxKey_BackSpace)) { event.filterAndAccept(); doReset(inputContext); return true; } if (!event.isVirtual()) { int idx = event.key().keyListIndex(selectionKeys_); if (idx == -1 && *config_.useKeypadAsSelectionKey) { idx = event.key().keyListIndex(numpadSelectionKeys_); } if (idx >= 0) { event.filterAndAccept(); if (idx < candidateList->size()) { candidateList->candidate(idx).select(inputContext); } return true; } if (auto *movable = candidateList->toCursorMovable()) { if (event.key().checkKeyList(*config_.nextCandidate)) { movable->nextCandidate(); updatePuncPreedit(inputContext); inputContext->updateUserInterface( UserInterfaceComponent::InputPanel); event.filterAndAccept(); return true; } if (event.key().checkKeyList(*config_.prevCandidate)) { movable->prevCandidate(); updatePuncPreedit(inputContext); inputContext->updateUserInterface( UserInterfaceComponent::InputPanel); event.filterAndAccept(); return true; } } } auto index = candidateList->cursorIndex(); if (index >= 0) { candidateList->candidate(index).select(inputContext); } doReset(inputContext); return false; } bool PinyinEngine::handleCloudpinyinTrigger(KeyEvent &event) { if (cloudpinyin() && event.key().checkKeyList( cloudpinyin()->call())) { config_.cloudPinyinEnabled.setValue(!*config_.cloudPinyinEnabled); safeSaveAsIni(config_, "conf/pinyin.conf"); if (notifications()) { notifications()->call( "fcitx-cloudpinyin-toggle", _("Pinyin"), "", _("Cloud Pinyin Status"), *config_.cloudPinyinEnabled ? _("Cloud Pinyin is enabled.") : _("Cloud Pinyin is disabled."), 1000); } if (*config_.cloudPinyinEnabled) { cloudpinyin()->call(); } event.filterAndAccept(); return true; } return false; } bool PinyinEngine::handle2nd3rdSelection(KeyEvent &event) { auto *inputContext = event.inputContext(); auto candidateList = inputContext->inputPanel().candidateList(); if (!candidateList) { return false; } struct { const KeyList &list; int selection; } keyHandlers[] = { // Index starts with 0 {.list = *config_.secondCandidate, .selection = 1}, {.list = *config_.thirdCandidate, .selection = 2}, }; auto *state = inputContext->propertyFor(&factory_); int keyReleased = state->keyReleased_; int keyReleasedIndex = state->keyReleasedIndex_; // Keep these two values, and reset them in the state state->keyReleased_ = -1; state->keyReleasedIndex_ = -2; const bool isModifier = event.key().isModifier(); if (event.isRelease()) { int idx = 0; for (auto &keyHandler : keyHandlers) { if (keyReleased == idx && keyReleasedIndex == event.key().keyListIndex(keyHandler.list)) { if (isModifier) { if (instance_->globalConfig().checkModifierOnlyKeyTimeout( state->lastKeyPressedTime_) && keyHandler.selection < candidateList->size()) { candidateList->candidate(keyHandler.selection) .select(inputContext); } state->lastKeyPressedTime_ = 0; event.filterAndAccept(); return true; } event.filter(); return true; } idx++; } } if (!event.filtered() && !event.isRelease()) { int idx = 0; for (auto &keyHandler : keyHandlers) { auto keyIdx = event.key().keyListIndex(keyHandler.list); if (keyIdx >= 0) { state->keyReleased_ = idx; state->keyReleasedIndex_ = keyIdx; if (isModifier) { state->lastKeyPressedTime_ = now(CLOCK_MONOTONIC); // don't forward to input method, but make it pass // through to client. event.filter(); return true; } if (keyHandler.selection < candidateList->size()) { candidateList->candidate(keyHandler.selection) .select(inputContext); } event.filterAndAccept(); return true; } idx++; } } return false; } bool PinyinEngine::handleCandidateList( KeyEvent &event, const std::shared_future &keyChr) { auto *inputContext = event.inputContext(); auto candidateList = inputContext->inputPanel().candidateList(); if (!candidateList) { return false; } auto *state = inputContext->propertyFor(&factory_); if (event.key().checkKeyList(*config_.currentCandidate) && !state->predictWords_) { if (!candidateList->empty()) { event.filterAndAccept(); int idx = std::max(candidateList->cursorIndex(), 0); candidateList->candidate(idx).select(inputContext); return true; } } if (event.isVirtual()) { return false; } int idx = event.key().keyListIndex(selectionKeys_); if (idx == -1 && *config_.useKeypadAsSelectionKey) { idx = event.key().keyListIndex(numpadSelectionKeys_); } if (idx >= 0) { event.filterAndAccept(); if (idx < candidateList->size()) { candidateList->candidate(idx).select(inputContext); } return true; } if (event.key().checkKeyList(*config_.prevPage)) { auto *pageable = candidateList->toPageable(); if (!pageable->hasPrev()) { if (pageable->usedNextBefore()) { event.filterAndAccept(); return true; } // Only let key go through if it can reach handlePunc. auto c = keyChr.get(); if (event.key().hasModifier() || !c) { event.filterAndAccept(); return true; } } else { event.filterAndAccept(); pageable->prev(); inputContext->updateUserInterface( UserInterfaceComponent::InputPanel); return true; } } if (handleNextPage(event)) { return true; } if (auto *movable = candidateList->toCursorMovable()) { if (event.key().checkKeyList(*config_.nextCandidate)) { movable->nextCandidate(); inputContext->updateUserInterface( UserInterfaceComponent::InputPanel); event.filterAndAccept(); return true; } if (event.key().checkKeyList(*config_.prevCandidate)) { movable->prevCandidate(); inputContext->updateUserInterface( UserInterfaceComponent::InputPanel); event.filterAndAccept(); return true; } } return false; } bool PinyinEngine::handleNextPage(KeyEvent &event) const { auto *inputContext = event.inputContext(); auto candidateList = inputContext->inputPanel().candidateList(); if (event.key().checkKeyList(*config_.nextPage)) { event.filterAndAccept(); candidateList->toPageable()->next(); inputContext->updateUserInterface(UserInterfaceComponent::InputPanel); return true; } return false; } void PinyinEngine::updateStroke(InputContext *inputContext) { auto *state = inputContext->propertyFor(&factory_); auto &inputPanel = inputContext->inputPanel(); inputPanel.reset(); updatePreedit(inputContext); Text aux; aux.append(_("[Stroke Filtering]")); aux.append(pinyinhelper()->call( state->strokeBuffer_.userInput())); inputPanel.setAuxUp(aux); auto candidateList = std::make_unique(); candidateList->setPageSize(*config_.pageSize); candidateList->setCursorPositionAfterPaging( CursorPositionAfterPaging::ResetToFirst); auto *origCandidateList = state->strokeCandidateList_->toBulk(); for (int i = 0; i < origCandidateList->totalSize(); i++) { const auto &candidate = origCandidateList->candidateFromAll(i); auto str = candidate.text().toStringForCommit(); if (auto length = utf8::lengthValidated(str); length != utf8::INVALID_LENGTH && length >= 1) { auto charRange = utf8::MakeUTF8CharRange(str); bool strokeMatched = false; for (auto iter = std::begin(charRange), end = std::end(charRange); iter != end; ++iter) { std::string chr(iter.charRange().first, iter.charRange().second); auto stroke = pinyinhelper()->call( chr); if (stroke.starts_with(state->strokeBuffer_.userInput())) { strokeMatched = true; break; } } if (strokeMatched) { // PinyinCandidateWord is the only forgettable. if (dynamic_cast(&candidate)) { candidateList->append>(this, inputContext, candidate.text(), i); } else if (dynamic_cast(&candidate)) { candidateList->append>(this, inputContext, candidate.text(), i); } } } } candidateList->setSelectionKey(selectionKeys_); if (!candidateList->empty()) { candidateList->setGlobalCursorIndex(0); } candidateList->setActionableImpl( std::make_unique(this, inputContext)); inputContext->inputPanel().setCandidateList(std::move(candidateList)); inputContext->updatePreedit(); inputContext->updateUserInterface(UserInterfaceComponent::InputPanel); } void PinyinEngine::updateForgetCandidate(InputContext *inputContext) { auto *state = inputContext->propertyFor(&factory_); auto &inputPanel = inputContext->inputPanel(); inputPanel.reset(); updatePreedit(inputContext); Text aux(_("[Select the word to remove from history]")); inputPanel.setAuxUp(aux); auto candidateList = std::make_unique(); candidateList->setPageSize(*config_.pageSize); candidateList->setCursorPositionAfterPaging( CursorPositionAfterPaging::ResetToFirst); auto *origCandidateList = state->forgetCandidateList_->toBulk(); for (int i = 0; i < origCandidateList->totalSize(); i++) { const auto &candidate = origCandidateList->candidateFromAll(i); if (const auto *pyCandidate = dynamic_cast(&candidate)) { if (pyCandidate->idx_ >= state->context_.candidatesToCursor().size() || state->context_ .candidateFullPinyin( state->context_.candidatesToCursor()[pyCandidate->idx_]) .empty()) { continue; } candidateList->append( this, pyCandidate->text(), pyCandidate->idx_); } } candidateList->setSelectionKey(selectionKeys_); if (!candidateList->empty()) { candidateList->setGlobalCursorIndex(0); } inputContext->inputPanel().setCandidateList(std::move(candidateList)); inputContext->updatePreedit(); inputContext->updateUserInterface(UserInterfaceComponent::InputPanel); } void PinyinEngine::resetStroke(InputContext *inputContext) const { auto *state = inputContext->propertyFor(&factory_); state->strokeCandidateList_.reset(); state->strokeBuffer_.clear(); if (state->mode_ == PinyinMode::StrokeFilter) { state->mode_ = PinyinMode::Normal; } } void PinyinEngine::resetForgetCandidate(InputContext *inputContext) const { auto *state = inputContext->propertyFor(&factory_); state->forgetCandidateList_.reset(); if (state->mode_ == PinyinMode::ForgetCandidate) { state->mode_ = PinyinMode::Normal; } } void PinyinEngine::forgetCandidate(InputContext *inputContext, size_t index) { auto *state = inputContext->propertyFor(&factory_); const std::string currentInput = state->context_.userInput(); if (index < state->context_.candidatesToCursor().size()) { const auto &sentence = state->context_.candidatesToCursor()[index]; // If this is a word, remove it from user dict. if (sentence.size() == 1) { auto py = state->context_.candidateFullPinyin(index); state->context_.ime()->dict()->removeWord( libime::PinyinDictionary::UserDict, py, sentence.toString()); } for (const auto &word : sentence.sentence()) { state->context_.ime()->model()->history().forget(word->word()); } } resetForgetCandidate(inputContext); doReset(inputContext); state->context_.type(currentInput); updateUI(inputContext); } void PinyinEngine::saveCustomPhrase() { instance_->eventDispatcher().scheduleWithContext(watch(), [this]() { StandardPaths::global().safeSave( StandardPathsType::PkgData, "pinyin/customphrase", [this](int fd) { OFDStreamBuf buffer(fd); std::ostream out(&buffer); try { customPhrase_.save(out); return static_cast(out); } catch (const std::exception &e) { PINYIN_ERROR() << "Failed to save custom phrase: " << e.what(); return false; } }); }); } void PinyinEngine::pinCustomPhrase(InputContext *inputContext, const std::string &customPhrase) { auto *state = inputContext->propertyFor(&factory_); auto &context = state->context_; // Precompute some values. const auto selectedLength = context.selectedLength(); const auto pyLength = context.cursor() > selectedLength ? context.cursor() - selectedLength : std::string::npos; const auto py = context.userInput().substr(selectedLength, pyLength); customPhrase_.pinPhrase(py, customPhrase); resetStroke(inputContext); updateUI(inputContext); saveCustomPhrase(); } void PinyinEngine::deleteCustomPhrase(InputContext *inputContext, const std::string &customPhrase) { auto *state = inputContext->propertyFor(&factory_); auto &context = state->context_; // Precompute some values. const auto selectedLength = context.selectedLength(); const auto pyLength = context.cursor() > selectedLength ? context.cursor() - selectedLength : std::string::npos; const auto py = context.userInput().substr(selectedLength, pyLength); customPhrase_.removePhrase(py, customPhrase); resetStroke(inputContext); updateUI(inputContext); saveCustomPhrase(); } bool PinyinEngine::handleStrokeFilter( KeyEvent &event, const std::shared_future &keyChr) { auto *inputContext = event.inputContext(); auto candidateList = inputContext->inputPanel().candidateList(); auto *state = inputContext->propertyFor(&factory_); if (state->mode_ == PinyinMode::Normal) { if (candidateList && !candidateList->empty() && candidateList->toBulk() && event.key().checkKeyList(*config_.selectByStroke) && pinyinhelper()) { resetStroke(inputContext); state->strokeCandidateList_ = std::move(candidateList); state->mode_ = PinyinMode::StrokeFilter; updateStroke(inputContext); handleNextPage(event); event.filterAndAccept(); return true; } return false; } if (state->mode_ != PinyinMode::StrokeFilter) { return false; } event.filterAndAccept(); // A special case that allow prev page to quit stroke filtering. if ((state->strokeBuffer_.empty() && event.key().checkKeyList(*config_.prevPage))) { auto candidateList = inputContext->inputPanel().candidateList(); if (candidateList && candidateList->toPageable() && candidateList->toPageable()->currentPage() <= 1) { resetStroke(inputContext); updateUI(inputContext); return true; } } if (handleCandidateList(event, keyChr)) { return true; } // Skip all key combination. if (event.key().states().testAny(KeyState::SimpleMask)) { return true; } if (event.key().check(FcitxKey_Escape)) { resetStroke(inputContext); updateUI(inputContext); return true; } if (event.key().check(FcitxKey_BackSpace)) { // Do backspace is stroke is not empty. if (!state->strokeBuffer_.empty()) { state->strokeBuffer_.backspace(); updateStroke(inputContext); } else { // Exit stroke mode when stroke buffer is empty. resetStroke(inputContext); updateUI(inputContext); } return true; } // if it gonna commit something auto c = keyChr.get(); if (!c) { return true; } if (event.key().check(FcitxKey_h) || event.key().check(FcitxKey_p) || event.key().check(FcitxKey_s) || event.key().check(FcitxKey_n) || event.key().check(FcitxKey_z)) { static const std::unordered_map strokeMap{ {FcitxKey_h, '1'}, {FcitxKey_s, '2'}, {FcitxKey_p, '3'}, {FcitxKey_n, '4'}, {FcitxKey_z, '5'}}; if (auto iter = strokeMap.find(event.key().sym()); iter != strokeMap.end()) { state->strokeBuffer_.type(iter->second); updateStroke(inputContext); } } return true; } bool PinyinEngine::handleForgetCandidate(KeyEvent &event) { auto *inputContext = event.inputContext(); auto candidateList = inputContext->inputPanel().candidateList(); auto *state = inputContext->propertyFor(&factory_); if (state->mode_ == PinyinMode::Normal) { if (!state->predictWords_ && candidateList && !candidateList->empty() && candidateList->toBulk() && event.key().checkKeyList(*config_.forgetWord)) { resetForgetCandidate(inputContext); state->forgetCandidateList_ = std::move(candidateList); state->mode_ = PinyinMode::ForgetCandidate; updateForgetCandidate(inputContext); event.filterAndAccept(); return true; } return false; } if (state->mode_ != PinyinMode::ForgetCandidate) { return false; } event.filterAndAccept(); // Skip all key combination. if (event.key().states().testAny(KeyState::SimpleMask)) { return true; } if (event.key().check(FcitxKey_Escape)) { resetForgetCandidate(inputContext); updateUI(inputContext); return true; } return true; } bool PinyinEngine::handlePunc(KeyEvent &event, const std::shared_future &keyChr) { auto *inputContext = event.inputContext(); auto candidateList = inputContext->inputPanel().candidateList(); auto *state = inputContext->propertyFor(&factory_); if (event.filtered()) { return false; } // if it gonna commit something auto c = keyChr.get(); if (event.key().hasModifier() || !c) { // Handle quick phrase with modifier if (event.key().check(*config_.quickphraseKey) && quickphrase()) { quickphrase()->call(inputContext, "", "", "", "", Key()); event.filterAndAccept(); return true; } return false; } if (candidateList && !candidateList->empty()) { int idx = std::max(candidateList->cursorIndex(), 0); candidateList->candidate(idx).select(inputContext); } // We can't emit punctuation right now, since it's not yet fully selected. // There is an option to split the current input, however, it may not be // sane to do so based on the partial input and cursor location. E.g. // current input is n|i hao, and you press ",". Candidate will be filtered // until "ni", instead of "n". which means you are not inserting "," between // n & i. Thus, let's just make it select the default candidate. // Also, it can not work with punctuation candidate, or type punc in pair. if (!state->context_.empty()) { event.filterAndAccept(); return true; } // It's a key not handled by engine, just clear the word context. // Need to do after candidate selection, so the context is properly reset. state->context_.clearContextWords(); std::string punc; std::string puncAfter; // skip key pad if (c && !event.key().isKeyPad()) { auto candidates = punctuation()->call("zh_CN", c); auto pushResult = punctuation()->call( "zh_CN", inputContext, c); if (candidates.size() == 1) { std::tie(punc, puncAfter) = pushResult; } else if (candidates.size() > 1) { updatePuncCandidate(inputContext, utf8::UCS4ToUTF8(c), candidates); event.filterAndAccept(); return true; } } if (!event.isVirtual() && event.key().check(*config_.quickphraseKey) && quickphrase()) { auto keyString = utf8::UCS4ToUTF8(c); // s is punc or key auto output = !punc.empty() ? (punc + puncAfter) : keyString; // alt is key or empty auto altOutput = !punc.empty() ? keyString : ""; // if no punc: key -> key (s = key, alt = empty) // if there's punc: key -> punc, return -> key (s = punc, alt = // key) std::string text; if (!output.empty()) { if (!altOutput.empty()) { text = _("Press {} for {} and Return for {}", keyString, output, altOutput); } else { text = _("Press {} for {}", keyString, output); } } quickphrase()->call( inputContext, text, "", output, altOutput, *config_.quickphraseKey); event.filterAndAccept(); return true; } if (!punc.empty()) { event.filterAndAccept(); auto paired = punc + puncAfter; if (inputContext->capabilityFlags().test( CapabilityFlag::CommitStringWithCursor)) { if (size_t length = utf8::lengthValidated(punc); length != 0 && length != utf8::INVALID_LENGTH) { inputContext->commitStringWithCursor(paired, length); } else { inputContext->commitString(paired); } } else { inputContext->commitString(paired); if (size_t length = utf8::lengthValidated(puncAfter); length != 0 && length != utf8::INVALID_LENGTH) { for (size_t i = 0; i < length; i++) { inputContext->forwardKey(Key(FcitxKey_Left)); } } } } state->lastIsPunc_ = true; return false; } bool PinyinEngine::handleCompose(KeyEvent &event) { auto *inputContext = event.inputContext(); auto *state = inputContext->propertyFor(&factory_); if (event.key().states().testAny( KeyStates{KeyState::Ctrl, KeyState::Super}) || state->mode_ != PinyinMode::Normal) { return false; } auto candidateList = inputContext->inputPanel().candidateList(); if (event.filtered()) { return false; } auto compose = instance_->processComposeString(inputContext, event.key().sym()); if (!compose) { // invalid compose or in the middle of compose. event.filterAndAccept(); return true; } // Handle the key just like punc select. if (!compose->empty()) { // Reset predict in case we are in predict. resetPredict(inputContext); // punc like auto selection. auto candidateList = inputContext->inputPanel().candidateList(); if (candidateList && !candidateList->empty()) { candidateList->candidate(0).select(inputContext); } inputContext->commitString(*compose); event.filterAndAccept(); return true; } return false; } void PinyinEngine::resetPredict(InputContext *inputContext) { auto *state = inputContext->propertyFor(&factory_); if (!state->predictWords_) { return; } state->predictWords_.reset(); inputContext->inputPanel().reset(); inputContext->updatePreedit(); inputContext->updateUserInterface(UserInterfaceComponent::InputPanel); } void PinyinEngine::keyEvent(const InputMethodEntry &entry, KeyEvent &event) { FCITX_UNUSED(entry); PINYIN_DEBUG() << "Pinyin receive key: " << event.key() << " " << event.isRelease(); auto *inputContext = event.inputContext(); auto *state = inputContext->propertyFor(&factory_); std::shared_future keyChr = std::async(std::launch::deferred, [sym = event.key().sym()] { return Key::keySymToUnicode(sym); }).share(); std::shared_future keyStr = std::async(std::launch::deferred, [&keyChr] { return utf8::UCS4ToUTF8(keyChr.get()); }).share(); // 2nd/3rd selection is allowed to be modifier only, handle them before we // skip the release. if (handle2nd3rdSelection(event)) { return; } // by pass all key release if (event.isRelease()) { return; } if (handlePuncCandidate(event)) { return; } // and by pass all modifier if (event.key().isModifier()) { return; } if (handleCloudpinyinTrigger(event)) { return; } auto candidateList = inputContext->inputPanel().candidateList(); bool lastIsPunc = state->lastIsPunc_; state->lastIsPunc_ = false; if (handleStrokeFilter(event, keyChr)) { return; } if (handleCompose(event)) { return; } // handle number key selection and prev/next page/candidate. if (handleCandidateList(event, keyChr)) { return; } if (handleForgetCandidate(event)) { return; } // In prediction, as long as it's not candidate selection, clear, then // fallback // to remaining operation. if (state->predictWords_) { resetPredict(inputContext); if (event.key().check(FcitxKey_Escape) || (event.key().check(FcitxKey_BackSpace) && (*config_.backspaceBehaviorOnPrediction == BackspaceBehaviorOnPrediction::OnlyClearCandidates || (*config_.backspaceBehaviorOnPrediction == BackspaceBehaviorOnPrediction:: BackspaceWhenNotUsingOnScreenKeyboard && event.isVirtual()))) || event.key().check(FcitxKey_Delete)) { event.filterAndAccept(); return; } } auto checkSp = [this, &keyChr](const KeyEvent &event, PinyinState *state) { auto shuangpinProfile = ime_->shuangpinProfile(); if (!state->context_.useShuangpin() || !shuangpinProfile || !event.key().isSimple()) { return false; } // event.key().isSimple() make sure the return value is within range of // char. char chr = keyChr.get(); return (!state->context_.empty() && shuangpinProfile->validInput().contains(chr)) || (state->context_.empty() && shuangpinProfile->validInitial().contains(chr)); }; if (!event.key().hasModifier() && quickphrase() && !quickphraseTriggerRegex_.empty() && !keyStr.get().empty() && state->context_.selectedLength() == 0 && state->context_.cursor() == state->context_.size()) { std::string text = stringutils::concat(state->context_.userInput(), keyStr.get()); for (const auto ® : quickphraseTriggerRegex_) { if (std::regex_search(text, reg, std::regex_constants::match_default)) { // Keep the current state before reset. const std::string origin = state->context_.userInput(); doReset(inputContext); quickphrase()->call(inputContext, "", "", "", "", Key()); quickphrase()->call( inputContext, text, origin, [this](InputContext *ic, const std::string &origin) { if (this->instance()->inputMethodEngine(ic) == this) { auto *state = ic->propertyFor(&factory_); doReset(ic); state->context_.type(origin); updateUI(ic); } }); event.filterAndAccept(); return; } } } if (event.key().isLAZ() || event.key().isUAZ() || (event.key().check(FcitxKey_apostrophe) && !state->context_.empty()) || checkSp(event, state)) { // first v, use it to trigger quickphrase if (*config_.useVAsQuickphrase && quickphrase() && state->context_.empty()) { const bool isSp = state->context_.useShuangpin(); if ((event.key().check(FcitxKey_v) && !isSp) || (event.key().check(FcitxKey_V) && isSp)) { const std::string leadingString = Key::keySymToUTF8(event.key().sym()); quickphrase()->call( inputContext, "", leadingString, "", "", Key(FcitxKey_None)); event.filterAndAccept(); return; } } event.filterAndAccept(); if (!state->context_.type(keyStr.get())) { return; } } else if (!state->context_.empty()) { // key to handle when it is not empty. if (event.key().check(FcitxKey_BackSpace)) { if (*config_.useBackSpaceToUnselect && state->context_.selectedLength()) { state->context_.cancel(); } else { state->context_.backspace(); } event.filterAndAccept(); } else if (event.key().check(FcitxKey_Delete) || event.key().check(FcitxKey_KP_Delete)) { state->context_.del(); event.filterAndAccept(); } else if (event.key().check(FcitxKey_BackSpace, KeyState::Ctrl)) { if (state->context_.cursor() == state->context_.selectedLength()) { state->context_.cancel(); } auto cursor = state->context_.pinyinBeforeCursor(); if (cursor >= 0) { state->context_.erase(cursor, state->context_.cursor()); } event.filterAndAccept(); } else if (event.key().check(FcitxKey_Delete, KeyState::Ctrl) || event.key().check(FcitxKey_KP_Delete, KeyState::Ctrl)) { auto cursor = state->context_.pinyinAfterCursor(); if (cursor >= 0 && static_cast(cursor) <= state->context_.size()) { state->context_.erase(state->context_.cursor(), cursor); } event.filterAndAccept(); } else if (event.key().check(FcitxKey_Home) || event.key().check(FcitxKey_KP_Home)) { state->context_.setCursor(state->context_.selectedLength()); event.filterAndAccept(); } else if (event.key().check(FcitxKey_End) || event.key().check(FcitxKey_KP_End)) { state->context_.setCursor(state->context_.size()); event.filterAndAccept(); } else if (event.key().check(FcitxKey_Left) || event.key().check(FcitxKey_KP_Left)) { if (state->context_.cursor() == state->context_.selectedLength()) { state->context_.cancel(); } auto cursor = state->context_.cursor(); if (cursor > 0) { state->context_.setCursor(cursor - 1); } event.filterAndAccept(); } else if (event.key().check(FcitxKey_Right) || event.key().check(FcitxKey_KP_Right)) { auto cursor = state->context_.cursor(); if (cursor < state->context_.size()) { state->context_.setCursor(cursor + 1); } event.filterAndAccept(); } else if (event.key().check(FcitxKey_Left, KeyState::Ctrl) || event.key().check(FcitxKey_KP_Left, KeyState::Ctrl)) { if (state->context_.cursor() == state->context_.selectedLength()) { state->context_.cancel(); } auto cursor = state->context_.pinyinBeforeCursor(); if (cursor >= 0) { state->context_.setCursor(cursor); } event.filterAndAccept(); } else if (event.key().check(FcitxKey_Right, KeyState::Ctrl) || event.key().check(FcitxKey_KP_Right, KeyState::Ctrl)) { auto cursor = state->context_.pinyinAfterCursor(); if (cursor >= 0 && static_cast(cursor) <= state->context_.size()) { state->context_.setCursor(cursor); } event.filterAndAccept(); } else if (event.key().check(FcitxKey_Escape)) { state->context_.clear(); event.filterAndAccept(); } else if (event.key().checkKeyList(*config_.commitRawInput)) { inputContext->commitString(preeditCommitString(inputContext)); state->context_.clear(); state->context_.clearContextWords(); event.filterAndAccept(); } else if (int idx = event.key().keyListIndex(*config_.selectCharFromPhrase); idx >= 0) { if (candidateList && !candidateList->empty() && candidateList->cursorIndex() >= 0) { const auto &candidate = candidateList->candidate(candidateList->cursorIndex()); auto str = candidate.text().toStringForCommit(); // Validate string and length. if (auto len = utf8::lengthValidated(str); len != utf8::INVALID_LENGTH && len > static_cast(idx)) { // Get idx-th char. const std::string_view chr = std::next(utf8::MakeUTF8CharRange(str).begin(), idx) .view(); auto segmentLength = state->context_.size() - state->context_.selectedLength(); const auto *pyCandidate = dynamic_cast(&candidate); if (pyCandidate) { const auto &contextCandidates = state->context_.candidatesToCursor(); if (pyCandidate->idx_ < contextCandidates.size()) { const auto &sentence = contextCandidates[pyCandidate->idx_].sentence(); const auto candidateSegmentLength = sentence.back()->to()->index(); segmentLength = std::min(segmentLength, candidateSegmentLength); } } state->context_.selectCustom(segmentLength, chr); event.filterAndAccept(); } } } } else if (event.key().check(FcitxKey_BackSpace)) { if (lastIsPunc) { const std::string &puncStr = punctuation()->call("zh_CN", inputContext); if (!puncStr.empty()) { // forward the original key is the best choice. state->cancelLastEvent_ = instance()->eventLoop().addTimeEvent( CLOCK_MONOTONIC, now(CLOCK_MONOTONIC) + 3000, 1, [this, ref = inputContext->watch(), puncStr = puncStr](EventSourceTime *, uint64_t) { if (auto *inputContext = ref.get()) { inputContext->commitString(puncStr); auto *state = inputContext->propertyFor(&factory_); state->cancelLastEvent_.reset(); } return true; }); event.filter(); return; } } } if (handlePunc(event, keyChr)) { return; } if (event.filtered() && event.accepted()) { updateUI(inputContext); } } void PinyinEngine::setSubConfig(const std::string &path, const RawConfig &config) { FCITX_UNUSED(config); if (path == "dictmanager") { loadExtraDict(); } else if (path == "clearuserdict") { ime_->dict()->clear(libime::PinyinDictionary::UserDict); } else if (path == "clearalldict") { ime_->dict()->clear(libime::PinyinDictionary::UserDict); ime_->model()->history().clear(); } else if (path == "customphrase") { loadCustomPhrase(); } } void PinyinEngine::reset(const InputMethodEntry & /*entry*/, InputContextEvent &event) { auto *inputContext = event.inputContext(); doReset(inputContext); } void PinyinEngine::doReset(InputContext *inputContext) const { auto *state = inputContext->propertyFor(&factory_); resetStroke(inputContext); resetForgetCandidate(inputContext); state->mode_ = PinyinMode::Normal; state->context_.clear(); state->context_.clearContextWords(); state->predictWords_.reset(); inputContext->inputPanel().reset(); inputContext->updatePreedit(); inputContext->updateUserInterface(UserInterfaceComponent::InputPanel); // state->lastIsPunc_ = false; state->keyReleased_ = -1; state->keyReleasedIndex_ = -2; instance_->resetCompose(inputContext); } void PinyinEngine::save() { safeSaveAsIni(config_, "conf/pinyin.conf"); const auto &standardPath = StandardPaths::global(); standardPath.safeSave( StandardPathsType::PkgData, "pinyin/user.dict", [this](int fd) { OFDStreamBuf buffer(fd); std::ostream out(&buffer); try { ime_->dict()->save(libime::PinyinDictionary::UserDict, out, libime::PinyinDictFormat::Binary); return static_cast(out); } catch (const std::exception &e) { PINYIN_ERROR() << "Failed to save pinyin dict: " << e.what(); return false; } }); standardPath.safeSave( StandardPathsType::PkgData, "pinyin/user.history", [this](int fd) { OFDStreamBuf buffer(fd); std::ostream out(&buffer); try { ime_->model()->save(out); return true; } catch (const std::exception &e) { PINYIN_ERROR() << "Failed to save pinyin history: " << e.what(); return false; } }); } std::string PinyinEngine::subMode(const InputMethodEntry &entry, InputContext &inputContext) { FCITX_UNUSED(inputContext); if (entry.uniqueName() == "shuangpin" && *config_.showShuangpinMode && *config_.shuangpinProfile != ShuangpinProfileEnum::Custom) { return ShuangpinProfileEnumI18NAnnotation::toString( *config_.shuangpinProfile); } return {}; } const Configuration *PinyinEngine::getConfig() const { return &config_; } const Configuration * PinyinEngine::getConfigForInputMethod(const InputMethodEntry &entry) const { // Hide shuangpin options. if (entry.uniqueName() == "pinyin") { return &pyConfig_; } return &config_; } void PinyinEngine::invokeActionImpl(const InputMethodEntry &entry, InvokeActionEvent &event) { auto *inputContext = event.inputContext(); auto *state = inputContext->propertyFor(&factory_); auto &context = state->context_; auto &inputPanel = inputContext->inputPanel(); if (event.cursor() < 0 || event.action() != InvokeActionEvent::Action::LeftClick || !inputContext->capabilityFlags().test(CapabilityFlag::Preedit)) { InputMethodEngineV3::invokeActionImpl(entry, event); return; } auto [clientPreedit, _] = this->preedit(inputContext); auto preedit = clientPreedit.toString(); size_t cursor = event.cursor(); if (inputPanel.clientPreedit().toString() != clientPreedit.toString() || inputPanel.clientPreedit().cursor() != clientPreedit.cursor() || cursor > utf8::length(preedit)) { InputMethodEngineV3::invokeActionImpl(entry, event); return; } event.filter(); auto preeditWithCursor = context.preeditWithCursor(); auto selectedSentence = context.selectedSentence(); // The logic here need to match ::preedit() switch (*config_.preeditMode) { case PreeditMode::No: break; case PreeditMode::ComposingPinyin: if (utf8::length(selectedSentence) > cursor) { // If cursor is with in selected sentence range, cancel until cursor // is covered. do { context.cancel(); } while (utf8::length(context.selectedSentence()) > cursor); context.setCursor(context.selectedLength()); } else { // If cursor is with in the pinyin range, move to the left most and // then move it around. This is a easy way to cover Shuangpin and // Preedit Mode difference. setCursor should be cheap operation if // it does not cancel any selection. context.setCursor(context.selectedLength()); while (context.cursor() < context.size()) { auto [preeditText, preeditCursor] = context.preeditWithCursor(); if (utf8::length(preeditText.begin(), preeditText.begin() + preeditCursor) < cursor) { state->context_.setCursor(context.cursor() + 1); } else { break; } } auto [preeditText, preeditCursor] = context.preeditWithCursor(); if (utf8::length(preeditText.begin(), preeditText.begin() + preeditCursor) > cursor) { state->context_.setCursor(context.cursor() - 1); } } break; case PreeditMode::CommitPreview: if (utf8::length(selectedSentence) > cursor) { do { context.cancel(); } while (utf8::length(context.selectedSentence()) > cursor); } break; } updateUI(inputContext); } void PinyinEngine::cloudPinyinSelected(InputContext *inputContext, const std::string &selected, const std::string &word) { auto *state = inputContext->propertyFor(&factory_); auto words = state->context_.selectedWords(); // This ensure us to convert pinyin to the right one. auto preedit = state->context_.preedit(libime::PinyinPreeditMode::RawText); // preedit is "selected sentence" + Pinyin. do { // Validate selected is still the same. if (!preedit.starts_with(selected)) { words.clear(); break; } // Get segmented pinyin. preedit = preedit.substr(selected.size()); auto pinyins = stringutils::split(preedit, " '"); std::string_view wordView = word; // Check word length matches pinyin length. if (pinyins.empty() || pinyins.size() != utf8::length(word)) { break; } const auto &candidates = state->context_.candidates(); auto pinyinsIter = pinyins.begin(); auto pinyinsEnd = pinyins.end(); if (!candidates.empty()) { const auto &bestSentence = candidates[0].sentence(); auto iter = bestSentence.begin(); auto end = bestSentence.end(); while (iter != end) { auto consumed = wordView; if (!stringutils::consumePrefix(consumed, (*iter)->word())) { break; } if (!(*iter)->word().empty()) { words.push_back((*iter)->word()); PINYIN_DEBUG() << "Cloud Pinyin can reuse segment " << (*iter)->word(); const auto *pinyinNode = static_cast(*iter); auto pinyinSize = pinyinNode->encodedPinyin().size() / 2; if (pinyinSize && static_cast(std::distance( pinyinsIter, pinyinsEnd)) >= pinyinSize) { pinyinsIter += pinyinSize; } else { break; } } wordView = consumed; iter++; } } // if pinyin is not valid, it may throw try { if (utf8::length(wordView) == 1 && std::all_of(words.begin(), words.end(), [](const std::string &w) { return utf8::length(w) == 1; })) { words = state->context_.selectedWords(); auto joined = stringutils::join(pinyins.begin(), pinyins.end(), "'"); words.push_back(word); ime_->dict()->addWord(libime::PinyinDictionary::UserDict, joined, word); } else { if (state->context_.useShuangpin()) { bool end = false; for (auto &sppinyin : MakeIterRange(pinyinsIter, pinyinsEnd)) { // Likely something goes wrong. if (sppinyin.size() > 2) { end = true; break; } sppinyin = libime::PinyinEncoder::shuangpinToPinyin( sppinyin, *ime_->shuangpinProfile()); if (sppinyin.empty()) { end = true; break; } } if (end) { break; } } auto joined = stringutils::join(pinyinsIter, pinyinsEnd, "'"); PINYIN_DEBUG() << "Cloud pinyin saves word: " << wordView << " " << joined; ime_->dict()->addWord(libime::PinyinDictionary::UserDict, joined, wordView); words.push_back(std::string{wordView}); } ime_->model()->history().add(words); } catch (const std::exception &e) { PINYIN_DEBUG() << "Failed to save cloudpinyin: " << e.what(); } } while (0); state->context_.clear(); inputContext->commitString(selected + word); inputContext->inputPanel().reset(); if (*config_.keepCurrentContext) { state->context_.appendContextWords(words); } if (*config_.predictionEnabled) { state->predictWords_ = std::move(words); updatePredict(inputContext); } inputContext->updatePreedit(); inputContext->updateUserInterface(UserInterfaceComponent::InputPanel); } } // namespace fcitx