new widget Label; ManageDB: setters, dummy callbacks
[aoi.git] / src / aoi.cxx
blobe90c66937a9919179d4bc279ffdf7a7f259d7dd1
1 /*
2 Copyright 2013 Karel Matas
4 This program is free software: you can redistribute it and/or modify
5 it under the terms of the GNU General Public License as published by
6 the Free Software Foundation, either version 3 of the License, or
7 (at your option) any later version.
9 This program is distributed in the hope that it will be useful,
10 but WITHOUT ANY WARRANTY; without even the implied warranty of
11 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 GNU General Public License for more details.
14 You should have received a copy of the GNU General Public License
15 along with this program. If not, see <http://www.gnu.org/licenses/>.
17 #include <cmath>
18 #include <set>
19 #include <FL/fl_ask.H>
20 #include "aoi.hxx"
21 #include "utils.hxx"
23 namespace aoi {
25 using std::set;
26 using aoi_ui::TextStyle;
27 using utils::to_string;
29 const char *LOG_FILE = "aoi.log";
30 const char SENSE_SEARCH_CHAR = ':';
32 App* App::instance_ = nullptr;
34 App::App ()
36 remove(LOG_FILE);
37 logger_.filename(LOG_FILE);
38 logger_.loglevel(Logger::MSG_DEBUG);
40 cfg_ = new aoi_config::Config();
41 ui_ = new aoi_ui::GUI(this);
42 rmn_ = new Romanization();
44 ui_->progress(0,"Opening database...");
46 try {
47 db_ = new SQLite3::SQLite3( get_config("db/file_main").c_str() );
49 catch (SQLite3::CantOpenDatabase &e){
50 log_e( "Aoi: Can't open database '" + string(e.what()) + "'.");
53 std::stringstream ss;
54 ss << "ATTACH DATABASE '" << get_config("db/file_user") << "'as user;";
55 query(ss.str().c_str());
57 ui_->progress( 30, "Checking tables..." );
58 check_tables();
59 ui_->progress( 60, "Checking indexes..." );
60 check_indexes();
62 load_config();
63 init_dicview();
65 #ifdef DEBUG
66 // ui_->progress(90, "DEBUG query..." );
67 // parse_dic_input("ana*");
68 // cb_kanji_search();
69 // ui_->cb_toggle_group(nullptr);
70 #endif
72 ui_->progress( 98, "Initializing fonts..." );
73 ui_->fontname_kanji( get_config("font/kanji") );
74 ui_->help_file( get_config("sources/help_index") );
76 ui_->progress_hide();
79 auto q = query("select val from aoi where key='jmdict_version'");
80 string jmdict_version = (q.empty()) ? "NONE":q[0];
81 q = query("select val from aoi where key='kanjidic_version'");
82 string kanjidic_version = q.empty() ? "NONE":q[0];
83 if ( jmdict_version == "NONE" || kanjidic_version == "NONE" )
84 cb_manage_db();
87 #ifdef DEBUG
88 logger_.loglevel(Logger::MSG_DEBUG);
89 #endif
91 instance_ = this;
95 App::~App ()
97 delete db_;
98 delete rmn_;
99 delete ui_;
100 delete cfg_;
104 void App::init_dicview ()
106 // initialize textstyles
107 TextStyle style_default(FL_HELVETICA,1.2);
108 TextStyle style_reading(aoi_ui::FONT_KANJI,1.5);
109 TextStyle style_reading_freq = style_reading;
110 style_reading_freq.color = get_color("frequent");
111 TextStyle style_kanji(aoi_ui::FONT_KANJI,1.7);
112 TextStyle style_kanji_freq = style_kanji;
113 style_kanji_freq.color = get_color("frequent");
114 TextStyle style_inf(FL_HELVETICA_ITALIC,0.8,get_color("pos"));
115 style_inf.offset_y = 3;
116 TextStyle style_pos = style_inf;
117 TextStyle style_misc = style_pos;
118 style_misc.color = get_color("misc");
119 TextStyle style_field = style_pos;
120 style_field.color = get_color("field");
121 TextStyle style_dial = style_pos;
123 // register TextStyles
124 ui_->register_tag_dicview( "default", style_default );
125 ui_->register_tag_dicview( "reading", style_reading );
126 ui_->register_tag_dicview( "reading_freq", style_reading_freq );
127 ui_->register_tag_dicview( "kanji", style_kanji );
128 ui_->register_tag_dicview( "kanji_freq", style_kanji_freq );
129 ui_->register_tag_dicview( "kinf", style_inf );
130 ui_->register_tag_dicview( "rinf", style_inf );
131 ui_->register_tag_dicview( "pos", style_pos );
132 ui_->register_tag_dicview( "misc", style_misc );
133 ui_->register_tag_dicview( "field", style_field );
134 ui_->register_tag_dicview( "dial", style_dial );
138 vector<string> App::query ( const char *q, bool log_query, bool replace_separator )
140 if ( !db_ ){
141 log_e("App::query(): Database does not exist.");
142 return {};
144 ui_->cursor_wait();
145 vector<string> result;
146 try {
147 if ( log_query )
148 log_d(string(q));
149 result = db_->query(q);
151 catch (SQLite3::DatabaseError &e){
152 log_e("App: DatabaseError:: " + string(e.what()) + string("\nQuery: ")
153 + string(e.query()) );
154 ui_->cursor_default();
156 string msg = std::to_string(db_->result_rows()) + " results";
157 log( "App::query(): " + msg);
158 if ( replace_separator )
159 for ( string &s: result )
160 utils::replace_all(s, parsers::SEPARATOR_SQL, ", ");
161 ui_->cursor_default();
162 return result;
166 void App::cb_set_components ()
168 if ( curr_components_.empty() )
169 return;
171 using aoi_ui::ComponentView;
173 // prepare cells
174 vector<string> included = ui_->components_include();
175 vector<string> excluded = ui_->components_exclude();
177 vector<ComponentView::Cell> v;
178 for ( string &s: included )
179 v.push_back( ComponentView::Cell(s,ComponentView::CELL_SELECTED_1) );
180 for ( string &s: excluded )
181 v.push_back( ComponentView::Cell(s,ComponentView::CELL_SELECTED_2) );
183 // sort by occurences
184 if ( !ui_->sort_components_by_strokes() ){
185 for ( auto mi = curr_components_.rbegin(); mi!=curr_components_.rend(); ++mi ){
186 if ( !utils::is_in( included, mi->second )
187 && !utils::is_in( excluded, mi->second ) )
188 v.push_back( ComponentView::Cell(mi->second) );
191 // sort by strokes
192 else {
193 vector<string> comps_by_strokes;
194 for ( auto mi: curr_components_ ){
195 if ( mi.first < get_config<int>("knj/min_compo_count") )
196 continue;
197 comps_by_strokes.push_back(mi.second);
200 struct SortByStrokes {
201 map<string,int> comps;
202 SortByStrokes( const map<string,int> &c ): comps(c){};
203 bool operator() (const string &c1, const string &c2 ){
204 return ( this->comps[c1] < this->comps[c2] );
206 } sbc(components_);
208 std::sort( comps_by_strokes.begin(), comps_by_strokes.end(), sbc );
210 int prev_strokes=0;
211 for ( string &s: comps_by_strokes ){
212 int curr_strokes = components_[s];
213 if ( prev_strokes != curr_strokes )
214 v.push_back( ComponentView::Cell( std::to_string(curr_strokes),
215 ComponentView::CELL_LABEL) );
216 v.push_back( ComponentView::Cell(s) );
217 prev_strokes = curr_strokes;
221 ui_->set_components( v );
225 void App::cb_kanji_search ()
228 if ( components_.empty() ){
229 log("Loading components...");
230 vector<string> res = query("select component, strokes from components");
231 for ( size_t i=0; i<res.size(); i=i+2 ){
232 components_[res[i]] = std::stoi(res[i+1]);
236 // strokes
237 std::pair<int,int> strokes = utils::parse_range( utils::strip( ui_->strokes() ) );
238 // jlpt
239 std::pair<int,int> jlpt = utils::parse_range( utils::strip( ui_->jlpt() ) );
240 // grade
241 std::pair<int,int> grade = utils::parse_range( utils::strip( ui_->grade() ) );
245 // skip
246 vector<string> skip = utils::split_string( ui_->skip() );
247 std::stringstream sskip;
248 if ( skip.size() > 0 ){
249 string skip1 = utils::strip(skip[0].c_str());
250 sskip << " S.skip1=" << skip1;
252 if ( skip.size() > 1 ){
253 std::pair<int,int> skip2 = utils::parse_range( utils::strip(skip[1].c_str()) );
254 sskip << " and S.skip2>=" << skip2.first << " and S.skip2<=" << skip2.second;
256 if ( skip.size() > 2 ){
257 std::pair<int,int> skip3 = utils::parse_range( utils::strip(skip[2].c_str()) );
258 sskip << " and S.skip3>=" << skip3.first << " and S.skip3<=" << skip3.second;
260 if ( !sskip.str().empty() )
261 sskip << " and ";
263 // ordering
264 string order_by;
265 switch ( ui_->sort_mode() ){
266 case SORT_FREQ_ASC:
267 order_by = "(case freq when 0 then 9999 else freq end) asc";
268 break;
269 case SORT_FREQ_DESC:
270 order_by = "(case freq when 0 then 9999 else freq end) desc";
271 break;
272 case SORT_STROKES_ASC:
273 order_by = "strokes asc";
274 break;
275 case SORT_STROKES_DESC:
276 order_by = "strokes desc";
277 break;
280 vector<string> components_include = ui_->components_include();
281 vector<string> components_exclude = ui_->components_exclude();
282 std::stringstream comps;
283 for ( auto c: components_include )
284 comps << " and components like '%" << c << "%'";
285 for ( auto c: components_exclude )
286 comps << " and components not like '%" << c << "%'";
287 printf("%s\n",comps.str().c_str());
289 string jis208 = "";
290 if ( get_config<bool>("knj/jis208_only") )
291 jis208 = " flags glob '*jis208*' and ";
293 // build query
294 std::stringstream ss;
295 ss << "select distinct "
296 << "K.kanji,freq,components,flags "
297 << " from k_kanji as K"
298 << (sskip.str().empty() ? " where ":", k_skip as S where K.kanji=S.kanji and")
299 << jis208
300 << sskip.str()
301 << " strokes>=" << strokes.first << " and strokes<=" << strokes.second
302 << " and jlpt>=" << jlpt.first << " and jlpt<=" << jlpt.second
303 << " and grade>=" << grade.first << " and grade<=" << grade.second
304 << comps.str()
305 << " order by " << order_by;
308 // perform query
309 vector<string> q = query( ss.str().c_str(), true, false );
310 vector<aoi_ui::KanjiView::Cell> data;
311 vector<string> components;
312 set<string> flags;
313 for ( size_t i=0; i<q.size(); i+=4 ){
314 string kanji = q[i];
315 int freq = std::stoi(q[i+1]);
316 data.push_back(
317 aoi_ui::KanjiView::Cell(
318 kanji,
319 (freq>0)? get_color("frequent"):-1
322 components.push_back(q[i+2]);
323 for ( string &s: utils::split_string( q[i+3], parsers::SEPARATOR_SQL) )
324 flags.insert(s);
327 log_d("Groups: " + utils::to_string(flags));
329 ui_->set_kanjiview( data );
330 char b[32];
331 sprintf( b, "%d results", db_->result_rows() );
332 ui_->set_kanji_results( b );
334 utils::Histogram<string> histogram;
335 for ( string &s: components )
336 histogram.add( utils::str_to_chars(s.c_str()) );
337 curr_components_ = histogram.sorted();
339 cb_set_components();
343 void App::alert ( const string &msg, const string &desc )
345 std::stringstream ss;
346 ss << msg;
347 if ( !desc.empty() )
348 ss << "\n\nDetails:\n" << desc;
349 log_e(ss);
350 ui_->alert(ss.str());
354 int App::run ( int argc, char **argv )
356 return ui_->run(argc,argv);
360 void App::check_tables ()
362 for ( auto &dbit: aoi_config::db_tables) {
363 // no need for vacuum here, it will be done by check_indexes()
364 log("Checking tables in database: " + string(dbit.first));
365 std::stringstream q;
366 q << "SELECT name FROM " << dbit.first << "." << "sqlite_master WHERE type='table'";
367 vector<string> existing = query(q.str().c_str());
368 // CREATE TABLE name ( column1 TYPE, column2 TYPE, ... )
369 for ( auto &table: dbit.second ){
370 if ( utils::is_in(existing, string(table.first)) )
371 continue;
372 log("Creating table " + string(table.first));
373 vector<string> v;
374 for ( auto &column: table.second ){
375 std::stringstream sstr;
376 sstr << column.name << " " << column.type;
377 v.push_back(sstr.str());
379 std::stringstream ss;
380 ss << "CREATE TABLE " << dbit.first << "." << table.first
381 << " (" << to_string(v,",") << ");\n";
382 query(ss.str().c_str());
384 log("Check done.");
389 void App::check_indexes ()
391 bool do_vacuum = false;
392 log("Checking indexes...");
393 for ( auto &dbit: aoi_config::db_tables ) {
394 std::stringstream q;
395 q << "SELECT name FROM " << dbit.first << "." << "sqlite_master WHERE type='index'";
396 vector<string> existing = query(q.str().c_str());
397 // CREATE INDEX idx_table_column ON table ( column ASC )
398 for ( auto &mi: dbit.second ){ // tables
399 for ( auto &c: mi.second ){ // columns
400 std::stringstream idx_name;
401 idx_name << "idx_" << mi.first << "_" << c.name;
402 if ( c.index && !utils::is_in( existing, idx_name.str() ) ){
403 log(string("Creating index ") + idx_name.str());
404 std::stringstream ss;
405 ss << "CREATE INDEX " << idx_name.str() << " ON " << mi.first
406 << "(" << c.name << " " << c.sort << ")";
407 query(ss.str().c_str());
408 do_vacuum = true;
413 if ( do_vacuum )
414 query("VACUUM");
415 log("Check done.");
419 void App::cb_dic_input ()
421 parse_dic_input( ui_->get_dic_input() );
422 ui_->reset_filters();
426 void App::on_dic_selected ( int id )
428 log_d("App::on_dic_selected()");
431 void App::cb_edit_word ( int id )
433 std::stringstream ss;
434 ss << "App::edit_word( " << id << " )";
435 log_d(ss);
436 ui_->edit_word();
440 void App::cb_popup_kanji ( const string &kanji )
442 std::stringstream ss;
443 ss << "App::on_kanji_clicked()" << kanji;
444 Kanji k = db_get_kanji(kanji);
445 ui_->popup_kanji( k );
446 // XXX: this should be somewhere else (it is not logical here)
447 ui_->highlight_components( k.components() );
448 log(ss);
452 void App::set_listview ( const vector<string> &v )
454 // TODO: nokanji
455 if ( !listview_items_.empty() ) listview_items_.clear();
456 vector<string> d;
457 vector<int> cell_ids;
458 size_t i = 0;
459 while ( i < v.size() ) {
460 int cell_id = std::stoi(v[i]); // jmdict id
461 cell_ids.push_back(cell_id);
462 cell_ids.push_back(cell_id);
463 cell_ids.push_back(cell_id);
464 // pos
465 set<string> pos;
466 for ( string &elt: utils::split_string( v[i+1], ",") )
467 if ( elt.size() > 0 )
468 pos.insert(utils::strip(elt.c_str()));
469 d.push_back( v[i+2] ); // reading
470 d.push_back( v[i+3] ); // kanji
471 d.push_back( v[i+4] ); // sense
472 listview_items_.push_back( {cell_id, pos, v[i+2], v[i+3], v[i+4]} );
473 i += 5;
475 char buff[32];
476 sprintf( buff, "%d results", db_->result_rows() );
477 ui_->set_dic_results( buff );
478 ui_->set_listview(d,cell_ids);
482 void App::parse_dic_input ( const char *str )
484 log_d(string("App::parse_dic_input: \"") +string(str) + string("\""));
486 string stripped = utils::strip(str);
488 if ( stripped.empty() )
489 return;
491 const char *s = stripped.c_str();
493 string qq;
494 if ( s[0] != SENSE_SEARCH_CHAR ){
495 DictionaryInputParser p;
496 qq = p.parse(s);
498 if ( p.warning() ){
499 int res = fl_choice(
500 "Too broad search.\n"\
501 "Your computer may become unresponsible for a long time.\n"\
502 "Proceed?",
503 "Proceed",
504 "Cancel",
507 if ( res == 1 ) // Cancel
508 return;
511 if ( !strchr(s,'*') && !strchr(s,'?') && !strchr(s,'[') && !strchr(s,'{')
512 && !strchr(s,'(') )
513 qq += "*";
516 std::stringstream q;
517 if ( s[0] == SENSE_SEARCH_CHAR ){
518 q << "select did,"
519 << "group_concat(pos) as pos,"
520 << q_reading("d_sense.did")
521 << q_kanji("d_sense.did")
522 << q_sense()
523 << " from d_sense where gloss glob '*" << stripped.substr(1) << "*'"
524 << " group by did";
526 else if ( rmn_->contains_kanji( qq.c_str() ) ) {
527 q << "select d_kanji.did as did,"
528 << "(select group_concat(pos) from d_sense where d_sense.did = d_kanji.did) as pos,"
529 << q_kanji()
530 << q_reading("d_kanji.did")
531 << q_sense("d_kanji.did")
532 << "from d_kanji where "
533 << "kanji glob '" << rmn_->romaji_to_hiragana(qq.c_str()) << "' "
534 << " or kanji glob '" << rmn_->romaji_to_katakana(qq.c_str()) << "' "
535 << " group by did order by d_kanji.freq desc, d_kanji.kanji asc";
537 else {
538 q << "select d_reading.did as did,"
539 << "(select group_concat(pos) from d_sense where d_sense.did = d_reading.did) as pos,"
540 << q_reading()
541 << q_kanji("d_reading.did")
542 << q_sense("d_reading.did")
543 << "from d_reading where "
544 << "reading glob '" << rmn_->romaji_to_hiragana(qq.c_str())
545 << "' or reading glob '"<< rmn_->romaji_to_katakana(qq.c_str()) << "' "
546 << " group by did order by d_reading.freq desc, d_reading.reading asc";
548 set_listview(query(q.str().c_str()));
553 Kanji App::db_get_kanji ( const string &kanji )
555 log("App::db_get_kanji()");
556 std::stringstream q;
557 q << "select kanji,strokes,ucs, rad_classic, rad_nelson, "
558 << "jlpt, grade, freq, onyomi, kunyomi, nanori, meaning,flags,components "
559 << "from k_kanji where kanji='" << kanji << "';";
561 vector<string> res = query(q.str().c_str());
562 Kanji kk(res[0]);
563 kk.strokes(std::stoi(res[1]));
564 kk.ucs(res[2]);
565 kk.rad_classic(std::stoi(res[3]));
566 kk.rad_nelson( (res[4].empty()) ? -1:std::stoi(res[4]));
567 kk.jlpt(std::stoi(res[5]));
568 kk.grade(std::stoi(res[6]));
569 kk.freq(std::stoi(res[7]));
570 kk.onyomi( utils::split_string(res[8],parsers::SEPARATOR_SQL) );
571 kk.kunyomi( utils::split_string(res[9],parsers::SEPARATOR_SQL) );
572 kk.nanori( utils::split_string(res[10],parsers::SEPARATOR_SQL) );
573 kk.meaning( utils::split_string(res[11],parsers::SEPARATOR_SQL) );
574 kk.flags( utils::split_string(res[12],parsers::SEPARATOR_SQL) );
575 kk.components( res[13] );
577 string qq = "select skip1,skip2,skip3,misclass from k_skip where kanji='"+kanji+"';";
578 vector<string> res2 = query(qq.c_str());
579 if ( res2.size() % 4 != 0 ){
580 std::stringstream ss;
581 ss << "Wrong SKIP count. Kanji: " << kanji
582 << "Query result size: " << res2.size()
583 << " (should be 4,8 or 12). SKIP not loaded.";
584 log_e(ss);
585 return kk;
587 for ( size_t i=0; i < res2.size(); i+=4 )
588 kk.skip( res2[i], res2[i+1], res2[i+2], res2[i+3] );
589 return kk;
593 void App::cb_filter_listview ()
595 vector<string> pos = ui_->listview_filters();
596 log_d("App::cb_filter_listview(): pos: " + utils::to_string(pos));
597 bool filter_expr = ( utils::is_in( pos, string("expr") ) );
598 bool filter_noun = ( utils::is_in( pos, string("noun") ) );
599 bool filter_verb = ( utils::is_in( pos, string("verb") ) );
600 bool filter_adj = ( utils::is_in( pos, string("adj") ) );
601 vector<string> data;
602 vector<int> ids;
603 size_t n = 0;
604 for ( auto &elt: listview_items_ ){
605 auto start = elt.pos.begin();
606 auto end = elt.pos.end();
607 if ( filter_expr && std::find( start, end, "exp") == end )
608 continue;
609 if ( filter_noun && std::find( start, end, "n" ) == end )
610 continue;
611 if ( filter_adj && std::find_if( start, end,
612 [](const string &s){ return strncmp(s.c_str(),"adj",3)==0;} ) == end )
613 continue;
614 if ( filter_verb && std::find_if( start, end,
615 [](const string &s){ return strncmp(s.c_str(),"v",1)==0;} ) == end )
616 continue;
617 ids.push_back( elt.did );
618 ids.push_back( elt.did );
619 ids.push_back( elt.did );
620 data.push_back ( elt.reading );
621 data.push_back ( elt.kanji );
622 data.push_back ( elt.sense );
623 n++;
625 ui_->set_listview( data, ids );
626 std::stringstream ss;
627 ss << n << " results";
628 if ( listview_items_.size() != n )
629 ss << " (" << listview_items_.size()-n << " hidden)";
630 log(ss.str());
631 ui_->set_dic_results( ss.str() );
635 void App::cb_dicview_rightclick ( int did )
637 string q = "select group_concat(kanji,'') from d_kanji where did="+std::to_string(did);
638 vector<string> res = query( q.c_str() );
639 set<string> kanji;
640 for ( string &c: utils::str_to_chars(res[0].c_str()) )
641 if ( App::get()->rmn()->is_kanji(c.c_str()) )
642 kanji.insert(c);
643 ui_->dicview_menu( did, vector<string>(kanji.begin(),kanji.end()), false, false );
647 void App::apply_config ()
649 ui_->font_base_size( get_config<int>("font/base_size"));
650 logger_.loglevel( get_config("log/level") );
651 ui_->init_colors();
652 Fl::check();
656 void App::cb_manage_db ()
658 auto q = query("select val from aoi where key='jmdict_version'");
659 string jmdict_version = (q.empty()) ? "NONE":q[0];
660 q = query("select val from aoi where key='kanjidic_version'");
661 string kanjidic_version = q.empty() ? "NONE":q[0];
662 q = query("select val from aoi where key='components_version'");
663 string components_version = q.empty() ? "NONE":q[0];
664 auto *d = ui_->dlg_manage_db();
665 d->main_version("MAIN VERSION");
666 d->main_created("ONCE UPON A TIME...");
667 d->main_ver_jmdict("JMDICT");
668 d->main_ver_kanjidic("KANJIDIC");
669 d->main_ver_kradfile("KRADFILE");
670 d->main_ver_tatoeba("TATOEBA");
671 d->user_checked_against("WHO KNOWS");
672 d->show();
676 void App::load_config ()
678 log("Loading config...");
679 vector<string> res = query("select key,val from config;");
680 for ( size_t i=0; i < res.size(); i+=2 ){
681 set_config( res[i], res[i+1] );
683 apply_config();
687 void App::save_config ( const std::map<string,aoi_config::Config::Value> &newmap)
689 log("Saving config...");
691 typedef std::pair<string,aoi_config::Config::Value> cfg_pair;
693 // merge new and old config
694 for ( const cfg_pair &p: newmap )
695 set_config( p.first, p.second.val );
696 apply_config();
698 // prepare SQL script
699 std::stringstream ss;
700 ss << "BEGIN TRANSACTION;\n";
701 ss << "DROP TABLE IF EXISTS user.config;\n";
702 ss << "CREATE TABLE user.config (key TEXT, val TEXT);\n";
703 for ( const cfg_pair &p: get_config_map() )
704 ss << "INSERT INTO user.config (key,val) VALUES('"
705 << p.first << "','" << p.second.val << "');\n";
706 ss << "END TRANSACTION;\n";
708 query(ss.str().c_str());
710 // apply new fonts and colors
711 init_dicview();
713 // TODO:
714 // check new keys
715 // check keys in oldconfig not present in newmap
716 // set oldkeys
717 // set newkeys
718 // mnoziny...
721 } // namespace aoi