clean up PYConfig.h/cc
[platform/upstream/ibus-libpinyin.git] / src / PYEnglishEditor.cc
1 /* vim:set et ts=4 sts=4:
2  *
3  * ibus-pinyin - The Chinese PinYin engine for IBus
4  *
5  * Copyright (c) 2010-2011 Peng Wu <alexepico@gmail.com>
6  *
7  * This program is free software; you can redistribute it and/or modify
8  * it under the terms of the GNU General Public License as published by
9  * the Free Software Foundation; either version 2, or (at your option)
10  * any later version.
11  *
12  * This program is distributed in the hope that it will be useful,
13  * but WITHOUT ANY WARRANTY; without even the implied warranty of
14  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15  * GNU General Public License for more details.
16  *
17  * You should have received a copy of the GNU General Public License
18  * along with this program; if not, write to the Free Software
19  * Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
20  */
21
22 #include "PYEnglishEditor.h"
23 #include <string.h>
24 #include <string>
25 #include <vector>
26 #include <stdio.h>
27 #include <sqlite3.h>
28 #include <glib.h>
29 #include <glib/gstdio.h>
30 #include "PYConfig.h"
31 #include "PYString.h"
32
33
34 namespace PY {
35
36 #define DB_BACKUP_TIMEOUT   (60)
37
38 class EnglishDatabase{
39 public:
40     EnglishDatabase(){
41         m_sqlite = NULL;
42         m_sql = "";
43         m_user_db = "";
44         m_timeout_id = 0;
45         m_timer = g_timer_new ();
46     }
47
48     ~EnglishDatabase(){
49         g_timer_destroy (m_timer);
50         if (m_timeout_id != 0) {
51             saveUserDB ();
52             g_source_remove (m_timeout_id);
53         }
54
55         if (m_sqlite){
56             sqlite3_close (m_sqlite);
57             m_sqlite = NULL;
58         }
59         m_sql = "";
60         m_user_db = NULL;
61     }
62
63     gboolean isDatabaseExisted(const char *filename) {
64          gboolean result = g_file_test (filename, G_FILE_TEST_IS_REGULAR);
65          if (!result)
66              return FALSE;
67
68          sqlite3 *tmp_db = NULL;
69          if (sqlite3_open_v2 (filename, &tmp_db,
70                               SQLITE_OPEN_READONLY, NULL) != SQLITE_OK){
71              return FALSE;
72          }
73
74          /* Check the desc table */
75          sqlite3_stmt *stmt = NULL;
76          const char *tail = NULL;
77          m_sql = "SELECT value FROM desc WHERE name = 'version';";
78          result = sqlite3_prepare_v2 (tmp_db, m_sql.c_str(), -1, &stmt, &tail);
79          g_assert (result == SQLITE_OK);
80          result = sqlite3_step (stmt);
81          if (result != SQLITE_ROW)
82              return FALSE;
83          result = sqlite3_column_type (stmt, 0);
84          if (result != SQLITE_TEXT)
85              return FALSE;
86          const char *version = (const char *) sqlite3_column_text (stmt, 0);
87          if (strcmp("1.2.0", version ) != 0)
88              return FALSE;
89          result = sqlite3_finalize (stmt);
90          g_assert (result == SQLITE_OK);
91          sqlite3_close (tmp_db);
92          return TRUE;
93     }
94
95     gboolean createDatabase(const char *filename) {
96         /* unlink the old database. */
97         gboolean retval = g_file_test (filename, G_FILE_TEST_IS_REGULAR);
98         if (retval) {
99             int result = g_unlink (filename);
100             if (result == -1)
101                 return FALSE;
102         }
103
104         char *dirname = g_path_get_dirname (filename);
105         g_mkdir_with_parents (dirname, 0700);
106         g_free (dirname);
107
108         sqlite3 *tmp_db = NULL;
109         if (sqlite3_open_v2 (filename, &tmp_db,
110                              SQLITE_OPEN_READWRITE | 
111                              SQLITE_OPEN_CREATE, NULL) != SQLITE_OK) {
112             return FALSE;
113         }
114
115         /* Create DESCription table */
116         m_sql = "BEGIN TRANSACTION;\n";
117         m_sql << "CREATE TABLE IF NOT EXISTS desc (name TEXT PRIMARY KEY, value TEXT);\n";
118         m_sql << "INSERT OR IGNORE INTO desc VALUES ('version', '1.2.0');";
119         m_sql << "COMMIT;\n";
120
121         if (!executeSQL (tmp_db)) {
122             sqlite3_close (tmp_db);
123             return FALSE;
124         }
125
126         /* Create Schema */
127         m_sql = "CREATE TABLE IF NOT EXISTS english ("
128                 "word TEXT NOT NULL PRIMARY KEY,"
129                 "freq FLOAT NOT NULL DEFAULT(0)"
130                 ");";
131         if (!executeSQL (tmp_db)) {
132             sqlite3_close (tmp_db);
133             return FALSE;
134         }
135         return TRUE;
136     }
137
138     gboolean openDatabase(const char *system_db, const char *user_db){
139         if (!isDatabaseExisted (system_db))
140             return FALSE;
141         if (!isDatabaseExisted (user_db)) {
142             gboolean result = createDatabase (user_db);
143             if (!result)
144                 return FALSE;
145         }
146         /* cache the user db name. */
147         m_user_db = user_db;
148
149         /* do database attach here. :) */
150         if (sqlite3_open_v2 (system_db, &m_sqlite,
151                              SQLITE_OPEN_READWRITE |
152                              SQLITE_OPEN_CREATE, NULL) != SQLITE_OK) {
153             m_sqlite = NULL;
154             return FALSE;
155         }
156
157 #if 0
158         m_sql.printf (SQL_ATTACH_DB, user_db);
159         if (!executeSQL (m_sqlite)) {
160             sqlite3_close (m_sqlite);
161             m_sqlite = NULL;
162             return FALSE;
163         }
164         return TRUE;
165 #endif
166         return loadUserDB();
167     }
168
169     /* List the words in freq order. */
170     gboolean listWords(const char *prefix, std::vector<std::string> & words){
171         sqlite3_stmt *stmt = NULL;
172         const char *tail = NULL;
173         words.clear ();
174
175         /* list words */
176         const char *SQL_DB_LIST = 
177             "SELECT word FROM ( "
178             "SELECT * FROM english UNION ALL SELECT * FROM userdb.english) "
179             " WHERE word LIKE '%s%%' GROUP BY word ORDER BY SUM(freq) DESC;";
180         m_sql.printf (SQL_DB_LIST, prefix);
181         int result = sqlite3_prepare_v2 (m_sqlite, m_sql.c_str(), -1, &stmt, &tail);
182         g_assert(result == SQLITE_OK);
183         result = sqlite3_step (stmt);
184         while (result == SQLITE_ROW){
185             /* get the words. */
186             result = sqlite3_column_type (stmt, 0);
187             if (result != SQLITE_TEXT)
188                 return FALSE;
189             const char *word = (const char *)sqlite3_column_text (stmt, 0);
190             words.push_back (word);
191             result = sqlite3_step (stmt);
192         }
193         sqlite3_finalize (stmt);
194         if (result != SQLITE_DONE)
195             return FALSE;
196         return TRUE;
197     }
198
199     /* Get the freq of user sqlite db. */
200     gboolean getWordInfo(const char *word, float & freq){
201         sqlite3_stmt *stmt = NULL;
202         const char *tail = NULL;
203         /* get word info. */
204         const char *SQL_DB_SELECT = 
205             "SELECT freq FROM userdb.english WHERE word = \"%s\";";
206         m_sql.printf (SQL_DB_SELECT, word);
207         int result = sqlite3_prepare_v2 (m_sqlite, m_sql.c_str(), -1, &stmt, &tail);
208         g_assert (result == SQLITE_OK);
209         result = sqlite3_step (stmt);
210         if (result != SQLITE_ROW)
211             return FALSE;
212         result = sqlite3_column_type (stmt, 0);
213         if (result != SQLITE_FLOAT)
214             return FALSE;
215         freq = sqlite3_column_double (stmt, 0);
216         result = sqlite3_finalize (stmt);
217         g_assert (result == SQLITE_OK);
218         return TRUE;
219     }
220
221     /* Update the freq with delta value. */
222     gboolean updateWord(const char *word, float freq){
223         const char *SQL_DB_UPDATE =
224             "UPDATE userdb.english SET freq = \"%f\" WHERE word = \"%s\";";
225         m_sql.printf (SQL_DB_UPDATE, freq, word);
226         gboolean retval =  executeSQL (m_sqlite);
227         modified ();
228         return retval;
229     }
230
231     /* Insert the word into user db with the initial freq. */
232     gboolean insertWord(const char *word, float freq){
233         const char *SQL_DB_INSERT =
234             "INSERT INTO userdb.english (word, freq) VALUES (\"%s\", \"%f\");";
235         m_sql.printf (SQL_DB_INSERT, word, freq);
236         gboolean retval = executeSQL (m_sqlite);
237         modified ();
238         return retval;
239     }
240
241 private:
242     gboolean executeSQL(sqlite3 *sqlite){
243         gchar *errmsg = NULL;
244         if (sqlite3_exec (sqlite, m_sql.c_str (), NULL, NULL, &errmsg)
245              != SQLITE_OK) {
246             g_warning ("%s: %s", errmsg, m_sql.c_str());
247             sqlite3_free (errmsg);
248             return FALSE;
249         }
250         m_sql.clear ();
251         return TRUE;
252     }
253
254     gboolean loadUserDB (void){
255         sqlite3 *userdb =  NULL;
256         /* Attach user database */
257         do {
258             const char *SQL_ATTACH_DB =
259                 "ATTACH DATABASE ':memory:' AS userdb;";
260             m_sql.printf (SQL_ATTACH_DB);
261             if (!executeSQL (m_sqlite))
262                 break;
263
264             /* Note: user db is always created by openDatabase. */
265             if (sqlite3_open_v2 ( m_user_db, &userdb,
266                                   SQLITE_OPEN_READWRITE |
267                                   SQLITE_OPEN_CREATE, NULL) != SQLITE_OK)
268                 break;
269
270             sqlite3_backup *backup = sqlite3_backup_init (m_sqlite, "userdb", userdb, "main");
271
272             if (backup) {
273                 sqlite3_backup_step (backup, -1);
274                 sqlite3_backup_finish (backup);
275             }
276
277             sqlite3_close (userdb);
278             return TRUE;
279         } while (0);
280
281         if (userdb)
282             sqlite3_close (userdb);
283         return FALSE;
284     }
285
286     gboolean saveUserDB (void){
287         sqlite3 *userdb = NULL;
288         String tmpfile = String(m_user_db) + "-tmp";
289         do {
290             /* remove tmpfile if it exist */
291             g_unlink(tmpfile);
292
293             if (sqlite3_open_v2 (tmpfile, &userdb,
294                                  SQLITE_OPEN_READWRITE |
295                                  SQLITE_OPEN_CREATE, NULL) != SQLITE_OK)
296                 break;
297
298             sqlite3_backup *backup = sqlite3_backup_init (userdb, "main", m_sqlite, "userdb");
299
300             if (backup == NULL)
301                 break;
302
303             sqlite3_backup_step (backup, -1);
304             sqlite3_backup_finish (backup);
305             sqlite3_close (userdb);
306
307             g_rename(tmpfile, m_user_db);
308             return TRUE;
309         } while (0);
310
311         if (userdb)
312             sqlite3_close (userdb);
313         g_unlink (tmpfile);
314         return FALSE;
315     }
316
317     void modified (void){
318         /* Restart the timer */
319         g_timer_start (m_timer);
320
321         if (m_timeout_id != 0)
322             return;
323
324         m_timeout_id = g_timeout_add_seconds (DB_BACKUP_TIMEOUT,
325                                               EnglishDatabase::timeoutCallback,
326                                               static_cast<gpointer> (this));
327     }
328
329     static gboolean timeoutCallback (gpointer data){
330         EnglishDatabase *self = static_cast<EnglishDatabase *> (data);
331
332         /* Get elapsed time since last modification of database. */
333         guint elapsed = (guint) g_timer_elapsed (self->m_timer, NULL);
334
335         if (elapsed >= DB_BACKUP_TIMEOUT &&
336             self->saveUserDB ()) {
337             self->m_timeout_id = 0;
338             return FALSE;
339         }
340
341         return TRUE;
342     }
343
344     sqlite3 *m_sqlite;
345     String m_sql;
346     const char *m_user_db;
347
348     guint m_timeout_id;
349     GTimer *m_timer;
350 };
351
352 EnglishEditor::EnglishEditor (PinyinProperties & props, Config &config)
353     : Editor (props, config), m_train_factor (0.1)
354 {
355     m_english_database = new EnglishDatabase;
356
357     gchar *path = g_build_filename (g_get_user_cache_dir (),
358                                      "ibus", "pinyin", "english-user.db", NULL);
359
360     gboolean result = m_english_database->openDatabase
361         (".." G_DIR_SEPARATOR_S "data" G_DIR_SEPARATOR_S "english.db",
362          "english-user.db") ||
363         m_english_database->openDatabase
364         (PKGDATADIR G_DIR_SEPARATOR_S "db" G_DIR_SEPARATOR_S "english.db", path);
365     if (!result)
366         g_warning ("can't open english word list database.\n");
367 }
368
369 EnglishEditor::~EnglishEditor ()
370 {
371     delete m_english_database;
372     m_english_database = NULL;
373 }
374
375 gboolean
376 EnglishEditor::processKeyEvent (guint keyval, guint keycode, guint modifiers)
377 {
378     //IBUS_SHIFT_MASK is removed.
379     modifiers &= (IBUS_CONTROL_MASK |
380                   IBUS_MOD1_MASK |
381                   IBUS_SUPER_MASK |
382                   IBUS_HYPER_MASK |
383                   IBUS_META_MASK |
384                   IBUS_LOCK_MASK);
385     if (modifiers)
386         return FALSE;
387
388     //handle backspace/delete here.
389     if (processEditKey (keyval))
390         return TRUE;
391
392     //handle page/cursor up/down here.
393     if (processPageKey (keyval))
394         return TRUE;
395
396     //handle label key select here.
397     if (processLabelKey (keyval))
398         return TRUE;
399
400     if (processSpace (keyval))
401         return TRUE;
402
403     if (processEnter (keyval))
404         return TRUE;
405
406     m_cursor = std::min (m_cursor, (guint)m_text.length ());
407
408     /* Remember the input string. */
409     if (m_cursor == 0) {
410         g_return_val_if_fail ('v' == keyval, FALSE);
411         m_text = "v";
412         m_cursor ++;
413     }
414     else {
415         g_return_val_if_fail ('v' == m_text[0], FALSE);
416         if ((keyval >= 'a' && keyval <= 'z') ||
417             (keyval >= 'A' && keyval <= 'Z')) {
418             m_text.insert (m_cursor, keyval);
419             m_cursor++;
420         }
421     }
422
423     /* Deal other staff with updateStateFromInput (). */
424     updateStateFromInput ();
425     update ();
426     return TRUE;
427 }
428
429 gboolean
430 EnglishEditor::processEditKey (guint keyval)
431 {
432     switch (keyval) {
433     case IBUS_Delete:
434     case IBUS_KP_Delete:
435         removeCharAfter ();
436         updateStateFromInput ();
437         update ();
438         return TRUE;
439     case IBUS_BackSpace:
440         removeCharBefore ();
441         updateStateFromInput ();
442         update ();
443         return TRUE;
444     }
445     return FALSE;
446 }
447
448 gboolean
449 EnglishEditor::processPageKey (guint keyval)
450 {
451     switch (keyval) {
452     case IBUS_comma:
453         if (m_config.commaPeriodPage ()) {
454             pageUp ();
455             return TRUE;
456         }
457         break;
458     case IBUS_minus:
459         if (m_config.minusEqualPage ()) {
460             pageUp ();
461             return TRUE;
462         }
463         break;
464     case IBUS_period:
465         if (m_config.commaPeriodPage ()) {
466             pageDown ();
467             return TRUE;
468         }
469         break;
470     case IBUS_equal:
471         if (m_config.minusEqualPage ()) {
472             pageDown ();
473             return TRUE;
474         }
475         break;
476     case IBUS_Up:
477     case IBUS_KP_Up:
478         cursorUp ();
479         return TRUE;
480
481     case IBUS_Down:
482     case IBUS_KP_Down:
483         cursorDown ();
484         return TRUE;
485
486     case IBUS_Page_Up:
487     case IBUS_KP_Page_Up:
488         pageUp ();
489         return TRUE;
490
491     case IBUS_Page_Down:
492     case IBUS_KP_Page_Down:
493         pageDown ();
494         return TRUE;
495
496     case IBUS_Escape:
497         reset ();
498         return TRUE;
499     }
500     return FALSE;
501 }
502
503 gboolean
504 EnglishEditor::processLabelKey (guint keyval)
505 {
506     switch (keyval) {
507     case '1' ... '9':
508         return selectCandidateInPage (keyval - '1');
509         break;
510     case '0':
511         return selectCandidateInPage (9);
512         break;
513     }
514     return FALSE;
515 }
516
517 gboolean
518 EnglishEditor::processEnter (guint keyval){
519     if (keyval != IBUS_Return)
520         return FALSE;
521
522     if (m_text.length () == 0)
523         return FALSE;
524
525     String preedit = m_text.substr (1);
526     Text text (preedit);
527     commitText (text);
528     train (preedit.c_str (), m_train_factor);
529     reset ();
530     return TRUE;
531 }
532
533 gboolean
534 EnglishEditor::processSpace (guint keyval)
535 {
536     if (!(keyval == IBUS_space || keyval == IBUS_KP_Space))
537         return FALSE;
538
539     guint cursor_pos = m_lookup_table.cursorPos ();
540     return selectCandidate (cursor_pos);
541 }
542
543 void
544 EnglishEditor::candidateClicked (guint index, guint button, guint state)
545 {
546     selectCandidateInPage (index);
547 }
548
549 gboolean
550 EnglishEditor::selectCandidateInPage (guint index)
551 {
552     guint page_size = m_lookup_table.pageSize ();
553     guint cursor_pos = m_lookup_table.cursorPos ();
554
555     if (G_UNLIKELY (index >= page_size))
556         return FALSE;
557     index += (cursor_pos / page_size) * page_size;
558
559     return selectCandidate (index);
560 }
561
562 gboolean
563 EnglishEditor::selectCandidate (guint index)
564 {
565     if (index >= m_lookup_table.size ())
566         return FALSE;
567
568     IBusText *candidate = m_lookup_table.getCandidate (index);
569     Text text (candidate);
570     commitText (text);
571     train (candidate->text, m_train_factor);
572     reset ();
573     return TRUE;
574 }
575
576 gboolean
577 EnglishEditor::updateStateFromInput (void)
578 {
579     /* Do parse and candidates update here. */
580     /* prefix v double check here. */
581     if (m_text.empty ()) {
582         m_preedit_text = "";
583         m_auxiliary_text = "";
584         m_cursor = 0;
585         clearLookupTable ();
586         return FALSE;
587     }
588
589     if ('v' != m_text[0]) {
590         g_warning ("v is expected in m_text string.\n");
591         m_auxiliary_text = "";
592         clearLookupTable ();
593         return FALSE;
594     }
595
596     m_auxiliary_text = "v";
597     if (1 == m_text.length ()) {
598         clearLookupTable ();
599         return TRUE;
600     }
601
602     m_auxiliary_text += " ";
603
604     String prefix = m_text.substr (1);
605     m_auxiliary_text += prefix;
606
607     /* lookup table candidate fill here. */
608     std::vector<std::string> words;
609     gboolean retval = m_english_database->listWords (prefix.c_str (), words);
610     if (!retval)
611         return FALSE;
612
613     clearLookupTable ();
614     std::vector<std::string>::iterator iter;
615     for (iter = words.begin (); iter != words.end (); ++iter){
616         Text text (*iter);
617         m_lookup_table.appendCandidate (text);
618     }
619     return TRUE;
620 }
621
622 /* Auxiliary Functions */
623
624 void
625 EnglishEditor::pageUp (void)
626 {
627     if (G_LIKELY (m_lookup_table.pageUp ())) {
628         update ();
629     }
630 }
631
632 void
633 EnglishEditor::pageDown (void)
634 {
635     if (G_LIKELY (m_lookup_table.pageDown ())) {
636         update ();
637     }
638 }
639
640 void
641 EnglishEditor::cursorUp (void)
642 {
643     if (G_LIKELY (m_lookup_table.cursorUp ())) {
644         update ();
645     }
646 }
647
648 void
649 EnglishEditor::cursorDown (void)
650 {
651     if (G_LIKELY (m_lookup_table.cursorDown ())) {
652         update ();
653     }
654 }
655
656 void
657 EnglishEditor::update (void)
658 {
659     updateLookupTable ();
660     updatePreeditText ();
661     updateAuxiliaryText ();
662 }
663
664 void
665 EnglishEditor::reset (void)
666 {
667     m_text = "";
668     updateStateFromInput ();
669     update ();
670 }
671
672 void
673 EnglishEditor::clearLookupTable (void)
674 {
675     m_lookup_table.clear ();
676     m_lookup_table.setPageSize (m_config.pageSize ());
677     m_lookup_table.setOrientation (m_config.orientation ());
678 }
679
680 void
681 EnglishEditor::updateLookupTable (void)
682 {
683     if (m_lookup_table.size ()) {
684         Editor::updateLookupTableFast (m_lookup_table, TRUE);
685     }
686     else {
687         hideLookupTable ();
688     }
689 }
690
691 void
692 EnglishEditor::updatePreeditText (void)
693 {
694     if (G_UNLIKELY (m_preedit_text.empty ())) {
695         hidePreeditText ();
696         return;
697     }
698
699     StaticText preedit_text (m_preedit_text);
700     Editor::updatePreeditText (preedit_text, m_cursor, TRUE);
701 }
702
703 void
704 EnglishEditor::updateAuxiliaryText (void)
705 {
706     if (G_UNLIKELY (m_auxiliary_text.empty ())) {
707         hideAuxiliaryText ();
708         return;
709     }
710     
711     StaticText aux_text (m_auxiliary_text);
712     Editor::updateAuxiliaryText (aux_text, TRUE);
713 }
714
715 gboolean
716 EnglishEditor::removeCharBefore (void)
717 {
718     if (G_UNLIKELY (m_cursor <= 0)) {
719         m_cursor = 0;
720         return FALSE;
721     }
722
723     if (G_UNLIKELY (m_cursor > m_text.length ())) {
724         m_cursor = m_text.length ();
725         return FALSE;
726     }
727
728     m_text.erase (m_cursor - 1, 1);
729     m_cursor = std::max (0, static_cast<int>(m_cursor) - 1);
730     return TRUE;
731 }
732
733 gboolean
734 EnglishEditor::removeCharAfter (void)
735 {
736     if (G_UNLIKELY (m_cursor < 0)) {
737         m_cursor = 0;
738         return FALSE;
739     }
740
741     if (G_UNLIKELY (m_cursor >= m_text.length ())) {
742         m_cursor = m_text.length ();
743         return FALSE;
744     }
745
746     m_text.erase (m_cursor, 1);
747     m_cursor = std::min (m_cursor, (guint) m_text.length ());
748     return TRUE;
749 }
750
751 gboolean
752 EnglishEditor::train (const char *word, float delta)
753 {
754     float freq = 0;
755     gboolean retval = m_english_database->getWordInfo (word, freq);
756     if (retval) {
757         freq += delta;
758         m_english_database->updateWord (word, freq);
759     } else {
760         m_english_database->insertWord (word, delta);
761     }
762     return TRUE;
763 }
764
765 #if 0
766
767 /* using static initializor to test english database here. */
768 static class TestEnglishDatabase{
769 public:
770     TestEnglishDatabase (){
771         EnglishDatabase *db = new EnglishDatabase ();
772         bool retval = db->isDatabaseExisted ("/tmp/english-user.db");
773         g_assert (!retval);
774         retval = db->createDatabase ("english-user.db");
775         g_assert (retval);
776         retval = db->openDatabase ("english.db", "english-user.db");
777         g_assert (retval);
778         float freq = 0;
779         retval = db->getWordInfo ("hello", freq);
780         printf ("word hello:%d, %f.\n", retval, freq);
781         if (retval) {
782             db->updateWord ("hello", 0.1);
783         } else {
784             db->insertWord ("hello", 0.1);
785         }
786         printf ("english database test ok.\n");
787     }
788 } test_english_database;
789
790 #endif
791 };