*** empty log message ***
[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_entries (GtkTooltips *tip, MSymbol lang,
166                                                MSymbol name);
167 extern GtkWidget *create_command_entries (GtkTooltips *tip, MSymbol lang,
168                                               MSymbol name);
169
170 static void
171 edit_im (GtkTreeView *tree, MSymbol lang, MSymbol name)
172 {
173   GtkWidget *dialog, *notebook, *scrolled, *vbox, *label;
174   GtkTooltips *tip = gtk_tooltips_new ();
175   gint response;
176   MPlist *plist;
177
178   dialog = (gtk_dialog_new_with_buttons
179             (name == Mnil ? "global" : msymbol_name (name),
180              GTK_WINDOW (gtk_widget_get_toplevel (GTK_WIDGET (tree))),
181              GTK_DIALOG_DESTROY_WITH_PARENT | GTK_DIALOG_NO_SEPARATOR,
182              GTK_STOCK_CLOSE, GTK_RESPONSE_CLOSE,
183              NULL));
184   gtk_widget_set_size_request (dialog, 500, 300);
185
186   notebook = gtk_notebook_new ();
187   gtk_container_add (GTK_CONTAINER (GTK_DIALOG (dialog)->vbox), notebook);
188
189   /* Variables' page */
190   scrolled = gtk_scrolled_window_new (NULL, NULL);
191   gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled),
192                                   GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
193   label = gtk_label_new_with_mnemonic ("_Variables");
194   gtk_notebook_append_page (GTK_NOTEBOOK (notebook), scrolled, label);
195   vbox = gtk_vbox_new (FALSE, 0);
196   gtk_scrolled_window_add_with_viewport (GTK_SCROLLED_WINDOW (scrolled),
197                                          vbox);
198   gtk_box_pack_start (GTK_BOX (vbox),
199                       create_variable_entries (tip, lang, name),
200                       FALSE, FALSE, 0);
201
202   /* Commands' pages */
203   scrolled = gtk_scrolled_window_new (NULL, NULL);
204   gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (scrolled),
205                                   GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
206   label = gtk_label_new_with_mnemonic ("Co_mmands");
207   gtk_notebook_append_page (GTK_NOTEBOOK (notebook), scrolled, label);
208   vbox = gtk_vbox_new (FALSE, 0);
209   gtk_scrolled_window_add_with_viewport (GTK_SCROLLED_WINDOW (scrolled),
210                                          vbox);
211   gtk_box_pack_start (GTK_BOX (vbox),
212                       create_command_entries (tip, lang, name),
213                       FALSE, FALSE, 0);
214
215   gtk_widget_show_all (dialog);
216   gtk_dialog_run (GTK_DIALOG (dialog));
217   gtk_widget_destroy (dialog);
218 }
219
220 static void
221 update_child_row (GtkTreeModel *model, GtkTreeIter *iter,
222                   enum MimStatus status, MimConfigStatus *config_status,
223                   GtkTreeView *tree)
224 {
225   GtkTreeIter parent;
226   gint inc_modified;
227
228   inc_modified = (status == MIM_STATUS_MODIFIED ? 1 : -1);
229
230   gtk_tree_store_set (GTK_TREE_STORE (model), iter,
231                       COL_STATUS_STR, mim_status_str[status],
232                       COL_STATUS, status, -1);
233   if (gtk_tree_model_iter_parent (model, &parent, iter))
234     {
235       gint num_modified;
236       gchar *status_str;
237
238       gtk_tree_model_get (model, &parent, COL_STATUS, &num_modified, -1);
239       num_modified += inc_modified;
240       gtk_tree_store_set (GTK_TREE_STORE (model), &parent,
241                           COL_STATUS, num_modified, -1);
242       if (num_modified <= 1)
243         {
244           status_str = (status == MIM_STATUS_MODIFIED
245                         ? mim_status_str[MIM_STATUS_MODIFIED] : NULL);
246           gtk_tree_store_set (GTK_TREE_STORE (model), &parent,
247                               COL_STATUS_STR, status_str);
248         }
249     }
250       
251   if (! config_status)
252     config_status = g_object_get_data (G_OBJECT (model), CONFIG_STATUS_DATA);
253   config_status->num_modified += inc_modified;
254   if (tree && config_status->num_modified <= 1)
255     {
256       MimConfigCallback *callback;
257
258       callback = g_object_get_data (G_OBJECT (tree), CONFIG_CALLBACK_DATA);
259       if (callback)
260         callback->func (callback->widget, callback->data);
261     }
262 }
263
264 static void
265 tree_activated_cb (GtkTreeView *tree, GtkTreePath *path,
266                    GtkTreeViewColumn *column, gpointer data)
267 {
268   GtkTreeModel *model;
269   GtkTreeIter iter;
270
271   model = gtk_tree_view_get_model (tree);
272   if (gtk_tree_model_get_iter (model, &iter, path))
273     {
274       MSymbol lang, name;
275
276       gtk_tree_model_get (model, &iter, COL_LANG, &lang, COL_NAME, &name, -1);
277       if (lang != Mnil)
278         {
279           /* child row for an IM */
280           enum MimStatus old, new;
281
282           old = get_mim_status (lang, name);
283           edit_im (tree, lang, name);
284           new = get_mim_status (lang, name);
285           if (old != new)
286             update_child_row (model, &iter, new, NULL, tree);
287         }
288       else
289         {
290           /* parent row for a language */
291           if (gtk_tree_view_row_expanded (tree, path))
292             gtk_tree_view_collapse_row (tree, path);
293           else
294             gtk_tree_view_expand_row (tree, path, TRUE);
295         }
296     }
297 }
298
299 typedef struct _MimTable
300 {
301   gchar *lang;
302   gchar *name;
303   MSymbol symlang;
304   MSymbol symname;
305 } MimTable;
306
307 static int
308 sort_im (const void *p1, const void *p2)
309 {
310   const MimTable *t1 = p1;
311   const MimTable *t2 = p2;
312   int result = strcmp (t1->lang, t2->lang);
313
314   return (result ? result : strcmp (t1->name, t2->name));
315 }
316
317 static GtkTreeStore *
318 make_store_for_input_methods ()
319 {
320   GtkTreeStore *store;
321   MPlist *imlist, *p;
322   int i;
323   MimTable *imtable;
324   char *lang;
325   GtkTreeIter iter1, iter2;
326   enum MimStatus status;
327   MimConfigStatus *config_status;
328
329   store = gtk_tree_store_new (NUM_COLS,
330                               G_TYPE_STRING,  /* COL_TAG */
331                               G_TYPE_STRING,  /* COL_STATUS_STR */
332                               G_TYPE_UINT,    /* COL_STATUS */
333                               G_TYPE_POINTER, /* COL_LANG */
334                               G_TYPE_POINTER  /* COL_NAME */
335                               );
336
337   config_status = g_new0 (MimConfigStatus, 1);
338   gtk_tree_store_append (store, &iter1, NULL);
339   status = get_mim_status (Mt, Mnil);
340   gtk_tree_store_set (store, &iter1,
341                       COL_TAG, _("global"),
342                       COL_STATUS_STR, mim_status_str[status],
343                       COL_STATUS, status, 
344                       COL_LANG, Mt,
345                       COL_NAME, Mnil,
346                       -1);
347
348   imlist = mdatabase_list (msymbol ("input-method"), Mnil, Mnil, Mnil);
349   config_status->num_im = mplist_length (imlist);
350   imtable = g_newa (MimTable, config_status->num_im);
351   for (i = 0, p = imlist; mplist_key (p) != Mnil; p = mplist_next (p))
352     {
353       MDatabase *mdb = (MDatabase *) mplist_value (p);
354       MSymbol *tag = mdatabase_tag (mdb);
355
356       if (tag[1] != Mnil && tag[2] != Mnil)
357         {
358           MSymbol language = mlanguage_name (tag[1]);
359
360           if (language != Mnil)
361             imtable[i].lang = msymbol_name (language);
362           else
363             /* `~' is for putting this element at the tail by sort.  */
364             imtable[i].lang = "~other";
365           imtable[i].name = msymbol_name (tag[2]);
366           imtable[i].symlang = tag[1];
367           imtable[i].symname = tag[2];
368           i++;
369         }
370     }
371   m17n_object_unref (imlist);
372   config_status->num_im = i;
373   qsort (imtable, config_status->num_im, sizeof (MimTable), sort_im);
374
375   for (lang = NULL, i = 0; i < config_status->num_im; i++)
376     {
377       if (lang != imtable[i].lang)
378         {
379           gchar *name;
380
381           gtk_tree_store_append (store, &iter1, NULL);
382           lang = imtable[i].lang;
383           if (lang[0] != '~')
384             {
385               MText *native_text;
386               gchar *native = NULL;
387               int nbytes;
388
389               if (imtable[i].symlang != Mt
390                   && (native_text = mlanguage_text (imtable[i].symlang)))
391                 {
392                   enum MTextFormat fmt;
393
394                   native = mtext_data (native_text, &fmt, &nbytes,
395                                        NULL, NULL);
396                   if (fmt != MTEXT_FORMAT_US_ASCII
397                       && fmt != MTEXT_FORMAT_UTF_8)
398                     native = 0;
399                 }
400               if (native)
401                 {
402                   name = alloca (strlen (lang) + nbytes + 4);
403                   sprintf (name, "%s (%s)", lang, native);
404                 }
405               else
406                 name = lang;
407             }
408           else
409             name = lang + 1;
410
411           gtk_tree_store_set (store, &iter1,
412                               COL_TAG, name,
413                               COL_STATUS_STR, NULL,
414                               COL_STATUS, 0,
415                               COL_LANG, Mnil,
416                               COL_NAME, Mnil,
417                               -1);
418         }
419       gtk_tree_store_append (store, &iter2, &iter1);
420       gtk_tree_store_set (store, &iter2,
421                           COL_TAG, imtable[i].name,
422                           COL_STATUS_STR, NULL,
423                           COL_LANG, imtable[i].symlang,
424                           COL_NAME, imtable[i].symname,
425                           -1);
426     }
427   config_status->num_modified = 0;
428   g_object_set_data_full (G_OBJECT (store), CONFIG_STATUS_DATA,
429                           config_status, g_free);
430   return store;
431 }
432
433 static gboolean
434 revert_to_saved (GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter,
435                gpointer data)
436 {
437   enum MimStatus status;
438   MSymbol lang, name;
439   MimConfigStatus *config_status = data;
440
441   gtk_tree_model_get (model, iter, COL_LANG, &lang, COL_NAME, &name, -1);
442   if (lang == Mnil)
443     return FALSE;
444   gtk_tree_model_get (model, iter, COL_STATUS, &status, -1);  
445   if (status != MIM_STATUS_MODIFIED)
446     return FALSE;
447   minput_config_variable (lang, name, Mnil, NULL);
448   minput_config_command (lang, name, Mnil, NULL);
449   status = get_mim_status (lang, name);
450   update_child_row (model, iter, status, config_status, NULL);
451   return FALSE;
452 }
453
454 static gboolean
455 set_as_saved (GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter,
456            gpointer data)
457 {
458   enum MimStatus status;
459   MSymbol lang, name;
460   MimConfigStatus *config_status = data;
461
462   gtk_tree_model_get (model, iter, COL_LANG, &lang, COL_NAME, &name, -1);
463   if (lang == Mnil)
464     return FALSE;
465   gtk_tree_model_get (model, iter, COL_STATUS, &status, -1);  
466   if (status != MIM_STATUS_MODIFIED)
467     return FALSE;
468   status = get_mim_status (lang, name);
469   update_child_row (model, iter, status, config_status, NULL);
470   return FALSE;
471 }
472
473 static int initialized = 0;
474
475 static void
476 destroy_cb (GtkWidget *widget, gpointer data)
477 {
478   M17N_FINI ();
479 }
480
481 \f
482 /* Public API */
483
484 GtkWidget *
485 mim_config_new (GCallback func, gpointer data)
486 {
487   GtkWidget *tree, *config;
488   GtkTreeStore *store;
489   GtkCellRenderer *renderer;
490   GtkTreeViewColumn *column;
491
492   if (initialized)
493     return NULL;
494   M17N_INIT ();
495   if (merror_code < 0)
496     return NULL;
497
498 #if ENABLE_NLS
499   bindtextdomain ("m17n-im-config", GETTEXTDIR);
500   bind_textdomain_codeset ("m17n-im-config", "UTF-8");
501 #endif
502
503   mim_status_str[MIM_STATUS_DEFAULT] = _("default");
504   mim_status_str[MIM_STATUS_CUSTOMIZED] = _("customized");
505   mim_status_str[MIM_STATUS_MODIFIED] = _("modified");
506   mim_status_str[MIM_STATUS_NO] = _("uncustomizable");
507
508   store = make_store_for_input_methods ();
509   tree = gtk_tree_view_new_with_model (GTK_TREE_MODEL (store));
510   g_object_unref (G_OBJECT (store));
511
512   renderer = gtk_cell_renderer_text_new ();
513   column = (gtk_tree_view_column_new_with_attributes
514             (_("Input Method"), renderer, "text", COL_TAG, NULL));
515   gtk_tree_view_append_column (GTK_TREE_VIEW (tree), column);
516
517   renderer = gtk_cell_renderer_text_new ();
518   column = (gtk_tree_view_column_new_with_attributes
519             (_("Status"), renderer, "text", COL_STATUS_STR, NULL));
520   gtk_tree_view_append_column (GTK_TREE_VIEW (tree), column);
521
522   g_signal_connect (G_OBJECT (tree), "row-expanded",
523                     G_CALLBACK (tree_expanded_cb), NULL);
524   g_signal_connect (G_OBJECT (tree), "row-activated",
525                     G_CALLBACK (tree_activated_cb), NULL);
526
527   config =gtk_scrolled_window_new (NULL, NULL);
528   gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (config),
529                                   GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
530   gtk_scrolled_window_add_with_viewport (GTK_SCROLLED_WINDOW (config), tree);
531   g_signal_connect (G_OBJECT (config), "destroy",
532                     G_CALLBACK (destroy_cb), NULL);
533
534   g_object_set_data (G_OBJECT (config), CONFIG_TREE_VIEW, tree);
535   if (func)
536     {
537       MimConfigCallback *callback;
538
539       callback = g_new (MimConfigCallback, 1);
540       callback->widget = config;
541       callback->func = (MimConfigCallbackFunc) func;
542       callback->data = data;
543       g_object_set_data_full (G_OBJECT (tree), CONFIG_CALLBACK_DATA,
544                               callback, g_free);
545     }
546
547   return config;
548 }
549
550 gboolean
551 mim_config_modified (GtkWidget *config)
552 {
553   GtkTreeView *tree;
554   GtkTreeModel *model;
555   MimConfigStatus *config_status;
556
557   tree = g_object_get_data (G_OBJECT (config), CONFIG_TREE_VIEW);
558   model = gtk_tree_view_get_model (tree);
559   config_status = g_object_get_data (G_OBJECT (model), CONFIG_STATUS_DATA);
560
561   return (config_status->num_modified > 0 ? TRUE : FALSE);
562 }
563
564 gboolean
565 mim_config_revert (GtkWidget *config)
566 {
567   GtkTreeView *tree;
568   GtkTreeModel *model;
569   MimConfigStatus *config_status;
570
571   tree = g_object_get_data (G_OBJECT (config), CONFIG_TREE_VIEW);
572   model = gtk_tree_view_get_model (tree);
573   config_status = g_object_get_data (G_OBJECT (model), CONFIG_STATUS_DATA);
574
575   if (config_status->num_modified == 0)
576     return FALSE;
577   gtk_tree_model_foreach (model, revert_to_saved, config_status);
578   return TRUE;
579 }
580
581 gboolean
582 mim_config_save (GtkWidget *config)
583 {
584   GtkTreeView *tree;
585   GtkTreeModel *model;
586   MimConfigStatus *config_status;
587
588   tree = g_object_get_data (G_OBJECT (config), CONFIG_TREE_VIEW);
589   model = gtk_tree_view_get_model (tree);
590   config_status = g_object_get_data (G_OBJECT (model), CONFIG_STATUS_DATA);
591
592   if (config_status->num_modified == 0)
593     return FALSE;
594   minput_save_config ();
595   gtk_tree_model_foreach (model, set_as_saved, config_status);
596   return TRUE;
597 }
598
599 GtkTreeView *
600 mim_config_get_tree_view (GtkWidget *config)
601 {
602   GtkTreeView *tree;
603
604   tree = g_object_get_data (G_OBJECT (config), CONFIG_TREE_VIEW);
605   return tree;
606 }