1 // OVIMPhoneticSQLite.cpp: SQLite-based BPMF input method
2 //
3 // Copyright (c) 2004-2006 The OpenVanilla Project (http://openvanilla.org)
4 // All rights reserved.
5 //
6 // Redistribution and use in source and binary forms, with or without
7 // modification, are permitted provided that the following conditions
8 // are met:
9 //
10 // 1. Redistributions of source code must retain the above copyright
11 // notice, this list of conditions and the following disclaimer.
12 // 2. Redistributions in binary form must reproduce the above copyright
13 // notice, this list of conditions and the following disclaimer in the
14 // documentation and/or other materials provided with the distribution.
15 // 3. Neither the name of OpenVanilla nor the names of its contributors
16 // may be used to endorse or promote products derived from this software
17 // without specific prior written permission.
18 //
19 // THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
20 // AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
21 // IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
22 // ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
23 // LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
24 // CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
25 // SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
26 // INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
27 // CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
28 // ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
29 // POSSIBILITY OF SUCH DAMAGE.
30
31 // #define OV_DEBUG
32
33 #include <OpenVanilla/OpenVanilla.h>
34 #include <OpenVanilla/OVLibrary.h>
35 #include <OpenVanilla/OVUtility.h>
36 #include <stdio.h>
37 #include <stdlib.h>
38 #include <string.h>
39 #include <ctype.h>
40
41 //#include <sys/syslimits.h>
42 #include "OVOSDef.h"
43
44 #include "OVSQLite3.h"
45 #include "OVPhoneticLib.h"
46
47 #ifndef OVIMPHONETIC_EXT
48 #define OVIMP_MODID "OVIMPhoneticSQLite"
49 #define OVIMP_NAME_ZH_TW "注音(SQLite版)"
50 #define OVIMP_NAME_ZH_CN "繁体注音(SQLite版)"
51 #define OVIMP_NAME_EN "Phonetic (Bopomofo) Using SQLite"
52 #else
53 #define OVIMP_MODID "OVIMPhoneticSQLite"
54 #define OVIMP_NAME_ZH_TW "注音(大字集,SQLite版)"
55 #define OVIMP_NAME_ZH_CN "繁体注音(大字集,SQLite版)"
56 #define OVIMP_NAME_EN "Phonetic (Bopomofo) Large Set Using SQLite"
57 #endif
58
59 class OVIMPhoneticSQLite;
60
61 class OVIMPhoneticContext : public OVInputMethodContext {
62 public:
63 OVIMPhoneticContext(OVIMPhoneticSQLite *p);
64 virtual void start(OVBuffer*, OVCandidate*, OVService*);
65 virtual void clear();
66 virtual void end();
67 virtual int keyEvent(OVKeyCode*, OVBuffer*, OVCandidate*, OVService*);
68
69 protected:
70 int keyEsc();
71 int keyBackspace();
72 int keyCompose();
73 int keyPrintable();
74 int keyNonBPMF();
75 int keyCapslock();
76 int fetchCandidate(const char *);
77 int fetchCandidateWithPrefix(const char *prefix, char c);
78 int isPunctuationCombination();
79 int punctuationKey();
80 int updateCandidateWindow();
81 int closeCandidateWindow();
82 int commitFirstCandidate();
83 int candidateEvent();
84 int candidatePageUp();
85 int candidatePageDown();
86
87 OVKeyCode *k;
88 OVBuffer *b;
89 OVCandidate *c;
90 OVService *s;
91 OVIMPhoneticSQLite *parent;
92 OVPhoneticSyllable syl;
93 OVPCandidate *candi;
94 int page;
95 };
96
97 class OVIMPhoneticSQLite : public OVInputMethod {
98 public:
99 virtual OVInputMethodContext *newContext();
100 virtual int initialize(OVDictionary *, OVService *, const char *);
101 virtual void update(OVDictionary *, OVService *);
102 virtual const char *identifier();
103 virtual const char *localizedName(const char *);
104
105 protected:
106 friend class OVIMPhoneticContext;
107 int layout;
108 char selkey[96];
109 SQLite3 *db;
110 };
111
112 OV_SINGLE_MODULE_WRAPPER(OVIMPhoneticSQLite);
113
newContext()114 OVInputMethodContext *OVIMPhoneticSQLite::newContext() {
115 return new OVIMPhoneticContext(this);
116 }
117
initialize(OVDictionary * cfg,OVService * s,const char * p)118 int OVIMPhoneticSQLite::initialize(OVDictionary *cfg, OVService * s, const char *p) {
119 db=new SQLite3; // this never gets deleted, but so do we
120 char dbfile[PATH_MAX];
121 sprintf(dbfile, "%s/OVIMPhoneticSQLite/bpmf.db", p);
122 murmur("OVIMPhoneticSQLite: database file is %s", dbfile);
123
124 if (int err=db->open(dbfile)) {
125 s->notify("SQLite3 error");
126 murmur("SQLite3 error! code=%d", err);
127 return 0;
128 }
129 update(cfg, s);
130 return 1;
131 }
132
update(OVDictionary * cfg,OVService *)133 void OVIMPhoneticSQLite::update(OVDictionary *cfg, OVService *) {
134 layout=cfg->getIntegerWithDefault("keyboardLayout", 0);
135 if (layout !=0 && layout !=1) layout=0;
136 strcpy(selkey, cfg->getStringWithDefault("selectKey", "123456789"));
137 murmur("OVIMPhoneticSQLite: config update! keyboard layout=%d, select key=%s", layout, selkey);
138 }
139
identifier()140 const char *OVIMPhoneticSQLite::identifier() {
141 return OVIMP_MODID;
142 }
143
localizedName(const char * lc)144 const char *OVIMPhoneticSQLite::localizedName(const char *lc) {
145 if (!strcasecmp(lc, "zh_TW")) return OVIMP_NAME_ZH_TW;
146 if (!strcasecmp(lc, "zh_CN")) return OVIMP_NAME_ZH_CN;
147 return OVIMP_NAME_EN;
148 }
149
OVIMPhoneticContext(OVIMPhoneticSQLite * p)150 OVIMPhoneticContext::OVIMPhoneticContext(OVIMPhoneticSQLite *p) {
151 parent=p;
152 }
153
start(OVBuffer *,OVCandidate *,OVService * s)154 void OVIMPhoneticContext::start(OVBuffer*, OVCandidate*, OVService* s) {
155 syl.clear();
156 syl.setLayout(parent->layout);
157 candi=NULL;
158 }
159
clear()160 void OVIMPhoneticContext::clear() {
161 syl.clear();
162 }
163
end()164 void OVIMPhoneticContext::end() {
165 }
166
keyEvent(OVKeyCode * pk,OVBuffer * pb,OVCandidate * pc,OVService * ps)167 int OVIMPhoneticContext::keyEvent(OVKeyCode* pk, OVBuffer* pb, OVCandidate* pc, OVService* ps) {
168 k=pk; b=pb; c=pc; s=ps;
169 if (candi) return candidateEvent();
170 if (isPunctuationCombination() && b->isEmpty()) return punctuationKey();
171 if (k->isFunctionKey() && b->isEmpty()) return 0;
172 if (k->isCapslock() && b->isEmpty()) return keyCapslock();
173 if (k->code()==ovkEsc) return keyEsc();
174 if (k->code()==ovkBackspace || k->code()==ovkDelete) return keyBackspace();
175 if (!b->isEmpty() && (syl.isComposeKey(k->code()) || k->code()==ovkReturn)) return keyCompose();
176 if (isprint(k->code())) return keyPrintable();
177 return 0;
178 }
179
keyEsc()180 int OVIMPhoneticContext::keyEsc() {
181 if (b->isEmpty()) return 0; // if buffer is empty, do nothing
182 syl.clear(); // otherwise we clear the syllable
183 b->clear()->update();
184 return 1;
185 }
186
keyBackspace()187 int OVIMPhoneticContext::keyBackspace() {
188 if (b->isEmpty()) return 0;
189 syl.removeLast();
190 b->clear();
191 if (!syl.empty()) b->append(syl.compose());
192 b->update();
193 return 1;
194 }
195
keyPrintable()196 int OVIMPhoneticContext::keyPrintable() {
197 if (isalpha(k->code()) && k->isShift() && b->isEmpty()) {
198 char keystr[2];
199 sprintf(keystr, "%c", tolower(k->code()));
200 b->clear()->append(keystr)->send();
201 return 1;
202 }
203 if (!syl.addKey(k->code())) {
204 if (b->isEmpty()) return keyNonBPMF(); // not a BPMF keycode
205 s->beep();
206 }
207 b->clear()->append(syl.compose())->update();
208 return 1;
209 }
210
keyNonBPMF()211 int OVIMPhoneticContext::keyNonBPMF() {
212 char keystr[2];
213 keystr[0]=k->code(); keystr[1]=0;
214 int count=fetchCandidateWithPrefix("_punctuation_", k->code());
215 if (!count) { // not a punctuation mark
216 b->clear()->append(keystr)->send();
217 return 1;
218 }
219 if (count==1) return commitFirstCandidate();
220
221 // we need to put the first candidate onto the composing buffer
222 // to make some application (e.g. iTerm.app) happy
223 b->clear()->append(candi->candidates[0])->update();
224 return updateCandidateWindow();
225 }
226
isPunctuationCombination()227 int OVIMPhoneticContext::isPunctuationCombination() {
228 // only accept CTRL-1 or CTRL-0
229 if (k->isCtrl() && !k->isOpt() && !k->isCommand() &&
230 (k->code()=='1' || k->code()=='0')) return 1;
231 // only accept CTRL-OPT-[printable]
232 if (k->isCtrl() && k->isOpt() && !k->isCommand() && !k->isShift() &&
233 ((k->code() >=1 && k->code() <=26) || isprint(k->code()))) return 1;
234 return 0;
235 }
236
punctuationKey()237 int OVIMPhoneticContext::punctuationKey() {
238 int count;
239 char kc=k->code();
240 if ((kc=='0' || kc=='1') && !k->isOpt())
241 count=fetchCandidate("_punctuation_list");
242 else {
243 if (kc >= 1 && kc <= 26) kc+='a'-1;
244 count=fetchCandidateWithPrefix("_ctrl_opt_", kc);
245 }
246 if (!count) return 0; // we send back the combination key
247 if (count==1) return commitFirstCandidate();
248
249 // we need to put the first candidate onto the composing buffer
250 // to make some application (e.g. iTerm.app) happy
251 b->clear()->append(candi->candidates[0])->update();
252 return updateCandidateWindow();
253 }
254
keyCapslock()255 int OVIMPhoneticContext::keyCapslock() {
256 char keystr[2];
257 keystr[1]=0;
258 if (isprint(k->code())) {
259 if (k->isShift()) keystr[0]=toupper(k->code());
260 else keystr[0]=tolower(k->code());
261 b->clear()->append(keystr)->send();
262 return 1;
263 }
264 return 0;
265 }
266
keyCompose()267 int OVIMPhoneticContext::keyCompose() {
268 if (k->code()!=ovkSpace && k->code()!=ovkReturn) syl.addKey(k->code());
269
270 // we don't update the compose key (tone key) at this stage,
271 // because it often causes display nauseousness
272 // b->clear()->append(syl.compose())->update();
273
274 int count=fetchCandidate(syl.standardLayoutCode());
275 if (!count) {
276 b->clear()->append(syl.compose())->update(); // update here
277 s->beep();
278 return 1;
279 }
280
281 if (count==1) return commitFirstCandidate();
282
283 // we display first candidate instead
284 b->clear()->append(candi->candidates[0])->update();
285 return updateCandidateWindow();
286 }
287
288
fetchCandidateWithPrefix(const char * prefix,char c)289 int OVIMPhoneticContext::fetchCandidateWithPrefix(const char *prefix, char c) {
290 char keystr[64];
291 sprintf(keystr, "%s%c", prefix, c);
292 return fetchCandidate(keystr);
293 }
294
295
fetchCandidate(const char * qs)296 int OVIMPhoneticContext::fetchCandidate(const char *qs) {
297 page=0;
298 if (candi) {
299 delete candi;
300 candi=NULL;
301 }
302
303 SQLite3Statement *sth=parent->db->prepare("select chr from bpmf where bpmf=?1 order by ord;");
304 if (!sth) return 0;
305 sth->bind_text(1, qs);
306
307 int rows=0;
308 while (sth->step()==SQLITE_ROW) rows++;
309 murmur("query string=%s, number of candidates=%d", qs, rows);
310 if (!rows) {
311 delete sth;
312 return 0;
313 }
314
315 candi=new OVPCandidate;
316 candi->count=0;
317 candi->candidates=new char* [rows];
318 sth->reset();
319 while (sth->step()==SQLITE_ROW) {
320 const char *v=sth->column_text(0);
321 char *s=(char*)calloc(1, strlen(v)+1);
322 strcpy(s, v);
323 candi->candidates[candi->count++]=s;
324 }
325 delete sth;
326 return rows;
327 }
328
candidateEvent()329 int OVIMPhoneticContext::candidateEvent() {
330 char kc=k->code();
331 if (kc==ovkEsc || kc==ovkBackspace || kc==ovkDelete) { //ESC/BKSP/DELETE cancels candi window
332 clear();
333 b->clear()->update();
334 return closeCandidateWindow();
335 }
336
337 if (kc==ovkSpace || kc==ovkRight || kc==ovkDown || kc==ovkPageDown || kc =='>')
338 return candidatePageDown();
339 if (kc==ovkLeft || kc==ovkUp || kc==ovkPageUp || kc=='<')
340 return candidatePageUp();
341
342 int perpage=strlen(parent->selkey);
343 int i=0, l=(candi->count >= perpage) ? perpage : candi->count, nextsyl=0;
344 for (i=0; i<l; i++) if(parent->selkey[i]==kc) break;
345 if (i==l) { // not a valid candidate key
346 if (kc==ovkReturn) i=0;
347 if (syl.isValidKey(kc)) { i=0; nextsyl=1; }
348 }
349 if (i==l) {
350 s->beep();
351 b->update(); // we do this to make some applications happy
352 }
353 else {
354 b->clear()->append(candi->candidates[i + page*perpage])->send();
355 closeCandidateWindow();
356 if (nextsyl) {
357 syl.clear();
358 syl.addKey(kc);
359 b->clear()->append(syl.compose())->update();
360 }
361 }
362 return 1;
363 }
364
updateCandidateWindow()365 int OVIMPhoneticContext::updateCandidateWindow() {
366 if (!candi) return 1;
367 int candicount=candi->count;
368 int perpage=strlen(parent->selkey);
369 int pgstart=page*perpage;
370
371 c->clear();
372 char dispstr[32];
373
374 for (int i=0; i<perpage; i++) {
375 if (pgstart+i >= candicount) break; // stop if idx exceeds candi counts
376 sprintf(dispstr, "%c.", parent->selkey[i]);
377 c->append(dispstr)->append(candi->candidates[page*perpage+i])->append(" ");
378 }
379 // add current page number
380 sprintf(dispstr, "(%d/%d)", page+1, (candicount-1)/perpage +1);
381 c->append(dispstr);
382 c->update();
383 if (!c->onScreen()) c->show();
384 b->update(); // we do this to make some application happy
385 return 1;
386 }
387
closeCandidateWindow()388 int OVIMPhoneticContext::closeCandidateWindow() {
389 syl.clear();
390 if (c->onScreen()) c->hide()->clear()->update();
391 if (candi) {
392 delete candi;
393 candi=NULL;
394 }
395 return 1;
396 }
397
commitFirstCandidate()398 int OVIMPhoneticContext::commitFirstCandidate() {
399 if (!candi) return 1;
400 b->clear()->append(candi->candidates[0])->send();
401 return closeCandidateWindow();
402 }
403
candidatePageUp()404 int OVIMPhoneticContext::candidatePageUp() {
405 int maxpage=(candi->count-1) / strlen(parent->selkey);
406 if (!maxpage) s->beep();
407 else {
408 if (!page) page=maxpage; else page--;
409 updateCandidateWindow();
410 }
411 return 1;
412 }
413
candidatePageDown()414 int OVIMPhoneticContext::candidatePageDown() {
415 int maxpage=(candi->count-1) / strlen(parent->selkey);
416 if (!maxpage) s->beep();
417 else {
418 if (page==maxpage) page=0; else page++;
419 updateCandidateWindow();
420 }
421 return 1;
422 }
423
424