mirror of
https://github.com/fcitx5-android/fcitx5-android.git
synced 2026-08-02 20:16:06 +08:00
547 lines
18 KiB
C++
547 lines
18 KiB
C++
/*
|
|
* SPDX-License-Identifier: LGPL-2.1-or-later
|
|
* SPDX-FileCopyrightText: Copyright 2021-2023 Fcitx5 for Android Contributors
|
|
*/
|
|
#include <fcitx/addonfactory.h>
|
|
#include <fcitx/addonmanager.h>
|
|
#include <fcitx/candidatelist.h>
|
|
#include <fcitx/focusgroup.h>
|
|
#include <fcitx/inputcontextmanager.h>
|
|
#include <fcitx/inputmethodengine.h>
|
|
#include <fcitx/inputmethodmanager.h>
|
|
#include <fcitx/inputpanel.h>
|
|
#include <fcitx/statusarea.h>
|
|
#include <fcitx-utils/event.h>
|
|
|
|
#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<int>(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<int>(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<std::string> 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<int>(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<CandidateEntity> 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<std::string> getCandidates(const int offset, const int limit) {
|
|
std::vector<std::string> 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<CandidateAction> getCandidateAction(const int idx) {
|
|
std::vector<CandidateAction> 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<InputMethodActivatedEvent &>(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<InputContextSwitchInputMethodEvent &>(event);
|
|
if (e.inputContext() != activeIC_) return;
|
|
const auto &reason = static_cast<std::underlying_type<InputMethodSwitchedReason>::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<InputContextFlushUIEvent &>(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<std::string> &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<CandidateAction> 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<AndroidInputContext *>(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<std::string> 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<ActionEntity> AndroidFrontend::makeStatusAreaActions(fcitx::InputContext *ic) {
|
|
auto actions = std::vector<ActionEntity>();
|
|
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)
|