/* * SPDX-License-Identifier: LGPL-2.1-or-later * SPDX-FileCopyrightText: Copyright 2021-2023 Fcitx5 for Android Contributors */ #include #include #include #include #include #include #include #include #include #include #include "androidfrontend.h" namespace fcitx { class AndroidInputContext : public InputContextV2 { public: AndroidInputContext(AndroidFrontend *frontend, InputContextManager &inputContextManager, int uid, const std::string &pkgName) : InputContextV2(inputContextManager, pkgName), frontend_(frontend), uid_(uid) { created(); } ~AndroidInputContext() override { frontend_->releaseInputContext(uid_); destroy(); } [[nodiscard]] const char *frontend() const override { return "androidfrontend"; } void commitStringImpl(const std::string &text) override { frontend_->commitString(text, -1); } void commitStringWithCursorImpl(const std::string &text, size_t cursor) override { frontend_->commitString(text, static_cast(cursor)); } void forwardKeyImpl(const ForwardKeyEvent &key) override { frontend_->forwardKey(key.rawKey(), key.isRelease()); } void deleteSurroundingTextImpl(int offset, unsigned int size) override { const int before = -offset; const int after = offset + static_cast(size); if (before < 0 || after < 0) { FCITX_WARN() << "Invalid deleteSurrounding request: offset=" << offset << ", size=" << size; return; } frontend_->deleteSurrounding(before, after); } void updatePreeditImpl() override { frontend_->updateClientPreedit(filterText(inputPanel().clientPreedit())); } void updateInputPanel() { const InputPanel &ip = inputPanel(); frontend_->updateInputPanel( filterText(ip.preedit()), filterText(ip.auxUp()), filterText(ip.auxDown()) ); } void updateCandidatesBulk() { std::vector candidates; int size = 0; const auto &list = inputPanel().candidateList(); if (list) { const auto &bulk = list->toBulk(); if (bulk) { size = bulk->totalSize(); // limit candidate count to 16 (for paging) const int limit = size < 0 ? 16 : std::min(size, 16); for (int i = 0; i < limit; i++) { try { auto &candidate = bulk->candidateFromAll(i); // maybe unnecessary; I don't see anywhere using `CandidateWord::setPlaceHolder` // if (candidate.isPlaceHolder()) continue; candidates.emplace_back(filterCandidate(candidate)); } catch (const std::invalid_argument &e) { size = static_cast(candidates.size()); break; } } } else { size = list->size(); for (int i = 0; i < size; i++) { candidates.emplace_back(filterCandidate(list->candidate(i))); } } } frontend_->updateCandidateList(candidates, size); } void updateCandidatesPaged() { const auto &list = inputPanel().candidateList(); if (!list) { frontend_->updatePagedCandidate(PagedCandidateEntity::Empty); return; } int cursorIndex = list->cursorIndex(); CandidateLayoutHint layoutHint = list->layoutHint(); bool hasPrev = false; bool hasNext = false; const auto &pageable = list->toPageable(); if (pageable) { hasPrev = pageable->hasPrev(); hasNext = pageable->hasNext(); } int size = list->size(); std::vector candidates; candidates.reserve(size); for (int i = 0; i < size; i++) { const auto &c = list->candidate(i); candidates.emplace_back( filterString(list->label(i)), filterString(c.text()), filterString(c.comment()) ); } PagedCandidateEntity paged(candidates, cursorIndex, layoutHint, hasPrev, hasNext); frontend_->updatePagedCandidate(paged); } bool selectCandidateBulk(int idx) { const auto &list = inputPanel().candidateList(); if (!list) { return false; } const auto &bulk = list->toBulk(); try { if (bulk) { bulk->candidateFromAll(idx).select(this); } else { list->candidate(idx).select(this); } } catch (const std::invalid_argument &e) { FCITX_WARN() << "selectCandidate index out of range"; return false; } return true; } bool selectCandidatePaged(int idx) { const auto &list = inputPanel().candidateList(); if (!list) { return false; } try { list->candidate(idx).select(this); } catch (const std::invalid_argument &e) { FCITX_WARN() << "selectCandidate index out of range"; return false; } return true; } std::vector getCandidates(const int offset, const int limit) { std::vector candidates; const auto &list = inputPanel().candidateList(); if (list) { const int last = offset + limit; const auto &bulk = list->toBulk(); if (bulk) { const int totalSize = bulk->totalSize(); const int end = totalSize < 0 ? last : std::min(totalSize, last); for (int i = offset; i < end; i++) { try { auto &candidate = bulk->candidateFromAll(i); candidates.emplace_back(filterCandidate(candidate)); } catch (const std::invalid_argument &e) { break; } } } else { const int end = std::min(list->size(), last); for (int i = offset; i < end; i++) { candidates.emplace_back(filterCandidate(list->candidate(i))); } } } return candidates; } std::vector getCandidateAction(const int idx) { std::vector actions; const auto &list = inputPanel().candidateList(); if (list) { const auto &actionable = list->toActionable(); if (actionable) { if (idx >= list->size()) { const auto &bulk = list->toBulk(); if (bulk) { try { const auto &c = bulk->candidateFromAll(idx); for (const auto &a: actionable->candidateActions(c)) { actions.emplace_back(a); } } catch (const std::exception &e) { FCITX_WARN() << "getCandidateAction(" << idx << ") failed:" << e.what(); } } } else { const auto &c = list->candidate(idx); for (const auto &a: actionable->candidateActions(c)) { actions.emplace_back(a); } } } } return actions; } void triggerCandidateAction(const int idx, const int actionIdx) { const auto &list = inputPanel().candidateList(); if (!list) return; const auto &actionable = list->toActionable(); if (!actionable) return; if (idx >= list->size()) { const auto &bulk = list->toBulk(); if (bulk) { try { const auto &c = bulk->candidateFromAll(idx); actionable->triggerAction(c, actionIdx); } catch (const std::exception &e) { FCITX_WARN() << "triggerCandidateAction(" << idx << ") failed:" << e.what(); } } } else { const auto &c = list->candidate(idx); actionable->triggerAction(c, actionIdx); } } void offsetCandidatePage(int delta) { if (delta == 0) { return; } const auto &list = inputPanel().candidateList(); if (!list) { return; } const auto &pageable = list->toPageable(); if (!pageable) { return; } if (delta > 0 && pageable->hasNext()) { pageable->next(); updateUserInterface(UserInterfaceComponent::InputPanel); } else if (delta < 0 && pageable->hasPrev()) { pageable->prev(); updateUserInterface(UserInterfaceComponent::InputPanel); } } private: AndroidFrontend *frontend_; int uid_; inline Text filterText(const Text &orig) { return frontend_->instance()->outputFilter(this, orig); } inline std::string filterString(const Text &orig) { return filterText(orig).toString(); } inline std::string filterCandidate(const CandidateWord &candidate) { const std::string separator = candidate.spaceBetweenComment() ? " " : ""; return filterString(candidate.textWithComment(separator)); } }; AndroidFrontend::AndroidFrontend(Instance *instance) : instance_(instance), focusGroup_("android", instance->inputContextManager()), activeIC_(nullptr), icCache_(), eventHandlers_(), pagingMode_(0) { eventHandlers_.emplace_back(instance_->watchEvent( EventType::InputContextInputMethodActivated, EventWatcherPhase::Default, [this](Event &event) { auto &e = static_cast(event); if (e.inputContext() != activeIC_) return; imChangeCallback(makeInputMethodStatus(activeIC_)); } )); eventHandlers_.emplace_back(instance_->watchEvent( EventType::InputContextSwitchInputMethod, EventWatcherPhase::Default, [this](Event &event) { auto &e = static_cast(event); if (e.inputContext() != activeIC_) return; const auto &reason = static_cast::type>(e.reason()); const std::string &oldIM = e.oldInputMethod(); switchInputMethodCallback(reason, oldIM); } )); eventHandlers_.emplace_back(instance_->watchEvent( EventType::InputContextFlushUI, EventWatcherPhase::Default, [this](Event &event) { auto &e = static_cast(event); if (e.inputContext() != activeIC_) return; switch (e.component()) { case UserInterfaceComponent::InputPanel: { activeIC_->updateInputPanel(); if (pagingMode_ == 0) { activeIC_->updateCandidatesBulk(); } else { activeIC_->updateCandidatesPaged(); } break; } case UserInterfaceComponent::StatusArea: { statusAreaUpdateCallback(makeStatusAreaActions(activeIC_), makeInputMethodStatus(activeIC_)); break; } } } )); } void AndroidFrontend::keyEvent(const Key &key, bool isRelease, const int timestamp) { if (!activeIC_) return; KeyEvent keyEvent(activeIC_, key, isRelease); activeIC_->keyEvent(keyEvent); if (!keyEvent.accepted()) { auto sym = key.sym(); keyEventCallback(sym, key.states(), Key::keySymToUnicode(sym), isRelease, timestamp); } } void AndroidFrontend::forwardKey(const Key &key, bool isRelease) { auto sym = key.sym(); keyEventCallback(sym, key.states(), Key::keySymToUnicode(sym), isRelease, -1); } void AndroidFrontend::commitString(const std::string &str, const int cursor) { commitStringCallback(str, cursor); } void AndroidFrontend::updateCandidateList(const std::vector &candidates, const int size) { candidateListCallback(candidates, size); } void AndroidFrontend::updateClientPreedit(const Text &clientPreedit) { preeditCallback(clientPreedit); } void AndroidFrontend::updateInputPanel(const Text &preedit, const Text &auxUp, const Text &auxDown) { inputPanelCallback(preedit, auxUp, auxDown); } void AndroidFrontend::releaseInputContext(const int uid) { icCache_.release(uid); } bool AndroidFrontend::selectCandidate(int idx) { if (!activeIC_) return false; if (pagingMode_) { return activeIC_->selectCandidatePaged(idx); } else { return activeIC_->selectCandidateBulk(idx); } } std::vector AndroidFrontend::getCandidateActions(const int idx) { if (!activeIC_) return {}; return activeIC_->getCandidateAction(idx); } void AndroidFrontend::triggerCandidateAction(const int idx, const int actionIdx) { if (!activeIC_) return; activeIC_->triggerCandidateAction(idx, actionIdx); } bool AndroidFrontend::isInputPanelEmpty() { if (!activeIC_) return true; return activeIC_->inputPanel().empty(); } void AndroidFrontend::resetInputContext() { if (!activeIC_) return; activeIC_->reset(); } void AndroidFrontend::repositionCursor(int position) { if (!activeIC_) return; InvokeActionEvent event(InvokeActionEvent::Action::LeftClick, position, activeIC_); activeIC_->invokeAction(event); } void AndroidFrontend::focusInputContext(bool focus) { if (!activeIC_) return; if (focus) { activeIC_->focusIn(); } else { activeIC_->focusOut(); } } void AndroidFrontend::activateInputContext(const int uid, const std::string &pkgName) { auto *ptr = icCache_.find(uid); if (ptr) { activeIC_ = dynamic_cast(ptr->get()); } else { auto *ic = new AndroidInputContext(this, instance_->inputContextManager(), uid, pkgName); activeIC_ = ic; icCache_.insert(uid, ic); ic->setFocusGroup(&focusGroup_); } } InputContext *AndroidFrontend::activeInputContext() const { return activeIC_; } void AndroidFrontend::deactivateInputContext(const int uid) { auto *ptr = icCache_.find(uid); if (!ptr) return; focusGroup_.setFocusedInputContext(nullptr); activeIC_ = nullptr; } void AndroidFrontend::setCapabilityFlags(uint64_t flag) { if (!activeIC_) return; activeIC_->setCapabilityFlags(CapabilityFlags(flag)); } void AndroidFrontend::setCandidateListCallback(const CandidateListCallback &callback) { candidateListCallback = callback; } std::vector AndroidFrontend::getCandidates(const int offset, const int limit) { if (!activeIC_) return {}; return activeIC_->getCandidates(offset, limit); } void AndroidFrontend::deleteSurrounding(const int before, const int after) { deleteSurroundingCallback(before, after); } void AndroidFrontend::showToast(const std::string &s) { toastCallback(s); } void AndroidFrontend::setCandidatePagingMode(const int mode) { pagingMode_ = mode; if (mode == 0) { activeIC_->updateCandidatesBulk(); } else { activeIC_->updateCandidatesPaged(); } } void AndroidFrontend::updatePagedCandidate(const PagedCandidateEntity &paged) { pagedCandidateCallback(paged); } void AndroidFrontend::offsetCandidatePage(int delta) { if (!activeIC_) return; activeIC_->offsetCandidatePage(delta); } void AndroidFrontend::setCommitStringCallback(const CommitStringCallback &callback) { commitStringCallback = callback; } void AndroidFrontend::setPreeditCallback(const ClientPreeditCallback &callback) { preeditCallback = callback; } void AndroidFrontend::setInputPanelAuxCallback(const InputPanelCallback &callback) { inputPanelCallback = callback; } void AndroidFrontend::setKeyEventCallback(const KeyEventCallback &callback) { keyEventCallback = callback; } void AndroidFrontend::setInputMethodChangeCallback(const InputMethodChangeCallback &callback) { imChangeCallback = callback; } void AndroidFrontend::setStatusAreaUpdateCallback(const StatusAreaUpdateCallback &callback) { statusAreaUpdateCallback = callback; } void AndroidFrontend::setDeleteSurroundingCallback(const DeleteSurroundingCallback &callback) { deleteSurroundingCallback = callback; } void AndroidFrontend::setToastCallback(const ToastCallback &callback) { toastCallback = callback; } void AndroidFrontend::setPagedCandidateCallback(const PagedCandidateCallback &callback) { pagedCandidateCallback = callback; } void AndroidFrontend::setSwitchInputMethodCallback(const SwitchInputMethodCallback &callback) { switchInputMethodCallback = callback; } InputMethodStatus AndroidFrontend::makeInputMethodStatus(InputContext *ic) { auto *entry = instance_->inputMethodEntry(ic); auto *engine = instance_->inputMethodEngine(ic); return {entry, engine, ic}; } std::vector AndroidFrontend::makeStatusAreaActions(fcitx::InputContext *ic) { auto actions = std::vector(); for (auto group: {fcitx::StatusGroup::BeforeInputMethod, fcitx::StatusGroup::InputMethod, fcitx::StatusGroup::AfterInputMethod}) { for (auto act: ic->statusArea().actions(group)) { actions.emplace_back(act, ic); } } return actions; } class AndroidFrontendFactory : public AddonFactory { public: AddonInstance *create(AddonManager *manager) override { return new AndroidFrontend(manager->instance()); } }; } // namespace fcitx FCITX_ADDON_FACTORY(fcitx::AndroidFrontendFactory)