*** 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       status = MIM_STATUS_DEFAULT;
82       p = mplist_next (mplist_next (p));
83       status_symbol = mplist_value (p);
84       if (status_symbol != Mnil && status_symbol != Minherited)
85         return (status_symbol == Mcustomized
86                 ? MIM_STATUS_CUSTOMIZED : MIM_STATUS_MODIFIED);
87     }
88   for (plist = minput_get_command (lang, name, Mnil);
89        plist && mplist_key (plist) != Mnil; plist = mplist_next (plist))
90     {
91       MPlist *p = mplist_value (plist);
92       MSymbol status_symbol;
93
94       status = MIM_STATUS_DEFAULT;
95       p = mplist_next (mplist_next (p));
96       status_symbol = mplist_value (p);
97       if (status_symbol != Mnil && status_symbol != Minherited)
98         return (status_symbol == Mcustomized
99                 ? MIM_STATUS_CUSTOMIZED : MIM_STATUS_MODIFIED);
100     }
101   return status;
102 }
103
104 /* Columns of each row.  */
105 enum
106   {
107     /* parent: language name
108         child: IM name  */
109     COL_TAG = 0,
110     /* parent: NULL or "modified"
111         child: "default", "customized", or "modified"  */
112     COL_STATUS_STR,
113     /* parent: num of modified children
114         child: enum MimStatus */
115     COL_STATUS,
116     /* parent: Mnil
117         child: symbolic language name.  */
118     COL_LANG,
119     /* parent: Mnil
120         child: symbolic IM name.  */
121     COL_NAME,
122     /* number of columns  */
123     NUM_COLS
124   };
125
126 /* Called when a row is expanded.  We may have to initialize
127    children.  */
128 static void
129 tree_expanded_cb (GtkTreeView *tree, GtkTreeIter *parent,
130                   GtkTreePath *path, gpointer data)
131 {
132   GtkTreeModel *model;
133   GtkTreeIter iter;
134   MSymbol lang, name;
135
136   model = gtk_tree_view_get_model (tree);
137   if (gtk_tree_model_iter_children (model, &iter, parent))
138     {
139       gchar *status_str;
140
141       gtk_tree_model_get (model, &iter, COL_STATUS_STR, &status_str, -1);
142       if (! status_str)
143         {
144           /* The first child is not yet initialized, and that means
145              the remaining children are not initialized either.  */
146           gtk_tree_model_get (model, &iter, COL_LANG, &lang, -1);
147           do {
148             enum MimStatus im_status;
149
150             gtk_tree_model_get (model, &iter, COL_NAME, &name, -1);
151             im_status = get_mim_status (lang, name);
152             gtk_tree_store_set (GTK_TREE_STORE (model), &iter,
153                                 COL_STATUS_STR, mim_status_str[im_status],
154                                 COL_STATUS, im_status,
155                                 -1);
156           } while (gtk_tree_model_iter_next (model, &iter));
157         }
158     }
159 }
160
161 static void
162 edit_im (GtkTreeView *tree, MSymbol lang, MSymbol name)
163 {
164   GtkWidget *dialog, *label;
165   gint response;
166
167   dialog = (gtk_dialog_new_with_buttons
168             (_("Edit"),
169              GTK_WINDOW (gtk_widget_get_toplevel (GTK_WIDGET (tree))),
170              GTK_DIALOG_DESTROY_WITH_PARENT | GTK_DIALOG_NO_SEPARATOR,
171              _("Default"), 0,
172              GTK_STOCK_REVERT_TO_SAVED, GTK_RESPONSE_NO,
173              GTK_STOCK_EDIT, GTK_RESPONSE_YES,
174              GTK_STOCK_CANCEL, GTK_RESPONSE_CANCEL,
175              NULL));
176   label = gtk_label_new (msymbol_name (name));
177   gtk_container_add (GTK_CONTAINER (GTK_DIALOG (dialog)->vbox), label);
178   gtk_widget_show_all (dialog);
179   response = gtk_dialog_run (GTK_DIALOG (dialog));
180   if (response != GTK_RESPONSE_CANCEL
181       && lang == Mt && (name == Mnil || name == msymbol ("unicode")))
182     {
183       MSymbol variable = msymbol (name == Mnil ? "candidates-group-size"
184                                   : "prompt");
185
186       if (response == GTK_RESPONSE_NO)
187         {
188           minput_config_variable (lang, name, variable, NULL);
189         }
190       else if (response == 0)
191         {
192           MPlist *plist = mplist ();
193
194           minput_config_variable (lang, name, variable, plist);
195           m17n_object_unref (plist);
196         }
197       else
198         {
199           MPlist *plist = mplist ();
200
201           if (name == Mnil)
202             {
203               mplist_add (plist, Minteger, (void *) 3);
204             }
205           else
206             {
207               MText *mt = mtext_from_data (">>", 2, MTEXT_FORMAT_US_ASCII);
208               mplist_add (plist, Mtext, mt);
209               m17n_object_unref (mt);
210             }
211           minput_config_variable (lang, name, variable, plist);
212           m17n_object_unref (plist);
213         }
214     }
215
216   gtk_widget_destroy (dialog);
217 }
218
219 static void
220 update_child_row (GtkTreeModel *model, GtkTreeIter *iter,
221                   enum MimStatus status, MimConfigStatus *config_status,
222                   GtkTreeView *tree)
223 {
224   GtkTreeIter parent;
225   gint inc_modified;
226
227   inc_modified = (status == MIM_STATUS_MODIFIED ? 1 : -1);
228
229   gtk_tree_store_set (GTK_TREE_STORE (model), iter,
230                       COL_STATUS_STR, mim_status_str[status],
231                       COL_STATUS, status, -1);
232   if (gtk_tree_model_iter_parent (model, &parent, iter))
233     {
234       gint num_modified;
235       gchar *status_str;
236
237       gtk_tree_model_get (model, &parent, COL_STATUS, &num_modified, -1);
238       num_modified += inc_modified;
239       gtk_tree_store_set (GTK_TREE_STORE (model), &parent,
240                           COL_STATUS, num_modified, -1);
241       if (num_modified <= 1)
242         {
243           status_str = (status == MIM_STATUS_MODIFIED
244                         ? mim_status_str[MIM_STATUS_MODIFIED] : NULL);
245           gtk_tree_store_set (GTK_TREE_STORE (model), &parent,
246                               COL_STATUS_STR, status_str);
247         }
248     }
249       
250   if (! config_status)
251     config_status = g_object_get_data (G_OBJECT (model), CONFIG_STATUS_DATA);
252   config_status->num_modified += inc_modified;
253   if (tree && config_status->num_modified <= 1)
254     {
255       MimConfigCallback *callback;
256
257       callback = g_object_get_data (G_OBJECT (tree), CONFIG_CALLBACK_DATA);
258       if (callback)
259         callback->func (callback->widget, callback->data);
260     }
261 }
262
263 static void
264 tree_activated_cb (GtkTreeView *tree, GtkTreePath *path,
265                    GtkTreeViewColumn *column, gpointer data)
266 {
267   GtkTreeModel *model;
268   GtkTreeIter iter;
269
270   model = gtk_tree_view_get_model (tree);
271   if (gtk_tree_model_get_iter (model, &iter, path))
272     {
273       MSymbol lang, name;
274
275       gtk_tree_model_get (model, &iter, COL_LANG, &lang, COL_NAME, &name, -1);
276       if (lang != Mnil)
277         {
278           /* child row for an IM */
279           enum MimStatus old, new;
280
281           old = get_mim_status (lang, name);
282           edit_im (tree, lang, name);
283           new = get_mim_status (lang, name);
284           if (old != new)
285             update_child_row (model, &iter, new, NULL, tree);
286         }
287       else
288         {
289           /* parent row for a language */
290           if (gtk_tree_view_row_expanded (tree, path))
291             gtk_tree_view_collapse_row (tree, path);
292           else
293             gtk_tree_view_expand_row (tree, path, TRUE);
294         }
295     }
296 }
297
298 typedef struct _MimTable
299 {
300   gchar *lang;
301   gchar *name;
302   MSymbol symlang;
303   MSymbol symname;
304 } MimTable;
305
306 static int
307 sort_im (const void *p1, const void *p2)
308 {
309   const MimTable *t1 = p1;
310   const MimTable *t2 = p2;
311   int result = strcmp (t1->lang, t2->lang);
312
313   return (result ? result : strcmp (t1->name, t2->name));
314 }
315
316 static GtkTreeStore *
317 make_store_for_input_methods ()
318 {
319   GtkTreeStore *store;
320   MPlist *imlist, *p;
321   int i;
322   MimTable *imtable;
323   char *lang;
324   GtkTreeIter iter1, iter2;
325   enum MimStatus status;
326   MimConfigStatus *config_status;
327
328   store = gtk_tree_store_new (NUM_COLS,
329                               G_TYPE_STRING,  /* COL_TAG */
330                               G_TYPE_STRING,  /* COL_STATUS_STR */
331                               G_TYPE_UINT,    /* COL_STATUS */
332                               G_TYPE_POINTER, /* COL_LANG */
333                               G_TYPE_POINTER  /* COL_NAME */
334                               );
335
336   config_status = g_new0 (MimConfigStatus, 1);
337   gtk_tree_store_append (store, &iter1, NULL);
338   status = get_mim_status (Mt, Mnil);
339   gtk_tree_store_set (store, &iter1,
340                       COL_TAG, _("global"),
341                       COL_STATUS_STR, mim_status_str[status],
342                       COL_STATUS, status, 
343                       COL_LANG, Mt,
344                       COL_NAME, Mnil,
345                       -1);
346
347   imlist = mdatabase_list (msymbol ("input-method"), Mnil, Mnil, Mnil);
348   config_status->num_im = mplist_length (imlist);
349   imtable = g_newa (MimTable, config_status->num_im);
350   for (i = 0, p = imlist; mplist_key (p) != Mnil; p = mplist_next (p))
351     {
352       MDatabase *mdb = (MDatabase *) mplist_value (p);
353       MSymbol *tag = mdatabase_tag (mdb);
354
355       if (tag[1] != Mnil && tag[2] != Mnil)
356         {
357           MSymbol language = mlanguage_name (tag[1]);
358
359           if (language != Mnil)
360             imtable[i].lang = msymbol_name (language);
361           else
362             /* `~' is for putting this element at the tail by sort.  */
363             imtable[i].lang = "~other";
364           imtable[i].name = msymbol_name (tag[2]);
365           imtable[i].symlang = tag[1];
366           imtable[i].symname = tag[2];
367           i++;
368         }
369     }
370   m17n_object_unref (imlist);
371   config_status->num_im = i;
372   qsort (imtable, config_status->num_im, sizeof (MimTable), sort_im);
373
374   for (lang = NULL, i = 0; i < config_status->num_im; i++)
375     {
376       if (lang != imtable[i].lang)
377         {
378           gchar *name;
379
380           gtk_tree_store_append (store, &iter1, NULL);
381           lang = imtable[i].lang;
382           if (lang[0] != '~')
383             {
384               MText *native_text;
385               gchar *native = NULL;
386               int nbytes;
387
388               if (imtable[i].symlang != Mt
389                   && (native_text = mlanguage_text (imtable[i].symlang)))
390                 {
391                   enum MTextFormat fmt;
392
393                   native = mtext_data (native_text, &fmt, &nbytes,
394                                        NULL, NULL);
395                   if (fmt != MTEXT_FORMAT_US_ASCII
396                       && fmt != MTEXT_FORMAT_UTF_8)
397                     native = 0;
398                 }
399               if (native)
400                 {
401                   name = alloca (strlen (lang) + nbytes + 4);
402                   sprintf (name, "%s (%s)", lang, native);
403                 }
404               else
405                 name = lang;
406             }
407           else
408             name = lang + 1;
409
410           gtk_tree_store_set (store, &iter1,
411                               COL_TAG, name,
412                               COL_STATUS_STR, NULL,
413                               COL_STATUS, 0,
414                               COL_LANG, Mnil,
415                               COL_NAME, Mnil,
416                               -1);
417         }
418       gtk_tree_store_append (store, &iter2, &iter1);
419       gtk_tree_store_set (store, &iter2,
420                           COL_TAG, imtable[i].name,
421                           COL_STATUS_STR, NULL,
422                           COL_LANG, imtable[i].symlang,
423                           COL_NAME, imtable[i].symname,
424                           -1);
425     }
426   config_status->num_modified = 0;
427   g_object_set_data_full (G_OBJECT (store), CONFIG_STATUS_DATA,
428                           config_status, g_free);
429   return store;
430 }
431
432 static gboolean
433 revert_to_saved (GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter,
434                gpointer data)
435 {
436   enum MimStatus status;
437   MSymbol lang, name;
438   MimConfigStatus *config_status = data;
439
440   gtk_tree_model_get (model, iter, COL_LANG, &lang, COL_NAME, &name, -1);
441   if (lang == Mnil)
442     return FALSE;
443   gtk_tree_model_get (model, iter, COL_STATUS, &status, -1);  
444   if (status != MIM_STATUS_MODIFIED)
445     return FALSE;
446   minput_config_variable (lang, name, Mnil, NULL);
447   minput_config_command (lang, name, Mnil, NULL);
448   status = get_mim_status (lang, name);
449   update_child_row (model, iter, status, config_status, NULL);
450   return FALSE;
451 }
452
453 static gboolean
454 set_as_saved (GtkTreeModel *model, GtkTreePath *path, GtkTreeIter *iter,
455            gpointer data)
456 {
457   enum MimStatus status;
458   MSymbol lang, name;
459   MimConfigStatus *config_status = data;
460
461   gtk_tree_model_get (model, iter, COL_LANG, &lang, COL_NAME, &name, -1);
462   if (lang == Mnil)
463     return FALSE;
464   gtk_tree_model_get (model, iter, COL_STATUS, &status, -1);  
465   if (status != MIM_STATUS_MODIFIED)
466     return FALSE;
467   status = get_mim_status (lang, name);
468   update_child_row (model, iter, status, config_status, NULL);
469   return FALSE;
470 }
471
472 static int initialized = 0;
473
474 static void
475 destroy_cb (GtkWidget *widget, gpointer data)
476 {
477   M17N_FINI ();
478 }
479
480 \f
481 /* Public API */
482
483 GtkWidget *
484 mim_config_new (GCallback func, gpointer data)
485 {
486   GtkWidget *tree, *config;
487   GtkTreeStore *store;
488   GtkCellRenderer *renderer;
489   GtkTreeViewColumn *column;
490
491   if (initialized)
492     return NULL;
493   M17N_INIT ();
494   if (merror_code < 0)
495     return NULL;
496
497 #if ENABLE_NLS
498   bindtextdomain ("m17n-im-config", GETTEXTDIR);
499   bind_textdomain_codeset ("m17n-im-config", "UTF-8");
500 #endif
501
502   mim_status_str[MIM_STATUS_DEFAULT] = _("default");
503   mim_status_str[MIM_STATUS_CUSTOMIZED] = _("customized");
504   mim_status_str[MIM_STATUS_MODIFIED] = _("modified");
505   mim_status_str[MIM_STATUS_NO] = _("uncustomizable");
506
507   store = make_store_for_input_methods ();
508   tree = gtk_tree_view_new_with_model (GTK_TREE_MODEL (store));
509   g_object_unref (G_OBJECT (store));
510
511   renderer = gtk_cell_renderer_text_new ();
512   column = (gtk_tree_view_column_new_with_attributes
513             (_("Input Method"), renderer, "text", COL_TAG, NULL));
514   gtk_tree_view_append_column (GTK_TREE_VIEW (tree), column);
515
516   renderer = gtk_cell_renderer_text_new ();
517   column = (gtk_tree_view_column_new_with_attributes
518             (_("status"), renderer, "text", COL_STATUS_STR, NULL));
519   gtk_tree_view_append_column (GTK_TREE_VIEW (tree), column);
520
521   g_signal_connect (G_OBJECT (tree), "row-expanded",
522                     G_CALLBACK (tree_expanded_cb), NULL);
523   g_signal_connect (G_OBJECT (tree), "row-activated",
524                     G_CALLBACK (tree_activated_cb), NULL);
525
526   config =gtk_scrolled_window_new (NULL, NULL);
527   gtk_scrolled_window_set_policy (GTK_SCROLLED_WINDOW (config),
528                                   GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
529   gtk_scrolled_window_add_with_viewport (GTK_SCROLLED_WINDOW (config), tree);
530   g_signal_connect (G_OBJECT (config), "destroy",
531                     G_CALLBACK (destroy_cb), NULL);
532
533   g_object_set_data (G_OBJECT (config), CONFIG_TREE_VIEW, tree);
534   if (func)
535     {
536       MimConfigCallback *callback;
537
538       callback = g_new (MimConfigCallback, 1);
539       callback->widget = config;
540       callback->func = (MimConfigCallbackFunc) func;
541       callback->data = data;
542       g_object_set_data_full (G_OBJECT (tree), CONFIG_CALLBACK_DATA,
543                               callback, g_free);
544     }
545
546   return config;
547 }
548
549 gboolean
550 mim_config_modified (GtkWidget *config)
551 {
552   GtkTreeView *tree;
553   GtkTreeModel *model;
554   MimConfigStatus *config_status;
555
556   tree = g_object_get_data (G_OBJECT (config), CONFIG_TREE_VIEW);
557   model = gtk_tree_view_get_model (tree);
558   config_status = g_object_get_data (G_OBJECT (model), CONFIG_STATUS_DATA);
559
560   return (config_status->num_modified > 0 ? TRUE : FALSE);
561 }
562
563 gboolean
564 mim_config_revert (GtkWidget *config)
565 {
566   GtkTreeView *tree;
567   GtkTreeModel *model;
568   MimConfigStatus *config_status;
569
570   tree = g_object_get_data (G_OBJECT (config), CONFIG_TREE_VIEW);
571   model = gtk_tree_view_get_model (tree);
572   config_status = g_object_get_data (G_OBJECT (model), CONFIG_STATUS_DATA);
573
574   if (config_status->num_modified == 0)
575     return FALSE;
576   gtk_tree_model_foreach (model, revert_to_saved, config_status);
577   return TRUE;
578 }
579
580 gboolean
581 mim_config_save (GtkWidget *config)
582 {
583   GtkTreeView *tree;
584   GtkTreeModel *model;
585   MimConfigStatus *config_status;
586
587   tree = g_object_get_data (G_OBJECT (config), CONFIG_TREE_VIEW);
588   model = gtk_tree_view_get_model (tree);
589   config_status = g_object_get_data (G_OBJECT (model), CONFIG_STATUS_DATA);
590
591   if (config_status->num_modified == 0)
592     return FALSE;
593   minput_save_config ();
594   gtk_tree_model_foreach (model, set_as_saved, config_status);
595   return TRUE;
596 }