[accounts-dialog] Improved account creation dialog



commit 29f3799600ad444b023400424212c8b383988670
Author: Matthias Clasen <mclasen redhat com>
Date:   Fri Jan 22 21:49:53 2010 -0500

    Improved account creation dialog
    
    It is now possible to set the account type when creating a new user,
    and we generate several proposals for shortnames. The improved
    shortname generation is based on code written by Milan Bouchet-Valat
    for gnome-system-tools.

 data/account-dialog.ui  |   77 +++++++++--
 src/um-account-dialog.c |  354 ++++++++++++++++++++++++++++++++++++++---------
 src/um-user-manager.c   |   16 ++-
 src/um-user-manager.h   |    3 +-
 4 files changed, 368 insertions(+), 82 deletions(-)
---
diff --git a/data/account-dialog.ui b/data/account-dialog.ui
index e2cb9c1..2322683 100644
--- a/data/account-dialog.ui
+++ b/data/account-dialog.ui
@@ -1,13 +1,31 @@
 <?xml version="1.0"?>
 <interface>
+  <!-- interface-requires gtk+ 2.12 -->
+  <!-- interface-naming-policy toplevel-contextual -->
+  <object class="GtkListStore" id="shortname-model">
+    <columns>
+      <column type="gchararray"/>
+    </columns>
+  </object>
+  <object class="GtkListStore" id="account-type-model">
+    <columns>
+      <column type="gchararray"/>
+      <column type="gint"/>
+    </columns>
+    <data>
+      <row><col id="0" translatable="True" context="Account type">Standard</col><col id="1">0</col></row>
+      <row><col id="0" translatable="True" context="Account type">Administrator</col><col id="1">1</col></row>
+      <row><col id="0" translatable="True" context="Account type">Supervised</col><col id="1">2</col></row>
+    </data>
+  </object>
   <object class="GtkDialog" id="dialog">
     <property name="border_width">5</property>
+    <property name="title"> </property>
     <property name="resizable">False</property>
     <property name="modal">True</property>
     <property name="window_position">center-on-parent</property>
     <property name="type_hint">dialog</property>
     <property name="has_separator">False</property>
-    <property name="title"> </property>
     <child internal-child="vbox">
       <object class="GtkVBox" id="content-area">
         <property name="visible">True</property>
@@ -21,21 +39,22 @@
             <child>
               <object class="GtkTable" id="table1">
                 <property name="visible">True</property>
-                <property name="n_rows">4</property>
+                <property name="n_rows">5</property>
                 <property name="n_columns">2</property>
                 <property name="column_spacing">6</property>
                 <property name="row_spacing">6</property>
                 <child>
-                  <object class="GtkEntry" id="shortname-entry">
+                  <object class="GtkComboBoxEntry" id="shortname-combo">
                     <property name="visible">True</property>
                     <property name="can_focus">True</property>
-                    <property name="activates_default">True</property>
+                    <property name="model">shortname-model</property>
+                    <property name="text_column">0</property>
                   </object>
                   <packing>
                     <property name="left_attach">1</property>
                     <property name="right_attach">2</property>
-                    <property name="top_attach">3</property>
-                    <property name="bottom_attach">4</property>
+                    <property name="top_attach">4</property>
+                    <property name="bottom_attach">5</property>
                   </packing>
                 </child>
                 <child>
@@ -48,8 +67,8 @@
                     </attributes>
                   </object>
                   <packing>
-                    <property name="top_attach">3</property>
-                    <property name="bottom_attach">4</property>
+                    <property name="top_attach">4</property>
+                    <property name="bottom_attach">5</property>
                     <property name="x_options">GTK_FILL</property>
                   </packing>
                 </child>
@@ -112,8 +131,8 @@
                   <packing>
                     <property name="left_attach">1</property>
                     <property name="right_attach">2</property>
-                    <property name="top_attach">2</property>
-                    <property name="bottom_attach">3</property>
+                    <property name="top_attach">3</property>
+                    <property name="bottom_attach">4</property>
                   </packing>
                 </child>
                 <child>
@@ -126,9 +145,41 @@
                     </attributes>
                   </object>
                   <packing>
+                    <property name="top_attach">3</property>
+                    <property name="bottom_attach">4</property>
+                    <property name="x_options">GTK_FILL</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkLabel" id="label7">
+                    <property name="visible">True</property>
+                    <property name="xalign">1</property>
+                    <property name="label" translatable="yes">Account Type:</property>
+                    <attributes>
+                      <attribute name="foreground" value="#555555555555"/>
+                    </attributes>
+                  </object>
+                  <packing>
+                    <property name="top_attach">2</property>
+                    <property name="bottom_attach">3</property>
+                  </packing>
+                </child>
+                <child>
+                  <object class="GtkComboBox" id="account-type-combo">
+                    <property name="visible">True</property>
+                    <property name="model">account-type-model</property>
+                    <child>
+                      <object class="GtkCellRendererText" id="account-type-cell"/>
+                      <attributes>
+                        <attribute name="text">0</attribute>
+                      </attributes>
+                    </child>
+                  </object>
+                  <packing>
+                    <property name="left_attach">1</property>
+                    <property name="right_attach">2</property>
                     <property name="top_attach">2</property>
                     <property name="bottom_attach">3</property>
-                    <property name="x_options">GTK_FILL</property>
                   </packing>
                 </child>
               </object>
@@ -185,5 +236,9 @@
         </child>
       </object>
     </child>
+    <action-widgets>
+      <action-widget response="0">cancel-button</action-widget>
+      <action-widget response="0">ok-button</action-widget>
+    </action-widgets>
   </object>
 </interface>
diff --git a/src/um-account-dialog.c b/src/um-account-dialog.c
index f0a3baf..8edc623 100644
--- a/src/um-account-dialog.c
+++ b/src/um-account-dialog.c
@@ -33,9 +33,13 @@
 
 struct _UmAccountDialog {
         GtkWidget *dialog;
-        GtkWidget *shortname_entry;
+        GtkWidget *shortname_combo;
         GtkWidget *name_entry;
+        GtkWidget *account_type_combo;
         GtkWidget *ok_button;
+
+        gboolean valid_name;
+        gboolean valid_shortname;
 };
 
 static void
@@ -46,96 +50,314 @@ cancel_account_dialog (GtkButton       *button,
 }
 
 static void
-user_created (GError *error, gpointer data)
-{
-	if (error) {
-		g_warning ("Creating user failed: %s", error->message);
-	}
-}
-
-static void
 accept_account_dialog (GtkButton       *button,
                        UmAccountDialog *um)
 {
         UmUserManager *manager;
         const gchar *shortname;
         const gchar *name;
+        gint account_type;
+        GtkTreeModel *model;
+        GtkTreeIter iter;
 
-
-        shortname = gtk_entry_get_text (GTK_ENTRY (um->shortname_entry));
-        name = gtk_entry_get_text (GTK_ENTRY (um->shortname_entry));
+        name = gtk_entry_get_text (GTK_ENTRY (um->name_entry));
+        shortname = gtk_combo_box_get_active_text (GTK_COMBO_BOX (um->shortname_combo));
+        model = gtk_combo_box_get_model (GTK_COMBO_BOX (um->account_type_combo));
+        gtk_combo_box_get_active_iter (GTK_COMBO_BOX (um->account_type_combo), &iter);
+        gtk_tree_model_get (model, &iter, 1, &account_type, -1);
 
         manager = um_user_manager_ref_default ();
-        um_user_manager_create_user (manager, shortname, name);
+        um_user_manager_create_user (manager, shortname, name, account_type);
         g_object_unref (manager);
 
         gtk_widget_hide (um->dialog);
 }
 
-static void
-update_sensitivity (UmAccountDialog *um)
+static gboolean
+is_shortname_used (const gchar *shortname)
 {
-        const gchar *name, *shortname;
-        gboolean can_create;
         UmUserManager *manager;
         UmUser *user;
-        gchar *tip;
 
-        name = gtk_entry_get_text (GTK_ENTRY (um->name_entry));
-        shortname = gtk_entry_get_text (GTK_ENTRY (um->shortname_entry));
-
-        can_create = name[0] != 0 && shortname[0] != 0;
-        if (can_create) {
-                manager = um_user_manager_ref_default ();
-                user = um_user_manager_get_user (manager, shortname);
-                g_object_unref (manager);
-                if (user != NULL) {
-                        gtk_widget_error_bell (um->shortname_entry);
-                        gtk_entry_set_icon_from_stock (GTK_ENTRY (um->shortname_entry),
-                                                       GTK_ENTRY_ICON_SECONDARY,
-                                                       GTK_STOCK_DIALOG_ERROR);
-                        tip = g_strdup_printf (_("A user with the short name '%s' already exists."),
-                                               shortname);
-                        gtk_entry_set_icon_tooltip_text (GTK_ENTRY (um->shortname_entry),
-                                                         GTK_ENTRY_ICON_SECONDARY,
-                                                         tip);
-                        g_free (tip);
-
-                        can_create = FALSE;
-                }
-               else {
-                        gtk_entry_set_icon_from_pixbuf (GTK_ENTRY (um->shortname_entry),
-                                                        GTK_ENTRY_ICON_SECONDARY,
-                                                        NULL);
-                }
-        }
+        manager = um_user_manager_ref_default ();
+        user = um_user_manager_get_user (manager, shortname);
+        g_object_unref (manager);
 
-        gtk_widget_set_sensitive (um->ok_button, can_create);
+        return user != NULL;
 }
 
 static void
-shortname_changed (GtkWidget       *entry,
-                   GParamSpec      *pspec,
+shortname_changed (GtkComboBox     *combo,
                    UmAccountDialog *um)
 {
-        update_sensitivity (um);
+        gboolean in_use;
+        gboolean empty;
+        gboolean valid;
+        const gchar *shortname;
+        const gchar *c;
+        gchar *tip;
+        GtkWidget *entry;
+
+        shortname = gtk_combo_box_get_active_text (combo);
+
+        in_use = is_shortname_used (shortname);
+        empty = strlen (shortname) == 0;
+        valid = TRUE;
+
+        if (!in_use && !empty) {
+                /* First char must be a letter, and it must only composed
+                 * of ASCII letters, digits, and a '.', '-', '_'
+                 */
+                for (c = shortname; *c; c++) {
+                        if ((c == shortname && !g_ascii_islower (*c)) ||
+                            !(g_ascii_isdigit (*c) || g_ascii_islower (*c) ||
+                              *c == '.' || *c == '-' || *c == '_' )) {
+                                valid = FALSE;
+                        }
+                }
+        }
+
+        um->valid_shortname = !empty && !in_use && valid;
+        gtk_widget_set_sensitive (um->ok_button, um->valid_name && um->valid_shortname);
+
+        entry = gtk_bin_get_child (GTK_BIN (combo));
+
+        if (!empty && (in_use || !valid)) {
+                gtk_entry_set_icon_from_stock (GTK_ENTRY (entry),
+                                               GTK_ENTRY_ICON_SECONDARY,
+                                               GTK_STOCK_DIALOG_ERROR);
+                if (in_use) {
+                        tip = g_strdup_printf (_("A user with the short name '%s' already exists."),
+                                               shortname);
+                }
+                else if (!g_ascii_islower (shortname[0])) {
+                        tip = g_strdup (_("The short name must start with a lowercase letter."));
+                }
+                else {
+                        tip = g_strdup (_("The short name must consist of:\n"
+                                          " \xe2\x9e\xa3 lower case letters from the English alphabet\n"
+                                          " \xe2\x9e\xa3 digits\n"
+                                          " \xe2\x9e\xa3 any of the characters '.', '-' and '_'"));
+                }
+
+                gtk_entry_set_icon_tooltip_text (GTK_ENTRY (entry),
+                                                 GTK_ENTRY_ICON_SECONDARY,
+                                                 tip);
+                g_free (tip);
+        }
+        else {
+               gtk_entry_set_icon_from_pixbuf (GTK_ENTRY (entry),
+                                               GTK_ENTRY_ICON_SECONDARY,
+                                               NULL);
+        }
 }
 
 static void
-name_changed (GtkWidget       *entry,
+name_changed (GtkEntry        *name_entry,
               GParamSpec      *pspec,
               UmAccountDialog *um)
 {
-        const gchar *name;
-        gchar *shortname;
+        GtkWidget *entry;
+        GtkTreeModel *model;
+        gboolean in_use;
+        const char *name;
+        char *lc_name, *ascii_name, *stripped_name;
+        char **words1;
+        char **words2 = NULL;
+        char **w1, **w2;
+        char *c;
+        char *unicode_fallback = "?";
+        GString *first_word, *last_word;
+        GString *item1, *item2, *item3, *item4;
+        int len;
+        int nwords1, nwords2, i;
+        GHashTable *items;
+
+        entry = gtk_bin_get_child (GTK_BIN (um->shortname_combo));
+        model = gtk_combo_box_get_model (GTK_COMBO_BOX (um->shortname_combo));
+        gtk_list_store_clear (GTK_LIST_STORE (model));
+
+        name = gtk_entry_get_text (GTK_ENTRY (name_entry));
+
+        um->valid_name = (strlen (name) > 0);
+        gtk_widget_set_sensitive (um->ok_button, um->valid_name && um->valid_shortname);
+
+        if (!um->valid_name) {
+                gtk_entry_set_text (GTK_ENTRY (entry), "");
+                return;
+        }
 
-        name = gtk_entry_get_text (GTK_ENTRY (um->name_entry));
+        ascii_name = g_convert_with_fallback (name, -1, "ASCII//TRANSLIT", "UTF-8",
+                                              unicode_fallback, NULL, NULL, NULL);
+
+        lc_name = g_ascii_strdown (ascii_name, -1);
+
+        /* remove all non ASCII alphanumeric chars from the name,
+         * apart from the few allowed symbols
+         */
+        stripped_name = g_strnfill (strlen (lc_name) + 1, '\0');
+        i = 0;
+        for (c = lc_name; *c; c++) {
+                if (!(g_ascii_isdigit (*c) || g_ascii_islower (*c) ||
+                    *c == ' ' || *c == '-' || *c == '.' || *c == '_' ||
+                    /* used to track invalid words, removed below */
+                    *c == '?') )
+                        continue;
+
+                    stripped_name[i] = *c;
+                    i++;
+        }
 
-        shortname = um_compute_short_name (name);
-        gtk_entry_set_text (GTK_ENTRY (um->shortname_entry), shortname);
-        g_free (shortname);
+        if (strlen (stripped_name) == 0) {
+                g_free (ascii_name);
+                g_free (lc_name);
+                g_free (stripped_name);
+                return;
+        }
+
+        /* we split name on spaces, and then on dashes, so that we can treat
+         * words linked with dashes the same way, i.e. both fully shown, or
+         * both abbreviated
+         */
+        words1 = g_strsplit_set (stripped_name, " ", -1);
+        len = g_strv_length (words1);
+
+        g_free (ascii_name);
+        g_free (lc_name);
+        g_free (stripped_name);
+
+        /* Concatenate the whole first word with the first letter of each
+         * word (item1), and the last word with the first letter of each
+         * word (item2). item3 and item4 are symmetrical respectively to
+         * item1 and item2.
+         *
+         * Constant 5 is the max reasonable number of words we may get when
+         * splitting on dashes, since we can't guess it at this point,
+         * and reallocating would be too bad.
+         */
+        item1 = g_string_sized_new (strlen (words1[0]) + len - 1 + 5);
+        item3 = g_string_sized_new (strlen (words1[0]) + len - 1 + 5);
+
+        item2 = g_string_sized_new (strlen (words1[len - 1]) + len - 1 + 5);
+        item4 = g_string_sized_new (strlen (words1[len - 1]) + len - 1 + 5);
+
+        /* again, guess at the max size of names */
+        first_word = g_string_sized_new (20);
+        last_word = g_string_sized_new (20);
+
+        nwords1 = 0;
+        nwords2 = 0;
+        for (w1 = words1; *w1; w1++) {
+                if (strlen (*w1) == 0)
+                        continue;
+
+                /* skip words with string '?', most likely resulting
+                 * from failed transliteration to ASCII
+                 */
+                if (strstr (*w1, unicode_fallback) != NULL)
+                        continue;
+
+                nwords1++; /* count real words, excluding empty string */
+
+                words2 = g_strsplit_set (*w1, "-", -1);
+                /* reset last word if a new non-empty word has been found */
+                if (strlen (*words2) > 0)
+                        last_word = g_string_set_size (last_word, 0);
+
+                for (w2 = words2; *w2; w2++) {
+                        if (strlen (*w2) == 0)
+                                continue;
+
+                        nwords2++;
+
+                        /* part of the first "toplevel" real word */
+                        if (nwords1 == 1) {
+                                item1 = g_string_append (item1, *w2);
+                                first_word = g_string_append (first_word, *w2);
+                        }
+                        else {
+                                item1 = g_string_append_unichar (item1,
+                                                                 g_utf8_get_char (*w2));
+                                item3 = g_string_append_unichar (item3,
+                                                                 g_utf8_get_char (*w2));
+                        }
+
+                        /* not part of the last "toplevel" word */
+                        if (w1 != words1 + len - 1) {
+                                item2 = g_string_append_unichar (item2,
+                                                                 g_utf8_get_char (*w2));
+                                item4 = g_string_append_unichar (item4,
+                                                                 g_utf8_get_char (*w2));
+                        }
+
+                        /* always save current word so that we have it if last one reveals empty */
+                        last_word = g_string_append (last_word, *w2);
+                }
+
+                g_strfreev (words2);
+        }
+        item2 = g_string_append (item2, last_word->str);
+        item3 = g_string_append (item3, first_word->str);
+        item4 = g_string_prepend (item4, last_word->str);
+
+        items = g_hash_table_new (g_str_hash, g_str_equal);
+
+        in_use = is_shortname_used (item1->str);
+        if (nwords2 > 0 && !in_use && !g_ascii_isdigit (item1->str[0])) {
+                gtk_combo_box_append_text (GTK_COMBO_BOX (um->shortname_combo), item1->str);
+                g_hash_table_insert (items, item1->str, item1->str);
+        }
 
-        update_sensitivity (um);
+        /* if there's only one word, would be the same as item1 */
+        if (nwords2 > 1) {
+                /* add other items */
+                in_use = is_shortname_used (item2->str);
+                if (!in_use && !g_ascii_isdigit (item2->str[0]) &&
+                    !g_hash_table_lookup (items, item2->str)) {
+                        gtk_combo_box_append_text (GTK_COMBO_BOX (um->shortname_combo), item2->str);
+                        g_hash_table_insert (items, item2->str, item2->str);
+                }
+
+                in_use = is_shortname_used (item3->str);
+                if (!in_use && !g_ascii_isdigit (item3->str[0]) &&
+                    !g_hash_table_lookup (items, item3->str)) {
+                        gtk_combo_box_append_text (GTK_COMBO_BOX (um->shortname_combo), item3->str);
+                        g_hash_table_insert (items, item3->str, item3->str);
+                }
+
+                in_use = is_shortname_used (item4->str);
+                if (!in_use && !g_ascii_isdigit (item4->str[0]) &&
+                    !g_hash_table_lookup (items, item4->str)) {
+                        gtk_combo_box_append_text (GTK_COMBO_BOX (um->shortname_combo), item4->str);
+                        g_hash_table_insert (items, item4->str, item4->str);
+                }
+
+                /* add the last word */
+                in_use = is_shortname_used (last_word->str);
+                if (!in_use && !g_ascii_isdigit (last_word->str[0]) &&
+                    !g_hash_table_lookup (items, last_word->str)) {
+                        gtk_combo_box_append_text (GTK_COMBO_BOX (um->shortname_combo), last_word->str);
+                        g_hash_table_insert (items, last_word->str, last_word->str);
+                }
+
+                /* ...and the first one */
+                in_use = is_shortname_used (first_word->str);
+                if (!in_use && !g_ascii_isdigit (first_word->str[0]) &&
+                    !g_hash_table_lookup (items, first_word->str)) {
+                        gtk_combo_box_append_text (GTK_COMBO_BOX (um->shortname_combo), first_word->str);
+                        g_hash_table_insert (items, first_word->str, first_word->str);
+                }
+        }
+
+        gtk_combo_box_set_active (GTK_COMBO_BOX (um->shortname_combo), 0);
+        g_hash_table_destroy (items);
+        g_strfreev (words1);
+        g_string_free (first_word, TRUE);
+        g_string_free (last_word, TRUE);
+        g_string_free (item1, TRUE);
+        g_string_free (item2, TRUE);
+        g_string_free (item3, TRUE);
+        g_string_free (item4, TRUE);
 }
 
 UmAccountDialog *
@@ -150,7 +372,7 @@ um_account_dialog_new (void)
         builder = gtk_builder_new ();
 
         filename = UIDIR "/account-dialog.ui";
-        if (!g_file_test (filename, G_FILE_TEST_EXISTS))
+        if (g_file_test (filename, G_FILE_TEST_EXISTS))
                 filename = "../data/account-dialog.ui";
         if (!gtk_builder_add_from_file (builder, filename, &error)) {
                 g_error ("%s", error->message);
@@ -174,10 +396,10 @@ um_account_dialog_new (void)
                           G_CALLBACK (accept_account_dialog), um);
         gtk_widget_grab_default (widget);
 
-        widget = (GtkWidget *) gtk_builder_get_object (builder, "shortname-entry");
-        g_signal_connect (widget, "notify::text",
+        widget = (GtkWidget *) gtk_builder_get_object (builder, "shortname-combo");
+        g_signal_connect (widget, "changed",
                           G_CALLBACK (shortname_changed), um);
-        um->shortname_entry = widget;
+        um->shortname_combo = widget;
 
         widget = (GtkWidget *) gtk_builder_get_object (builder, "name-entry");
         g_signal_connect (widget, "notify::text",
@@ -186,6 +408,9 @@ um_account_dialog_new (void)
 
         um->ok_button = (GtkWidget *) gtk_builder_get_object (builder, "ok-button");
 
+        widget = (GtkWidget *) gtk_builder_get_object (builder, "account-type-combo");
+        um->account_type_combo = widget;
+
         return um;
 }
 
@@ -201,11 +426,14 @@ um_account_dialog_show (UmAccountDialog *um,
                         GtkWindow       *parent)
 {
         gtk_entry_set_text (GTK_ENTRY (um->name_entry), "");
-        gtk_entry_set_text (GTK_ENTRY (um->shortname_entry), "");
+        gtk_entry_set_text (GTK_ENTRY (gtk_bin_get_child (GTK_BIN (um->shortname_combo))), "");
+        gtk_combo_box_set_active (GTK_COMBO_BOX (um->account_type_combo), 0);
 
         gtk_window_set_transient_for (GTK_WINDOW (um->dialog), parent);
         gtk_window_present (GTK_WINDOW (um->dialog));
         gtk_widget_grab_focus (um->name_entry);
+
+        um->valid_name = um->valid_shortname = TRUE;
 }
 
 
diff --git a/src/um-user-manager.c b/src/um-user-manager.c
index 4644d6d..695037c 100644
--- a/src/um-user-manager.c
+++ b/src/um-user-manager.c
@@ -266,12 +266,14 @@ um_user_manager_ref_default (void)
 void
 um_user_manager_create_user (UmUserManager *manager,
                              const char    *user_name,
-                             const char    *real_name)
+                             const char    *real_name,
+                             gint           account_type)
 {
         dbus_g_proxy_call_no_reply (manager->proxy,
                                     "CreateUser",
                                     G_TYPE_STRING, user_name,
                                     G_TYPE_STRING, real_name,
+                                    G_TYPE_INT, account_type,
                                     G_TYPE_INVALID);
 }
 
@@ -313,14 +315,14 @@ UmUser *
 um_user_manager_get_user_by_id (UmUserManager *manager,
                                 uid_t          uid)
 {
-	struct  passwd *pwent;
+        struct  passwd *pwent;
 
-	pwent = getpwuid (uid);
-	if (!pwent) {
-		return NULL;
-	}
+        pwent = getpwuid (uid);
+        if (!pwent) {
+                return NULL;
+        }
 
-	return um_user_manager_get_user (manager, pwent->pw_name);
+        return um_user_manager_get_user (manager, pwent->pw_name);
 }
 
 gboolean
diff --git a/src/um-user-manager.h b/src/um-user-manager.h
index 3c117af..fe8b193 100644
--- a/src/um-user-manager.h
+++ b/src/um-user-manager.h
@@ -75,7 +75,8 @@ UmUser *           um_user_manager_get_user_by_id        (UmUserManager *manager
                                                           uid_t          uid);
 void               um_user_manager_create_user           (UmUserManager *manager,
                                                           const char    *user_name,
-                                                          const char    *real_name);
+                                                          const char    *real_name,
+                                                          gint           account_type);
 void               um_user_manager_delete_user           (UmUserManager *manager,
                                                           UmUser        *user,
                                                           gboolean       remove_files);



[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]