// OVIMTobacco.cpp #define OV_DEBUG #ifndef WIN32 #include #include #include #else #include "OpenVanilla.h" #include "OVLibrary.h" #include "OVUtility.h" #endif #include #include #include #include "OVKeySequence.h" #include "OVSQLite3.h" #include "OVOSDef.h" /* #ifndef WIN32 #include //for PATH_MAX #else #include #define PATH_MAX MAX_PATH #define strcasecmp stricmp #endif */ #include #include "PredictorSingleton.h" // Some dirty public secrets goes here SQLite3 *db; //// Number of names in "tablelist" table static int IM_TABLES; //// The values of those names in "tablelist"... with some assumed limitations. //// Please, not extreme sports here. char *IM_TABLE_NAMES[32]; // End of secrets const char *QueryForCommand(SQLite3 *db, const char *command); const char *QueryForKey(SQLite3 *db, const char *tbl, const char *key); PredictorSingleton* predictor; class IMGKeySequence : public OVKeySequenceSimple { public: IMGKeySequence(const char *t) { strcpy(table, t); } virtual int isValidKey(char c); virtual bool add(char c); virtual int isEmpty(); virtual int length() { return len; } virtual const char *compose(); virtual const char *sequence() { return seq; } protected: virtual const char *query(char c); char composebuf[256]; char table[256]; }; struct IMGCandidate { IMGCandidate(); ~IMGCandidate(); unsigned int count; char **candidates; }; class OVIMTobacco; class OVIMTobaccoContext : public OVInputMethodContext { public: OVIMTobaccoContext(OVIMTobacco *p); virtual void start(OVBuffer*, OVCandidate*, OVService*); virtual void clear(); virtual void end(); virtual int keyEvent(OVKeyCode*, OVBuffer*, OVCandidate*, OVService*); protected: int keyCommit(); int keyEsc(); int keyRemove(); int keyMove(); int keyCompose(); int keyPrintable(); int keyNonRadical(); int keyCapslock(); int setCandidate(); int fetchCandidate(const char *); int fetchCandidateWithPrefix(const char *prefix, char c); int isPunctuationCombination(); int punctuationKey(); int setPunctuation(string punctuationCharacters); int updateCandidateWindow(); int closeCandidateWindow(); int commitFirstCandidate(); int candidateEvent(); int candidatePageUp(); int candidatePageDown(); void freshBuffer(); OVKeyCode *k; OVBuffer *b; OVCandidate *c; OVService *s; OVIMTobacco *parent; IMGKeySequence seq; IMGCandidate *candi; int page; size_t position; bool canNewSequence; bool doInsert; }; class OVIMTobacco : public OVInputMethod { public: OVIMTobacco(char *); virtual OVInputMethodContext *newContext(); virtual int initialize(OVDictionary *, OVService *, const char *); virtual void update(OVDictionary *, OVService *); virtual const char *identifier(); virtual const char *localizedName(const char *); virtual int maxSeqLen() { return cfgMaxSeqLen; } virtual bool doBeep() { return cfgBeep; } virtual bool doAutoCompose() { return cfgAutoCompose; } virtual bool doHitMaxAndCompose() { return cfgHitMaxAndCompose; } virtual bool doShiftSelKey() { return cfgDoShiftSelKey; }; virtual bool doClearSequenceOnError() { return cfgDoClearSequenceOnError; }; virtual bool doChooseInFrontOfCursor() { return cfgDoChooseInFrontOfCursor; }; protected: int isEndKey(char c); friend class OVIMTobaccoContext; int allowwildcard; char selkey[96]; char endkey[96]; char table[256]; char idstr[256]; char tsiDbFilePath[256]; int cfgMaxSeqLen; bool cfgBeep; bool cfgAutoCompose; bool cfgHitMaxAndCompose; bool cfgDoShiftSelKey; bool cfgDoChooseInFrontOfCursor; bool cfgDoClearSequenceOnError; }; /* class OVOFReverseLookupSQLite : public OVOutputFilter { public: OVOFReverseLookupSQLite(char *); virtual const char* identifier(); virtual const char* localizedName(const char* locale); virtual int initialize(OVDictionary*, OVService*, const char*); virtual const char* process(const char *src, OVService *srv); protected: char table[256]; char idstr[256]; char composebuffer[1024]; }; */ extern "C" unsigned int OVGetLibraryVersion() { return OV_VERSION; } extern "C" int OVInitializeLibrary(OVService*, const char*p) { db=new SQLite3; // this never gets deleted, but so do we char dbfile[PATH_MAX]; sprintf(dbfile, "%s/OVIMTobacco/imtables.db", p); if (int err=db->open(dbfile)) { murmur("SQLite3 error! code=%d", err); return 0; } SQLite3Statement *sth=db->prepare("select name from tablelist;"); IM_TABLES = 0; while(sth->step()==SQLITE_ROW) { const char *buf = (char*)sth->column_text(0); IM_TABLE_NAMES[IM_TABLES] = (char*)calloc(1,strlen(buf)+1); strcpy(IM_TABLE_NAMES[IM_TABLES],buf); IM_TABLES++; } return 1; } extern "C" OVModule *OVGetModuleFromLibrary(int x) { if(x < IM_TABLES) { return new OVIMTobacco(IM_TABLE_NAMES[x]); } /* else if (x < IM_TABLES * 2) { int n = x - IM_TABLES; return new OVOFReverseLookupSQLite(IM_TABLE_NAMES[n]); } */ return NULL; } /* OVOFReverseLookupSQLite::OVOFReverseLookupSQLite(char *name) { strcpy(table, name); sprintf(idstr,"OVOFReverseLookupSQLite-%s",name); } const char* OVOFReverseLookupSQLite::identifier() { return idstr; } const char* OVOFReverseLookupSQLite::localizedName(const char* lc) { static char buf[256]; const char *name; if(!strcasecmp(lc,"zh_TW")) { name=QueryForKey(db, table, "_property_cname"); } else { name=QueryForKey(db, table, "_property_ename"); } sprintf(buf,"%s Lookup (SQLite)",name); return buf; } int OVOFReverseLookupSQLite::initialize(OVDictionary *cfg, OVService * s, const char *p) { return 1; } const char* OVOFReverseLookupSQLite::process(const char *src, OVService *srv) { unsigned short *u16; int u16len=srv->UTF8ToUTF16(src, &u16); strcpy(composebuffer, ""); char sqlbuf[256]; sprintf(sqlbuf,"select key from %s where value=?1;",table); // WE HAVE TO DO SURROGATE CHECK, REMEMBER! for (int i=0; iUTF16ToUTF8(&(u16[i]), 1); char buf[256]; sprintf(buf, "%s=(", u8); strcat(composebuffer, buf); SQLite3Statement *sth=db->prepare(sqlbuf); sth->bind_text(1, u8); while (sth->step()==SQLITE_ROW) { sprintf(buf, "%s, ", sth->column_text(0)); strcat(composebuffer, buf); } strcat(composebuffer, ")\n"); } if(strlen(composebuffer)) srv->notify(composebuffer); return src; } */ OVIMTobacco::OVIMTobacco(char *name) { strcpy(table, name); sprintf(idstr,"OVIMTobacco-%s",name); } OVInputMethodContext *OVIMTobacco::newContext() { return new OVIMTobaccoContext(this); } int OVIMTobacco::initialize(OVDictionary *cfg, OVService * s, const char *p) { sprintf(tsiDbFilePath, "%sOVIMTobacco/tsi.db", p); murmur("initial predictor(%s)", tsiDbFilePath); predictor = PredictorSingleton::getInstance(tsiDbFilePath); update(cfg, s); murmur("initialized"); return 1; } void OVIMTobacco::update(OVDictionary *cfg, OVService *) { const char *warningBeep="warningBeep"; const char *autoCompose="autoCompose"; const char *maxSeqLen="maxKeySequenceLength"; const char *hitMax="hitMaxAndCompose"; strcpy(selkey, cfg->getStringWithDefault("selectKey", "123456789")); allowwildcard=cfg->getIntegerWithDefault("wildcard", 1); if (allowwildcard !=0 && allowwildcard !=1) allowwildcard=1; const char *ek=QueryForKey(db, table, "_property_endkey"); if (ek) { strcpy(endkey, ek); } else { strcpy(endkey, ""); } cfgMaxSeqLen = cfg->getInteger(maxSeqLen); cfgBeep = cfg->getIntegerWithDefault(warningBeep, 1) == 0 ? false : true; cfgAutoCompose = cfg->getIntegerWithDefault(autoCompose, 0) == 0 ? false : true; cfgHitMaxAndCompose = cfg->getIntegerWithDefault(hitMax, 0) == 0 ? false : true; cfgDoShiftSelKey = cfg->getIntegerWithDefault("shiftSelectionKey", 0) == 0 ? false : true; cfgDoChooseInFrontOfCursor = cfg->getIntegerWithDefault("chooseInFrontOfCursor", 1) == 0 ? false : true; cfgDoClearSequenceOnError = cfg->getIntegerWithDefault("clearSequenceOnError", 1) == 0 ? false : true; } const char *OVIMTobacco::identifier() { return idstr; } const char *OVIMTobacco::localizedName(const char *lc) { static char buf[256]; const char *name; if(!strcasecmp(lc,"zh_TW")) { name=QueryForKey(db, table, "_property_cname"); } else { name=QueryForKey(db, table, "_property_ename"); } sprintf(buf,"%s (Tobacco)",name); return buf; } int OVIMTobacco::isEndKey(char c) { if (strchr(endkey, c)) return 1; return 0; } OVIMTobaccoContext::OVIMTobaccoContext(OVIMTobacco *p) : seq(p->table) { parent=p; } void OVIMTobaccoContext::start(OVBuffer* buf, OVCandidate*, OVService* s) { clear(); b = buf; const char* ename = QueryForKey(db, parent->table, "_property_ename"); string inputMethodId(ename); predictor->setInputMethodId(inputMethodId); } void OVIMTobaccoContext::clear() { seq.clear(); b=NULL; candi=NULL; position = 0; canNewSequence = false; doInsert = false; predictor->clearAll(); } void OVIMTobaccoContext::end() { if(b) { if(!b->isEmpty()) b->send(); } clear(); } int OVIMTobaccoContext::keyEvent(OVKeyCode* pk, OVBuffer* pb, OVCandidate* pc, OVService* ps) { k=pk; b=pb; c=pc; s=ps; if (candi) return candidateEvent(); if (isPunctuationCombination()) return punctuationKey(); if (k->isFunctionKey() && b->isEmpty()) return 0; if (k->isCapslock()) { if(!b->isEmpty()) keyCommit(); return keyCapslock(); } if (k->code()==ovkEsc) return keyEsc(); if (k->code()==ovkBackspace || k->code()==ovkDelete) return keyRemove(); if (k->code()==ovkSpace && !seq.isEmpty()) return keyCompose(); if ((k->code()==ovkSpace || k->code()==ovkDown) && !b->isEmpty() && seq.isEmpty()) return setCandidate(); if (k->code()==ovkReturn) return keyCommit(); if (k->code()==ovkLeft || k->code()==ovkRight) return keyMove(); if (isprint(k->code())) return keyPrintable(); return 0; } int OVIMTobaccoContext::keyMove() { size_t currentBufferLength = predictor->tokenVector.size(); if((seq.isEmpty() || parent->doAutoCompose()) && !b->isEmpty() && position >= 0 && position <= currentBufferLength) { /// dirty hack for autoCompose mode... orz if(parent->doAutoCompose()) { if(!seq.isEmpty()) { seq.clear(); position++; } } if(k->code()==ovkLeft) { if(position > 0) position--; else return 0; } else { if(position < predictor->tokenVector.size()) position++; else return 0; } b->update((int)position, (int)position, (int)position); doInsert = true; return 1; } else return 0; } int OVIMTobaccoContext::keyCommit() { if (b->isEmpty()) return 0; if (!seq.isEmpty()) { if (keyCompose()) { b->send(); clear(); return 1; } else return 0; } else { b->send(); clear(); return 1; } } int OVIMTobaccoContext::keyEsc() { if (seq.isEmpty()) return 0; // if buffer is empty, do nothing seq.clear(); // otherwise we clear the syllable freshBuffer(); return 1; } int OVIMTobaccoContext::keyRemove() { if (b->isEmpty()) return 0; if (seq.isEmpty() || parent->doAutoCompose()) { /// dirty hack for autoCompose mode... orz if(parent->doAutoCompose()) { if(!seq.isEmpty()) { seq.clear(); position++; } } if (k->code() == ovkDelete) { murmur("do delete"); if (position < predictor->tokenVector.size()) predictor->removeWord(position, true); else return 0; } else { murmur("do backspace"); if (position > 0) { predictor->removeWord(position, false); position--; } else return 0; } } else seq.remove(); doInsert = true; freshBuffer(); return 1; } int OVIMTobaccoContext::keyPrintable() { if (isalpha(k->code()) && k->isShift() && b->isEmpty() && seq.isEmpty()) { char keystr[2]; sprintf(keystr, "%c", tolower(k->code())); b->clear()->append(keystr)->send(); return 1; } // wildcard hack: currently we don't allow ? * as the first character /* Conflict to punctuation. if (parent->allowwildcard && b->isEmpty() && (k->code()=='?' || k->code()=='*')) return keyNonRadical(); */ if (!seq.add(k->code())) { if (b->isEmpty()) return keyNonRadical(); s->beep(); } if (!seq.isEmpty()) { if (parent->isEndKey(k->code())) { freshBuffer(); return keyCompose(); } else if (parent->doAutoCompose()) { return keyCompose(); } } freshBuffer(); return 1; } void OVIMTobaccoContext::freshBuffer() { /// BAD UTF-8 Chinese character processing here, again... size_t currentBufferLength = predictor->tokenVector.size(); if(strlen(seq.sequence()) > 0 && position < currentBufferLength) { murmur("should be here with [%s], [%s], (%d)", seq.compose(), predictor->composedString.c_str(), position); string::size_type len = predictor->composedString.length(); string leftString, rightString; if(!predictor->composedString.empty()) { leftString = predictor->composedString.substr(0, (position)*3); rightString = predictor-> composedString.substr((position)*3, len-(position)*3); } else { leftString = rightString = ""; murmur("left[%s], right[%s]", leftString.c_str(), rightString.c_str()); } b->clear() ->append(leftString.c_str()) ->append(seq.compose()) ->append(rightString.c_str()) ->update((int)position, (int)position, (int)position + (int)seq.length()); } else { b->clear(); if(predictor->composedString.length() > 0) b->append(predictor->composedString.c_str()); if(strlen(seq.sequence()) > 0) b->append(seq.compose()); b->update((int)position, (int)position, (int)position + (int)seq.length()); } } int OVIMTobaccoContext::keyNonRadical() { char keystr[2]; keystr[0]=k->code(); keystr[1]=0; int count=fetchCandidateWithPrefix("_punctuation_", k->code()); if (!count) { // not a punctuation mark b->clear()->append(keystr)->send(); return 1; } if (count==1) return commitFirstCandidate(); return updateCandidateWindow(); } int OVIMTobaccoContext::isPunctuationCombination() { // only accept CTRL-1 or CTRL-0 if (k->isCtrl() && !k->isOpt() && !k->isCommand() && (k->code()=='1' || k->code()=='0')) return 1; // only accept CTRL-OPT(ALT)-[printable] if (k->isCtrl() && (k->isOpt() || k->isAlt()) && !k->isShift() && ((k->code() >=1 && k->code() <=26) || isprint(k->code()))) return 1; return 0; } int OVIMTobaccoContext::punctuationKey() { int count; char kc=k->code(); if ((kc=='0' || kc=='1') && !k->isOpt()) count=fetchCandidate("_punctuation_list"); else { if (kc >= 1 && kc <= 26) kc+='a'-1; count=fetchCandidateWithPrefix("_ctrl_opt_", kc); } if (!count) return 0; // we send back the combination key string punctuationCharacters("_ctrl_opt_" + kc); if (count > 0) return setPunctuation(punctuationCharacters); return 0; } int OVIMTobaccoContext::setPunctuation(string punctuationCharacters) { if (!candi) return 0; string punctuationString(candi->candidates[0]); predictor->setFixedToken( punctuationCharacters, punctuationString, position); seq.clear(); position++; freshBuffer(); delete candi; candi=NULL; /// candidate selecting does not work for punctuations currently... return 1; } int OVIMTobaccoContext::keyCapslock() { char keystr[2]; keystr[1]=0; if (isprint(k->code())) { if (k->isShift()) keystr[0]=toupper(k->code()); else keystr[0]=tolower(k->code()); b->clear()->append(keystr)->send(); return 1; } return 0; } int OVIMTobaccoContext::keyCompose() { murmur("key composing... [%s], (%d)", seq.compose(), position); std::string characterString(seq.compose()); if(predictor->setTokenVector( characterString, position, parent->doAutoCompose())) { if(!parent->doAutoCompose()) { position++; doInsert = true; seq.clear(); } canNewSequence = true; doInsert = false; freshBuffer(); return 1; } else if(parent->doAutoCompose()) { if(strlen(seq.sequence()) > 1 && canNewSequence) { canNewSequence = false; seq.clear(); seq.add(k->code()); murmur("test here[%s]", seq.sequence()); position++; doInsert = true; } freshBuffer(); return 0; } else { s->beep(); if(parent->doClearSequenceOnError()) seq.clear(); freshBuffer(); return 1; } } int OVIMTobaccoContext::fetchCandidateWithPrefix(const char *prefix, char c) { char keystr[64]; sprintf(keystr, "%s%c", prefix, c); return fetchCandidate(keystr); } int OVIMTobaccoContext::fetchCandidate(const char *qs) { page=0; if (candi) { delete candi; candi=NULL; } int wildcard=0; char realqs[256]; strcpy(realqs, qs); if (parent->allowwildcard && strlen(seq.sequence())>1) { size_t ql=strlen(realqs); for (size_t qi=0; qitable); else sprintf(cmd, "select value from %s where key like ?1;", parent->table); murmur("executing command=%s", cmd); SQLite3Statement *sth=db->prepare(cmd); if (!sth) return 0; sth->bind_text(1, realqs); int rows=0; while (sth->step()==SQLITE_ROW) rows++; murmur("query string=%s, number of candidates=%d", realqs, rows); if (!rows) { delete sth; return 0; } candi=new IMGCandidate; candi->count=0; candi->candidates=new char* [rows]; sth->reset(); while (sth->step()==SQLITE_ROW) { const char *v=sth->column_text(0); char *s=(char*)calloc(1, strlen(v)+1); strcpy(s, v); candi->candidates[candi->count++]=s; } delete sth; return rows; } int OVIMTobaccoContext::setCandidate() { murmur("start to set candidate at (%d)", position); page=0; if (candi) { delete candi; candi=NULL; } size_t choosingIndex = 0; if(parent->doChooseInFrontOfCursor()) if(position > 0) choosingIndex = position - 1; else { if(position == predictor->tokenVector.size() && predictor->tokenVector.size() > 0) choosingIndex = position - 1; else choosingIndex = position; } predictor->candidateVector.clear(); predictor->candidatePositionVector.clear(); predictor->setMultiCharacterWordCandidateVector(choosingIndex); predictor->setSingleCharacterWordCandidateVector(choosingIndex); /// Currently use "post" choosing mode only size_t rows = predictor->candidateVector.size(); murmur("got %d rows", rows); if(!rows) return 1; //zonble candi=new IMGCandidate; candi->count=0; candi->candidates=new char* [rows]; for(size_t i = 0; i < rows; i++) candi->candidates[candi->count++] = const_cast(predictor->candidateVector[i].word.c_str()); return updateCandidateWindow(); } int OVIMTobaccoContext::candidateEvent() { char kc=k->code(); char *localSelKey; if (kc==ovkEsc || kc==ovkBackspace || kc==ovkDelete) { /// ESC/BKSP/DELETE cancels candi window seq.clear(); return closeCandidateWindow(); } if (parent->doShiftSelKey()) { localSelKey = (char*)calloc(1,strlen(parent->selkey) + 2); sprintf(localSelKey," %s",parent->selkey); } else { localSelKey = parent->selkey; } if ((kc==ovkSpace && !parent->doShiftSelKey()) || kc==ovkRight || kc==ovkDown || kc==ovkPageDown || kc =='>') return candidatePageDown(); if (kc==ovkLeft || kc==ovkUp || kc==ovkPageUp || kc=='<') return candidatePageUp(); size_t perpage=strlen(localSelKey); size_t i=0, l=perpage, nextsyl=0; for (i=0; ibeep(); b->update((int)position, (int)position, (int)position); // we do this to make some applications happy } else { size_t choosingIndex; if(parent->doChooseInFrontOfCursor()) { if(position == 0) choosingIndex = 0; else choosingIndex = position - 1; } else { if(position == predictor->tokenVector.size()) choosingIndex = position - 1; else choosingIndex = position; } murmur("set selected candidate(%d, %d)", choosingIndex, i + page*perpage); predictor->setSelectedCandidate(choosingIndex, i + page*perpage); b->clear()->append(predictor->composedString.c_str()); closeCandidateWindow(); if (nextsyl) { seq.add(kc); b->append(seq.compose()); b->update((int)position, (int)position - 1, (int)position); } else b->update((int)position, (int)position, (int)position); } return 1; } int OVIMTobaccoContext::updateCandidateWindow() { if (!candi) return 1; size_t candicount=candi->count; size_t perpage=strlen(parent->selkey); size_t pgstart=page*perpage; c->clear(); char dispstr[32]; for (size_t i=0; i= candicount) break; // stop if idx exceeds candi counts sprintf(dispstr, "%c.", parent->selkey[i]); c->append(dispstr)->append(candi->candidates[page*perpage+i])->append(" "); } // add current page number sprintf(dispstr, "(%d/%lu)", page+1, (candicount-1)/perpage +1); c->append(dispstr); c->update(); if (!c->onScreen()) c->show(); b->update((int)position, (int)position, (int)position); return 1; } int OVIMTobaccoContext::closeCandidateWindow() { seq.clear(); if (c->onScreen()) c->hide()->clear()->update(); if (candi) { //< b6s: this crashed win32 version. //delete candi; candi=NULL; } return 1; } int OVIMTobaccoContext::commitFirstCandidate() { if (!candi) return 1; b->clear()->append(candi->candidates[0])->send(); return closeCandidateWindow(); } int OVIMTobaccoContext::candidatePageUp() { int maxpage=(candi->count-1) / strlen(parent->selkey); if (!maxpage) s->beep(); else { if (!page) page=maxpage; else page--; updateCandidateWindow(); } return 1; } int OVIMTobaccoContext::candidatePageDown() { int maxpage=(candi->count-1) / strlen(parent->selkey); if (!maxpage) s->beep(); else { if (page==maxpage) page=0; else page++; updateCandidateWindow(); } return 1; } int IMGKeySequence::isValidKey(char c) { if (query(c)) return 1; return 0; } bool IMGKeySequence::add(char c) { if (query(c)) return OVKeySequenceSimple::add(tolower(c)); return 0; } int IMGKeySequence::isEmpty() { if (len) return 0; return 1; } const char *IMGKeySequence::query(char c) { char keystr[32]; sprintf(keystr, "_key_%c", tolower(c)); return QueryForKey(db, table, keystr); } const char *IMGKeySequence::compose() { strcpy(composebuf, ""); for (int i=0; iprepare(command); if (sth) { if (sth->step()==SQLITE_ROW) { strcpy(valuebuffer, sth->column_text(0)); murmur("return value=%s", valuebuffer); r=valuebuffer; } delete sth; } return r; } const char *QueryForKey(SQLite3 *db, const char *tbl, const char *key) { char cmd[256]; char keyescaped[128]; const char *kp=key; char *ep=keyescaped; while (*kp) { if (*kp=='\'') { *ep++='\''; *ep++='\''; } else *ep++=*kp; kp++; } *ep=0; sprintf(cmd, "select value from %s where key='%s';", tbl, keyescaped); return QueryForCommand(db, cmd); }