b8a7d209f0cf403ba870a11f946e90b6647d400a
[m17n/m17n-im-config.git] / src / mim-config.c
1 /* mim-config.c -- M17N input method configuration
2    Copyright (C) 2007
3      National Institute of Advanced Industrial Science and Technology (AIST)
4      Registration Number H15PRO112
5
6    This file is part of the m17n-im-config package; a sub-part of the
7    m17n library.
8
9    The m17n library is free software; you can redistribute it and/or
10    modify it under the terms of the GNU Lesser General Public License
11    as published by the Free Software Foundation; either version 2.1 of
12    the License, or (at your option) any later version.
13
14    The m17n library is distributed in the hope that it will be useful,
15    but WITHOUT ANY WARRANTY; without even the implied warranty of
16    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
17    Lesser General Public License for more details.
18
19    You should have received a copy of the GNU Lesser General Public
20    License along with the m17n library; if not, write to the Free
21    Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
22    02111-1307, USA.  */
23
24 #include <stdlib.h>
25 #include <string.h>
26 #include <libintl.h>
27 #include <m17n.h>
28 #include <m17n-misc.h>
29 #include <gtk/gtk.h>
30 #include <config.h>
31 #include "m17n-im-config.h"
32
33 #define _(String) dgettext (PACKAGE, String)
34
35 #define CONFIG_CALLBACK_DATA " config-callback-data"
36 #define CONFIG_STATUS_DATA " config-status-data"
37 #define CONFIG_TREE_VIEW "  config-tree-view"
38
39 typedef void (*MimConfigCallbackFunc) (GtkWidget *widget, gpointer data);
40
41 typedef struct _MimConfigCallback
42 {
43   GtkWidget *widget;
44   MimConfigCallbackFunc func;
45   gpointer data;
46 } MimConfigCallback;
47
48 typedef struct _MimConfigStatus
49 {
50   /* Number of available input methods.  */
51   gint num_im;
52   /* Number of modified input methods.  */
53   gint num_modified;
54 } MimConfigStatus;
55
56 /* Status of variables and commands of an input method.  */
57
58 enum MimStatus
59   {
60     MIM_STATUS_DEFAULT,
61     MIM_STATUS_CUSTOMIZED,
62     MIM_STATUS_MODIFIED,
63     MIM_STATUS_NO,
64     MIM_STATUS_MAX
65   };
66
67 static char *mim_status_str[MIM_STATUS_MAX];
68
69 enum MimStatus
70 get_mim_status (MSymbol lang, MSymbol name)
71 {
72   MPlist *plist;
73   enum MimStatus status = MIM_STATUS_NO;
74
75   for (plist = minput_get_variable (lang, name, Mnil);
76        plist && mplist_key (plist) != Mnil; plist = mplist_next (plist))
77     {
78       MPlist *p = mplist_value (plist);
79       MSymbol status_symbol;
80
81       p = mplist_next (mplist_next (p));
82       status_symbol = mplist_value (p);
83       if (status_symbol == Mconfigured)
84         return MIM_STATUS_MODIFIED;
85       if (status_symbol == Mcustomized)
86         status = MIM_STATUS_CUSTOMIZED;
87       else if (status == MIM_STATUS_NO)
88         status = MIM_STATUS_DEFAULT;
89     }
90   for (plist = minput_get_command (lang, name, Mnil);
91        plist && mplist_key (plist) != Mnil; plist = mplist_next (plist))
92     {
93       MPlist *p = mplist_value (plist);
94       MSymbol status_symbol;
95
96       p = mplist_next (mplist_next (p));
97       status_symbol = mplist_value (p);
98       if (status_symbol == Mconfigured)
99         return MIM_STATUS_MODIFIED;
100       if (status_symbol == Mcustomized)
101         status = MIM_STATUS_CUSTOMIZED;
102       else if (status == MIM_STATUS_NO)
103         status = MIM_STATUS_DEFAULT;
104     }
105   return status;
106 }
107
108 /* Columns of each row.  */
109 enum
110   {
111     /* parent: language name
112         child: IM name  */
113     COL_TAG = 0,
114     /* parent: NULL or "modified"
115         child: "default", "customized", or "modified"  */
116     COL_STATUS_STR,
117     /* parent: num of modified children
118         child: enum MimStatus */
119     COL_STATUS,
120     /* parent: Mnil
121         child: symbolic language name.  */
122     COL_LANG,
123     /* parent: Mnil
124         child: symbolic IM name.  */
125     COL_NAME,
126     /* number of columns  */
127     NUM_COLS
128   };
129
130 /* Called when a row is expanded.  We may have to initialize
131    children.  */
132 static void
133 tree_expanded_cb (GtkTreeView *tree, GtkTreeIter *parent,
134                   GtkTreePath *path, gpointer data)
135 {
136   GtkTreeModel *model;
137   GtkTreeIter iter;
138   MSymbol lang, name;
139
140   model = gtk_tree_view_get_model (tree);
141   if (gtk_tree_model_iter_children (model, &iter, parent))
142     {
143       gchar *status_str;
144
145       gtk_tree_model_get (model, &iter, COL_STATUS_STR, &status_str, -1);
146       if (! status_str)
147         {
148           /* The first child is not yet initialized, and that means
149              the remaining children are not initialized either.  */
150           gtk_tree_model_get (model, &iter, COL_LANG, &lang, -1);
151           do {
152             enum MimStatus im_status;
153
154             gtk_tree_model_get (model, &iter, COL_NAME, &name, -1);
155             im_status = get_mim_status (lang, name);
156             gtk_tree_store_set (GTK_TREE_STORE (model), &iter,
157                                 COL_STATUS_STR, mim_status_str[im_status],
158                                 COL_STATUS, im_status,
159                                 -1);
160           } while (gtk_tree_model_iter_next (model, &iter));
161         }
162     }
163 }
164
165 extern GtkWidget *create_variable_list (MSymbol lang, MSymbol name);
166 extern GtkWidget *create_command_list (MSymbol lang, MSymbol name);
167
168 static void
169 edit_im (GtkTreeView *tree, MSymbol lang, MSymbol name)
170 {
171   GtkWidget *dialog, *notebook, *scrolled, *vbox, *label;
172   gint response;
173   MPlist *plist;
174
175   dialog = (gtk_dialog_new_with_buttons
176             (name == Mnil ? "global" : msymbol_name (name),
177              GTK_WINDOW (gtk_widget_get_toplevel (GTK_WIDGET (tree))),
178              GTK_DIALOG_DESTROY_WITH_PARENT | GTK_DIALOG_NO_SEPARATOR,
179              GTK_STOCK_CLOSE, GTK_RESPONSE_CLOSE,
180              NULL));
181   gtk_widget_set_size_request (dialog, 500, 300);
182
183   vbox = gtk_vbox_new (FALSE, 0);
184   gtk_container_set_border_width (GTK_CONTAINER(vbox), 10);
185   gtk_container_add (GTK_CONTAINER (GTK_DIALOG (dialog)->vbox), vbox);
186
187   notebook = gtk_notebook_new ();
188   gtk_container_add (GTK_CONTAINER (vbox), notebook);
189
190   /* Variables' page */
191   scrolled = gtk_scrolled_window_new (NULL, NULL);
192   gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled),
193                                   GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
194   label = gtk_label_new_with_mnemonic (_("_Variables"));
195   gtk_notebook_append_page (GTK_NOTEBOOK (notebook), scrolled, label);
196   vbox = gtk_vbox_new (FALSE, 0);
197   gtk_scrolled_window_add_with_viewport (GTK_SCROLLED_WINDOW (scrolled),
198                                          vbox);
199   gtk_box_pack_start (GTK_BOX (vbox),
200                       create_variable_list (lang, name),
201                       FALSE, FALSE, 0);
202
203   /* Commands' pages */
204   scrolled = gtk_scrolled_window_new (NULL, NULL);
205   gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled),
206                                   GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
207   label = gtk_label_new_with_mnemonic (_("Co_mmands"));
208   gtk_notebook_append_page (GTK_NOTEBOOK (notebook), scrolled, label);
209   vbox = gtk_vbox_new (FALSE, 0);
210   gtk_scrolled_window_add_with_viewport (GTK_SCROLLED_WINDOW (scrolled),
211                                          vbox);
212   gtk_box_pack_start (GTK_BOX (vbox),
213                       create_command_list (lang, name),
214                       FALSE, FALSE, 0);
215
216   gtk_widget_show_all (dialog);
217   gtk_dialog_run (GTK_DIALOG (dialog));
218   gtk_widget_destroy (dialog);
219 }
220
221 static void
222 update_child_row (GtkTreeModel *model, GtkTreeIter *iter,
223                   enum MimStatus status, MimConfigStatus *config_status,
224                   GtkTreeView *tree)
225 {
226   GtkTreeIter parent;
227   gint inc_modified;
228
229   inc_modified = (status == MIM_STATUS_MODIFIED ? 1 : -1);
230
231   gtk_tree_store_set (GTK_TREE_STORE (model), iter,
232                       COL_STATUS_STR, mim_status_str[status],
233                       COL_STATUS, status, -1);
234   if (gtk_tree_model_iter_parent (model, &parent, iter))
235     {
236       gint num_modified;
237       gchar *status_str;
238
239       gtk_tree_model_get (model, &parent, COL_STATUS, &num_modified, -1);
240       num_modified += inc_modified;
241       gtk_tree_store_set (GTK_TREE_STORE (model), &parent,
242                           COL_STATUS, num_modified, -1);
243       if (num_modified <= 1)
244         {
245           status_str = (status == MIM_STATUS_MODIFIED
246                         ? mim_status_str[MIM_STATUS_MODIFIED] : NULL);
247           gtk_tree_store_set (GTK_TREE_STORE (model), &parent,
248                               COL_STATUS_STR, status_str);
249         }
250     }
251       
252   if (! config_status)
253     config_status = g_object_get_data (G_OBJECT (model), CONFIG_STATUS_DATA);
254   config_status->num_modified += inc_modified;
255   if (tree && config_status->num_modified <= 1)
256     {
257       MimConfigCallback *callback;
258
259       callback = g_object_get_data (G_OBJECT (tree), CONFIG_CALLBACK_DATA);
260       if (callback)
261         callback->func (callback->widget, callback->data);
262     }
263 }
264
265 static void
266 tree_activated_cb (GtkTreeView *tree, GtkTreePath *path,
267                    GtkTreeViewColumn *column, gpointer data)
268 {
269   GtkTreeModel *model;
270   GtkTreeIter iter;
271
272   model = gtk_tree_view_get_model (tree);
273   if (gtk_tree_model_get_iter (model, &iter, path))
274     {
275       MSymbol lang, name;
276
277       gtk_tree_model_get (model, &iter, COL_LANG, &lang, COL_NAME, &name, -1);
278       if (lang != Mnil)
279         {
280           /* child row for an IM */
281           enum MimStatus old, new;
282
283           old = get_mim_status (lang, name);
284           edit_im (tree, lang, name);
285           new = get_mim_status (lang, name);
286           if (old != new)
287             update_child_row (model, &iter, new, NULL, tree);
288         }
289       else
290         {
291           /* parent row for a language */
292           if (gtk_tree_view_row_expanded (tree, path))
293             gtk_tree_view_collapse_row (tree, path);
294           else
295             gtk_tree_view_expand_row (tree, path, TRUE);
296         }
297     }
298 }
299
300 typedef struct _MimTable
301 {
302   gchar *lang;
303   gchar *name;
304   MSymbol symlang;
305   MSymbol symname;
306 } MimTable;
307
308 static int
309 sort_im (const void *p1, const void *p2)
310 {
311   const MimTable *t1 = p1;
312   const MimTable *t2 = p2;
313   int result = strcmp (t1->lang, t2->lang);
314
315   return (result ? result : strcmp (t1->name, t2->name));
316 }
317
318 static GtkTreeStore *
319 make_store_for_input_methods ()
320 {
321   GtkTreeStore *store;
322   MPlist *imlist, *p;
323   int i;
324   MimTable *imtable;
325   char *lang;
326   GtkTreeIter iter1, iter2;
327   enum MimStatus status;
328   MimConfigStatus *config_status;
329
330   store = gtk_tree_store_new (NUM_COLS,
331                               G_TYPE_STRING,  /* COL_TAG */
332                               G_TYPE_STRING,  /* COL_STATUS_STR */
333                               G_TYPE_UINT,    /* COL_STATUS */
334                               G_TYPE_POINTER, /* COL_LANG */
335                               G_TYPE_POINTER  /* COL_NAME */
336                               );
337
338   config_status = g_new0 (MimConfigStatus, 1);
339   gtk_tree_store_append (store, &iter1, NULL);
340   status = get_mim_status (Mt, Mnil);
341   gtk_tree_store_set (store, &iter1,
342                       COL_TAG, _("global"),
343                       COL_STATUS_STR, mim_status_str[status],
344                       COL_STATUS, status, 
345                       COL_LANG, Mt,
346                       COL_NAME, Mnil,
347                       -1);
348
349   imlist = mdatabase_list (msymbol ("input-method"), Mnil, Mnil, Mnil);
350   config_status->num_im = mplist_length (imlist);
351   imtable = g_newa (MimTable, config_status->num_im);
352   for (i = 0, p = imlist; mplist_key (p) != Mnil; p = mplist_next (p))
353     {
354       MDatabase *mdb = (MDatabase *) mplist_value (p);
355       MSymbol *tag = mdatabase_tag (mdb);
356
357       if (tag[1] != Mnil && tag[2] != Mnil)
358         {
359           MSymbol language = mlanguage_name (tag[1]);
360
361           if (language != Mnil)
362             imtable[i].lang = msymbol_name (language);
363           else
364             /* `~' is for putting this element at the tail by sort.  */
365             imtable[i].lang = "~other";
366           imtable[i].name = msymbol_name (tag[2]);
367           imtable[i].symlang = tag[1];
368           imtable[i].symname = tag[2];
369           i++;
370         }
371     }
372   m17n_object_unref (imlist);
373   config_status->num_im = i;
374   qsort (imtable, config_status->num_im, sizeof (MimTable), sort_im);
375
376   for (lang = NULL, i = 0; i < config_status->num_im; i++)
377     {
378       if (lang != imtable[i].lang)
379         {
380           gchar *name;
381
382           gtk_tree_store_append (store, &iter1, NULL);
383           lang = imtable[i].lang;
384           if (lang[0] != '~')
385             {
386               MText *native_text;
387               gchar *native = NULL;
388               int nbytes;
389
390               if (imtable[i].symlang != Mt
391                   && (native_text = mlanguage_text (imtable[i].symlang)))
392                 {
393                   enum MTextFormat fmt;
394
395                   native = mtext_data (native_text, &fmt, &nbytes,
396                                        NULL, NULL);
397                   if (fmt != MTEXT_FORMAT_US_ASCII
398                       && fmt != MTEXT_FORMAT_UTF_8)
399                     native = 0;
400                 }
401               if (native)
402                 {
403                   name = alloca (strlen (lang) + nbytes + 4);
404                   sprintf (name, "%s (%s)", lang, native);
405                 }
406               else
407                 name = lang;
408             }
409           else
410             name = lang + 1;
411
412           gtk_tree_store_set (store, &iter1,
413                               COL_TAG, name,
414                               COL_STATUS_STR, NULL,
415                               COL_STATUS, 0,
416                               COL_LANG, Mnil,
417                               COL_NAME, Mnil,
418                               -1);
419         }
420       gtk_tree_store_append (store, &iter2, &iter1);
421       gtk_tree_store_set (store, &iter2,
422                           COL_TAG, imtable[i].name,
423                           COL_STATUS_STR, NULL,
424                           COL_LANG, imtable[i].symlang,
425                           COL_NAME, imtable[i].symname,
426                           -1);
427     }
428   config_status->num_modified = 0;
429   g_object_set_data_full (G_OBJECT (store), CONFIG_STATUS_DATA,
430                           config_status, g_free);
431   return store;
432 }
433
434 static gboolean
435 revert_to_saved (GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter,
436                gpointer data)
437 {
438   enum MimStatus status;
439   MSymbol lang, name;
440   MimConfigStatus *config_status = data;
441
442   gtk_tree_model_get (model, iter, COL_LANG, &lang, COL_NAME, &name, -1);
443   if (lang == Mnil)
444     return FALSE;
445   gtk_tree_model_get (model, iter, COL_STATUS, &status, -1);  
446   if (status != MIM_STATUS_MODIFIED)
447     return FALSE;
448   minput_config_variable (lang, name, Mnil, NULL);
449   minput_config_command (lang, name, Mnil, NULL);
450   status = get_mim_status (lang, name);
451   update_child_row (model, iter, status, config_status, NULL);
452   return FALSE;
453 }
454
455 static gboolean
456 set_as_saved (GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter,
457            gpointer data)
458 {
459   enum MimStatus status;
460   MSymbol lang, name;
461   MimConfigStatus *config_status = data;
462
463   gtk_tree_model_get (model, iter, COL_LANG, &lang, COL_NAME, &name, -1);
464   if (lang == Mnil)
465     return FALSE;
466   gtk_tree_model_get (model, iter, COL_STATUS, &status, -1);  
467   if (status != MIM_STATUS_MODIFIED)
468     return FALSE;
469   status = get_mim_status (lang, name);
470   update_child_row (model, iter, status, config_status, NULL);
471   return FALSE;
472 }
473
474 static int initialized = 0;
475
476 static void
477 destroy_cb (GtkWidget *widget, gpointer data)
478 {
479   M17N_FINI ();
480 }
481
482 \f
483 /* Public API */
484
485 GtkWidget *
486 mim_config_new (GCallback func, gpointer data)
487 {
488   GtkWidget *tree, *config;
489   GtkTreeStore *store;
490   GtkCellRenderer *renderer;
491   GtkTreeViewColumn *column;
492
493   if (initialized)
494     return NULL;
495   M17N_INIT ();
496   if (merror_code < 0)
497     return NULL;
498
499 #if ENABLE_NLS
500   bindtextdomain ("m17n-im-config", GETTEXTDIR);
501   bind_textdomain_codeset ("m17n-im-config", "UTF-8");
502 #endif
503
504   mim_status_str[MIM_STATUS_DEFAULT] = _("default");
505   mim_status_str[MIM_STATUS_CUSTOMIZED] = _("customized");
506   mim_status_str[MIM_STATUS_MODIFIED] = _("modified");
507   mim_status_str[MIM_STATUS_NO] = _("uncustomizable");
508
509   store = make_store_for_input_methods ();
510   tree = gtk_tree_view_new_with_model (GTK_TREE_MODEL (store));
511   g_object_unref (G_OBJECT (store));
512
513   renderer = gtk_cell_renderer_text_new ();
514   column = (gtk_tree_view_column_new_with_attributes
515             (_("Input Method"), renderer, "text", COL_TAG, NULL));
516   gtk_tree_view_append_column (GTK_TREE_VIEW (tree), column);
517
518   renderer = gtk_cell_renderer_text_new ();
519   column = (gtk_tree_view_column_new_with_attributes
520             (_("Status"), renderer, "text", COL_STATUS_STR, NULL));
521   gtk_tree_view_append_column (GTK_TREE_VIEW (tree), column);
522
523   g_signal_connect (G_OBJECT (tree), "row-expanded",
524                     G_CALLBACK (tree_expanded_cb), NULL);
525   g_signal_connect (G_OBJECT (tree), "row-activated",
526                     G_CALLBACK (tree_activated_cb), NULL);
527
528   config =gtk_scrolled_window_new (NULL, NULL);
529   gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (config),
530                                   GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
531   gtk_scrolled_window_add_with_viewport (GTK_SCROLLED_WINDOW (config), tree);
532   g_signal_connect (G_OBJECT (config), "destroy",
533                     G_CALLBACK (destroy_cb), NULL);
534
535   g_object_set_data (G_OBJECT (config), CONFIG_TREE_VIEW, tree);
536   if (func)
537     {
538       MimConfigCallback *callback;
539
540       callback = g_new (MimConfigCallback, 1);
541       callback->widget = config;
542       callback->func = (MimConfigCallbackFunc) func;
543       callback->data = data;
544       g_object_set_data_full (G_OBJECT (tree), CONFIG_CALLBACK_DATA,
545                               callback, g_free);
546     }
547
548   return config;
549 }
550
551 gboolean
552 mim_config_modified (GtkWidget *config)
553 {
554   GtkTreeView *tree;
555   GtkTreeModel *model;
556   MimConfigStatus *config_status;
557
558   tree = g_object_get_data (G_OBJECT (config), CONFIG_TREE_VIEW);
559   model = gtk_tree_view_get_model (tree);
560   config_status = g_object_get_data (G_OBJECT (model), CONFIG_STATUS_DATA);
561
562   return (config_status->num_modified > 0 ? TRUE : FALSE);
563 }
564
565 gboolean
566 mim_config_revert (GtkWidget *config)
567 {
568   GtkTreeView *tree;
569   GtkTreeModel *model;
570   MimConfigStatus *config_status;
571
572   tree = g_object_get_data (G_OBJECT (config), CONFIG_TREE_VIEW);
573   model = gtk_tree_view_get_model (tree);
574   config_status = g_object_get_data (G_OBJECT (model), CONFIG_STATUS_DATA);
575
576   if (config_status->num_modified == 0)
577     return FALSE;
578   gtk_tree_model_foreach (model, revert_to_saved, config_status);
579   return TRUE;
580 }
581
582 gboolean
583 mim_config_save (GtkWidget *config)
584 {
585   GtkTreeView *tree;
586   GtkTreeModel *model;
587   MimConfigStatus *config_status;
588
589   tree = g_object_get_data (G_OBJECT (config), CONFIG_TREE_VIEW);
590   model = gtk_tree_view_get_model (tree);
591   config_status = g_object_get_data (G_OBJECT (model), CONFIG_STATUS_DATA);
592
593   if (config_status->num_modified == 0)
594     return FALSE;
595   minput_save_config ();
596   gtk_tree_model_foreach (model, set_as_saved, config_status);
597   return TRUE;
598 }
599
600 GtkTreeView *
601 mim_config_get_tree_view (GtkWidget *config)
602 {
603   GtkTreeView *tree;
604
605   tree = g_object_get_data (G_OBJECT (config), CONFIG_TREE_VIEW);
606   return tree;
607 }