Skip to content

Commit 7e6b9a5

Browse files
authored
horizontal scroll and fetch candidates on demand (#49)
1 parent e688d50 commit 7e6b9a5

File tree

8 files changed

+113
-15
lines changed

8 files changed

+113
-15
lines changed

entry/src/main/cpp/napi_init.cpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,14 @@ API(setCurrentInputMethod) {
8888
return {};
8989
}
9090

91+
API(scroll) {
92+
GET_ARGS(2)
93+
GET_I32(start, 0)
94+
GET_I32(count, 1)
95+
fcitx::scroll(start, count);
96+
return {};
97+
}
98+
9199
static void CallJs(napi_env env, napi_value jsCb, void *context, void *data) {
92100
if (env == nullptr) {
93101
return;
@@ -124,6 +132,7 @@ static napi_value Init(napi_env env, napi_value exports) {
124132
{"selectCandidate", nullptr, selectCandidate, nullptr, nullptr, nullptr, napi_default, nullptr},
125133
{"askCandidateActions", nullptr, askCandidateActions, nullptr, nullptr, nullptr, napi_default, nullptr},
126134
{"activateCandidateAction", nullptr, activateCandidateAction, nullptr, nullptr, nullptr, napi_default, nullptr},
135+
{"scroll", nullptr, scroll, nullptr, nullptr, nullptr, napi_default, nullptr},
127136
{"setCurrentInputMethod", nullptr, setCurrentInputMethod, nullptr, nullptr, nullptr, napi_default, nullptr},
128137
{"activateStatusAreaAction", nullptr, activateStatusAreaAction, nullptr, nullptr, nullptr, napi_default,
129138
nullptr}};

entry/src/main/cpp/src/fcitx.cpp

Lines changed: 49 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ namespace fcitx {
3838
std::unique_ptr<Instance> instance;
3939
std::unique_ptr<fcitx::EventDispatcher> dispatcher;
4040
HarmonyFrontend *frontend;
41+
WebKeyboard *ui;
4142

4243
static native_streambuf log_streambuf;
4344
static std::ostream stream(&log_streambuf);
@@ -81,6 +82,7 @@ void init(const std::string &bundle, const std::string &resfile) {
8182
dispatcher->attach(&instance->eventLoop());
8283
fcitx_thread = std::thread([] { instance->eventLoop().exec(); });
8384
frontend = dynamic_cast<HarmonyFrontend *>(addonMgr.addon("harmonyfrontend"));
85+
ui = dynamic_cast<WebKeyboard *>(addonMgr.addon("webkeyboard"));
8486
setInputMethods({"keyboard-us", "keyboard-th", "pinyin"}); // XXX: for test only.
8587
}
8688

@@ -109,6 +111,15 @@ void selectCandidate(int index) {
109111
const auto &list = ic->inputPanel().candidateList();
110112
if (!list)
111113
return;
114+
const auto &bulk = list->toBulk();
115+
if (bulk) {
116+
try {
117+
bulk->candidateFromAll(index).select(ic);
118+
} catch (const std::invalid_argument &e) {
119+
FCITX_ERROR() << "select candidate index out of range";
120+
}
121+
return;
122+
}
112123
try {
113124
// Engine is responsible for updating UI
114125
list->candidate(index).select(ic);
@@ -118,6 +129,17 @@ void selectCandidate(int index) {
118129
});
119130
}
120131

132+
static void answerCandidateAction(ActionableCandidateList *actionableList, const CandidateWord &candidate, int index) {
133+
if (actionableList->hasAction(candidate)) {
134+
json actions = json::array();
135+
for (const auto &action : actionableList->candidateActions(candidate)) {
136+
actions.push_back({{"id", action.id()}, {"text", action.text()}});
137+
}
138+
notify_main_async(
139+
json{{"type", "CANDIDATE_ACTIONS"}, {"data", {{"index", index}, {"actions", actions}}}}.dump());
140+
}
141+
}
142+
121143
void askCandidateAction(int index) {
122144
with_fcitx([index] {
123145
auto ic = instance->mostRecentInputContext();
@@ -128,16 +150,19 @@ void askCandidateAction(int index) {
128150
if (!actionableList) {
129151
return;
130152
}
153+
const auto &bulk = list->toBulk();
154+
if (bulk) {
155+
try {
156+
auto &candidate = bulk->candidateFromAll(index);
157+
answerCandidateAction(actionableList, candidate, index);
158+
} catch (const std::invalid_argument &e) {
159+
FCITX_ERROR() << "action candidate index out of range";
160+
}
161+
return;
162+
}
131163
try {
132164
auto &candidate = list->candidate(index);
133-
if (actionableList->hasAction(candidate)) {
134-
json actions = json::array();
135-
for (const auto &action : actionableList->candidateActions(candidate)) {
136-
actions.push_back({{"id", action.id()}, {"text", action.text()}});
137-
}
138-
notify_main_async(
139-
json{{"type", "CANDIDATE_ACTIONS"}, {"data", {{"index", index}, {"actions", actions}}}}.dump());
140-
}
165+
answerCandidateAction(actionableList, candidate, index);
141166
} catch (const std::invalid_argument &e) {
142167
FCITX_ERROR() << "action candidate index out of range";
143168
}
@@ -154,6 +179,18 @@ void activateCandidateAction(int index, int id) {
154179
if (!actionableList) {
155180
return;
156181
}
182+
const auto &bulk = list->toBulk();
183+
if (bulk) {
184+
try {
185+
const auto &candidate = bulk->candidateFromAll(index);
186+
if (actionableList->hasAction(candidate)) {
187+
actionableList->triggerAction(candidate, id);
188+
}
189+
} catch (const std::invalid_argument &e) {
190+
FCITX_ERROR() << "action candidate index out of range";
191+
}
192+
return;
193+
}
157194
try {
158195
auto &candidate = list->candidate(index);
159196
if (actionableList->hasAction(candidate)) {
@@ -181,4 +218,8 @@ void toggle() {
181218
void setCurrentInputMethod(const std::string &inputMethod) {
182219
with_fcitx([&] { instance->setCurrentInputMethod(inputMethod); });
183220
}
221+
222+
void scroll(int start, int count) {
223+
with_fcitx([=] { ui->scroll(start, count); });
224+
}
184225
} // namespace fcitx

entry/src/main/cpp/src/fcitx.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,5 @@ void activateCandidateAction(int index, int id);
2626
void activateStatusAreaAction(int id);
2727
void toggle();
2828
void setCurrentInputMethod(const std::string &inputMethod);
29+
void scroll(int start, int count);
2930
} // namespace fcitx

entry/src/main/cpp/types/libentry/Index.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,4 @@ export const askCandidateActions: (index: number) => void
1717
export const activateCandidateAction: (index: number, id: number) => void
1818
export const activateStatusAreaAction: (id: number) => void
1919
export const setCurrentInputMethod: (inputMethod: string) => void
20+
export const scroll: (start: number, count: number) => void

entry/src/main/cpp/webkeyboard/webkeyboard.cpp

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -35,20 +35,24 @@ void WebKeyboard::update(UserInterfaceComponent component, InputContext *inputCo
3535
auto preedit = instance_->outputFilter(inputContext, inputPanel.preedit()).toString();
3636
notify_main_async(json{{"type", "PREEDIT"}, {"data", {{"auxUp", auxUp}, {"preedit", preedit}}}}.dump());
3737
if (const auto &list = inputPanel.candidateList()) {
38+
const auto &bulk = list->toBulk();
39+
if (bulk) {
40+
return expand();
41+
}
3842
int size = list->size();
3943
candidates.reserve(size);
4044
for (int i = 0; i < size; ++i) {
4145
const auto &label = stringutils::trim(list->label(i).toString());
4246
const auto &candidate = list->candidate(i);
43-
candidates.push_back({label, instance_->outputFilter(inputContext, candidate.text()).toString(),
47+
candidates.push_back({instance_->outputFilter(inputContext, candidate.text()).toString(), label,
4448
instance_->outputFilter(inputContext, candidate.comment()).toString()});
4549
}
4650
highlighted = list->cursorIndex();
4751
}
4852
if (auxUp.empty() && preedit.empty() && candidates.empty()) {
4953
notify_main_async(R"JSON({"type":"CLEAR"})JSON");
5054
} else {
51-
setCandidatesAsync(candidates, highlighted);
55+
setCandidatesAsync(candidates, highlighted, 0, false, false);
5256
}
5357
break;
5458
}
@@ -58,11 +62,48 @@ void WebKeyboard::update(UserInterfaceComponent component, InputContext *inputCo
5862
}
5963
}
6064

61-
void WebKeyboard::setCandidatesAsync(const std::vector<Candidate> &candidates, int highlighted) {
62-
auto j = json{{"type", "CANDIDATES"}, {"data", {{"candidates", candidates}, {"highlighted", highlighted}}}};
65+
void WebKeyboard::setCandidatesAsync(const std::vector<Candidate> &candidates, int highlighted, int scrollState,
66+
bool scrollStart, bool scrollEnd) {
67+
auto j = json{{"type", "CANDIDATES"},
68+
{"data",
69+
{{"candidates", candidates},
70+
{"highlighted", highlighted},
71+
{"scrollState", scrollState},
72+
{"scrollStart", scrollStart},
73+
{"scrollEnd", scrollEnd}}}};
6374
notify_main_async(j.dump());
6475
}
6576

77+
void WebKeyboard::expand() { scroll(0, 20); }
78+
79+
void WebKeyboard::scroll(int start, int count) {
80+
auto ic = instance_->mostRecentInputContext();
81+
const auto &list = ic->inputPanel().candidateList();
82+
if (!list) {
83+
return;
84+
}
85+
const auto &bulk = list->toBulk();
86+
if (!bulk) {
87+
return;
88+
}
89+
int size = bulk->totalSize();
90+
int end = size < 0 ? start + count : std::min(start + count, size);
91+
bool endReached = size == end;
92+
std::vector<Candidate> candidates;
93+
for (int i = start; i < end; ++i) {
94+
try {
95+
auto &candidate = bulk->candidateFromAll(i);
96+
candidates.push_back({instance_->outputFilter(ic, candidate.text()).toString(), "",
97+
instance_->outputFilter(ic, candidate.comment()).toString()});
98+
} catch (const std::invalid_argument &e) {
99+
// size == -1 but actual limit is reached
100+
endReached = true;
101+
break;
102+
}
103+
}
104+
setCandidatesAsync(candidates, start == 0 ? 0 : -1, 2, start == 0, endReached);
105+
}
106+
66107
static nlohmann::json actionToJson(Action *action, InputContext *ic) {
67108
nlohmann::json j;
68109
j["id"] = action->id();

entry/src/main/cpp/webkeyboard/webkeyboard.h

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,8 @@ std::string escape_html(const std::string &content);
1313
namespace fcitx {
1414

1515
struct Candidate {
16-
std::string label;
1716
std::string text;
17+
std::string label;
1818
std::string comment;
1919

2020
friend void to_json(json &j, const Candidate &c) {
@@ -42,11 +42,14 @@ class WebKeyboard final : public VirtualKeyboardUserInterface {
4242
void showVirtualKeyboard() override;
4343
void hideVirtualKeyboard() override {}
4444
void updateStatusArea(InputContext *ic);
45+
void scroll(int start, int count);
4546

4647
private:
4748
Instance *instance_;
4849

49-
void setCandidatesAsync(const std::vector<Candidate> &candidates, int highlighted);
50+
void setCandidatesAsync(const std::vector<Candidate> &candidates, int highlighted, int scrollState,
51+
bool scrollStart, bool scrollEnd);
52+
void expand();
5053
};
5154

5255
class WebKeyboardFactory : public AddonFactory {

entry/src/main/ets/InputMethodExtensionAbility/model/KeyboardController.ets

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -318,6 +318,8 @@ export class KeyboardController {
318318
return this.textInputClient?.sendExtendAction(inputMethodEngine.ExtendAction.PASTE)
319319
case 'REDO':
320320
return redo(this.textInputClient!)
321+
case 'SCROLL':
322+
return fcitx.scroll(event.data.start, event.data.count)
321323
case 'SELECT':
322324
this.selecting = true
323325
break

0 commit comments

Comments
 (0)