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