[gnome-shell/wip/menus: 1/2] ShellApp: port to new GDBusActionGroup and GMenuProxy API



commit 5580cfaf634e3c0a8c1ead15d66c0bc2baee3ab5
Author: Giovanni Campagna <gcampagna src gnome org>
Date:   Tue May 10 18:29:52 2011 +0200

    ShellApp: port to new GDBusActionGroup and GMenuProxy API
    
    GDBusActionGroup and GMenuProxy are new objects in GIO 2.32 that
    help with accessing menus and actions of remote applications.
    This patch makes it possible for the shell to associate an
    application with a dbus name and from that a GMenu, that will
    be shown as the application menu.
    
    https://bugzilla.gnome.org/show_bug.cgi?id=621203

 src/shell-app-private.h    |    3 +
 src/shell-app.c            |  252 +++++++++++++++++++++++++++++++++++++++++++-
 src/shell-app.h            |    4 +
 src/shell-window-tracker.c |  167 +++++++++++++++++++++++++++++-
 src/test-gapplication.js   |   99 +++++++++++++++++
 5 files changed, 519 insertions(+), 6 deletions(-)
---
diff --git a/src/shell-app-private.h b/src/shell-app-private.h
index 8c6432e..62853cc 100644
--- a/src/shell-app-private.h
+++ b/src/shell-app-private.h
@@ -27,6 +27,9 @@ void _shell_app_do_match (ShellApp         *app,
                           GSList          **prefix_results,
                           GSList          **substring_results);
 
+void _shell_app_set_dbus_name (ShellApp   *app,
+                               const char *name);
+
 G_END_DECLS
 
 #endif /* __SHELL_APP_PRIVATE_H__ */
diff --git a/src/shell-app.c b/src/shell-app.c
index 54f81a0..4b3ddc8 100644
--- a/src/shell-app.c
+++ b/src/shell-app.c
@@ -37,6 +37,13 @@ typedef struct {
 
   /* Whether or not we need to resort the windows; this is done on demand */
   gboolean window_sort_stale : 1;
+
+  /* See GApplication documentation */
+  guint             name_watcher_id;
+  gchar            *dbus_name;
+  GDBusActionGroup *remote_actions;
+  GMenuProxy       *remote_menu;
+  GCancellable     *dbus_cancellable;
 } ShellAppRunningState;
 
 /**
@@ -72,11 +79,13 @@ struct _ShellApp
   char *casefolded_exec;
 };
 
-G_DEFINE_TYPE (ShellApp, shell_app, G_TYPE_OBJECT);
-
 enum {
   PROP_0,
-  PROP_STATE
+  PROP_STATE,
+  PROP_ID,
+  PROP_DBUS_ID,
+  PROP_ACTION_GROUP,
+  PROP_MENU
 };
 
 enum {
@@ -89,6 +98,8 @@ static guint shell_app_signals[LAST_SIGNAL] = { 0 };
 static void create_running_state (ShellApp *app);
 static void unref_running_state (ShellAppRunningState *state);
 
+G_DEFINE_TYPE (ShellApp, shell_app, G_TYPE_OBJECT)
+
 static void
 shell_app_get_property (GObject    *gobject,
                         guint       prop_id,
@@ -102,6 +113,20 @@ shell_app_get_property (GObject    *gobject,
     case PROP_STATE:
       g_value_set_enum (value, app->state);
       break;
+    case PROP_ID:
+      g_value_set_string (value, shell_app_get_id (app));
+      break;
+    case PROP_DBUS_ID:
+      g_value_set_string (value, shell_app_get_dbus_id (app));
+      break;
+    case PROP_ACTION_GROUP:
+      if (app->running_state)
+        g_value_set_object (value, app->running_state->remote_actions);
+      break;
+    case PROP_MENU:
+      if (app->running_state)
+        g_value_set_object (value, app->running_state->remote_menu);
+      break;
     default:
       G_OBJECT_WARN_INVALID_PROPERTY_ID (gobject, prop_id, pspec);
       break;
@@ -151,6 +176,15 @@ window_backed_app_get_icon (ShellApp *app,
   return actor;
 }
 
+const char *
+shell_app_get_dbus_id (ShellApp *app)
+{
+  if (app->running_state)
+    return app->running_state->dbus_name;
+  else
+    return NULL;
+}
+
 /**
  * shell_app_create_icon_texture:
  *
@@ -948,6 +982,142 @@ _shell_app_remove_window (ShellApp   *app,
   g_signal_emit (app, shell_app_signals[WINDOWS_CHANGED], 0);
 }
 
+static void
+on_action_group_acquired (GObject      *object,
+                          GAsyncResult *result,
+                          gpointer      user_data)
+{
+  ShellApp *self = SHELL_APP (user_data);
+  ShellAppRunningState *state = self->running_state;
+  GError *error = NULL;
+  char *object_path;
+
+  state->remote_actions = g_dbus_action_group_new_finish (result,
+                                                          &error);
+
+  if (error)
+    {
+      if (!g_error_matches (error, G_IO_ERROR, G_IO_ERROR_CANCELLED) &&
+          !g_error_matches (error, G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_METHOD))
+        {
+          g_warning ("Unexpected error while reading application actions: %s", error->message);
+        }
+
+      g_clear_error (&error);
+      g_clear_object (&state->dbus_cancellable);
+
+      if (state->name_watcher_id)
+        {
+          g_bus_unwatch_name (state->name_watcher_id);
+          state->name_watcher_id = 0;
+        }
+
+      g_free (state->dbus_name);
+      state->dbus_name = NULL;
+
+      g_object_unref (self);
+      return;
+    }
+
+  object_path = g_strconcat ("/", state->dbus_name, NULL);
+  g_strdelimit (object_path, ".", '/');
+
+  state->remote_menu = g_menu_proxy_get (g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, NULL),
+                                         state->dbus_name,
+                                         object_path);
+
+  g_object_notify (G_OBJECT (self), "dbus-id");
+  g_object_notify (G_OBJECT (self), "action-group");
+  g_object_notify (G_OBJECT (self), "menu");
+
+  g_object_unref (self);
+  g_free (object_path);
+}
+
+static void
+on_dbus_name_appeared (GDBusConnection *bus,
+                       const gchar     *name,
+                       const gchar     *name_owner,
+                       gpointer         user_data)
+{
+  ShellApp *self = SHELL_APP (user_data);
+  ShellAppRunningState *state = self->running_state;
+  char *object_path;
+
+  g_assert (state != NULL);
+
+  object_path = g_strconcat ("/", name, NULL);
+  g_strdelimit (object_path, ".", '/');
+
+  if (!state->dbus_cancellable)
+    state->dbus_cancellable = g_cancellable_new ();
+
+  g_dbus_action_group_new (bus,
+                           name,
+                           object_path,
+                           G_DBUS_ACTION_GROUP_FLAGS_NONE,
+                           state->dbus_cancellable,
+                           on_action_group_acquired,
+                           g_object_ref (self));
+
+  g_free (object_path);
+}
+
+static void
+on_dbus_name_disappeared (GDBusConnection *bus,
+                          const gchar     *name,
+                          gpointer         user_data)
+{
+  ShellApp *self = SHELL_APP (user_data);
+  ShellAppRunningState *state = self->running_state;
+
+  g_assert (state != NULL);
+
+  if (state->dbus_cancellable)
+    {
+      g_cancellable_cancel (state->dbus_cancellable);
+      g_clear_object (&state->dbus_cancellable);
+    }
+
+  g_clear_object (&state->remote_actions);
+  g_clear_object (&state->remote_menu);
+
+  g_free (state->dbus_name);
+  state->dbus_name = NULL;
+
+  g_bus_unwatch_name (state->name_watcher_id);
+  state->name_watcher_id = 0;
+}
+
+void
+_shell_app_set_dbus_name (ShellApp   *app,
+                          const char *bus_name)
+{
+  g_return_if_fail (app->running_state != NULL);
+
+  if (app->running_state->dbus_name != NULL)
+    {
+      /* already associating with another name
+         (can only happen if you restart the shell in the
+         middle of the session, in which case it will try
+         all names seen on the bus; otherwise, it uses
+         the Hello signal from GApplication and thus knows
+         for sure which name is the right one)
+      */
+      return;
+    }
+
+  app->running_state->dbus_name = g_strdup (bus_name);
+  app->running_state->name_watcher_id = g_bus_watch_name (G_BUS_TYPE_SESSION,
+                                                          bus_name,
+                                                          G_BUS_NAME_WATCHER_FLAGS_NONE,
+                                                          on_dbus_name_appeared,
+                                                          on_dbus_name_disappeared,
+                                                          g_object_ref (app),
+                                                          g_object_unref);
+
+}
+
 /**
  * shell_app_get_pids:
  * @app: a #ShellApp
@@ -1167,13 +1337,28 @@ unref_running_state (ShellAppRunningState *state)
 {
   MetaScreen *screen;
 
+  g_assert (state->refcount > 0);
+
   state->refcount--;
   if (state->refcount > 0)
     return;
 
   screen = shell_global_get_screen (shell_global_get ());
-
   g_signal_handler_disconnect (screen, state->workspace_switch_id);
+
+  if (state->dbus_cancellable)
+    {
+      g_cancellable_cancel (state->dbus_cancellable);
+      g_object_unref (state->dbus_cancellable);
+    }
+
+  g_clear_object (&state->remote_actions);
+  g_clear_object (&state->remote_menu);
+  g_free (state->dbus_name);
+
+  if (state->name_watcher_id)
+    g_bus_unwatch_name (state->name_watcher_id);
+
   g_slice_free (ShellAppRunningState, state);
 }
 
@@ -1349,6 +1534,9 @@ shell_app_dispose (GObject *object)
       while (app->running_state->windows)
         _shell_app_remove_window (app, app->running_state->windows->data);
     }
+  /* We should have been transitioned when we removed all of our windows */
+  g_assert (app->state == SHELL_APP_STATE_STOPPED);
+  g_assert (app->running_state == NULL);
 
   G_OBJECT_CLASS(shell_app_parent_class)->dispose (object);
 }
@@ -1399,4 +1587,60 @@ shell_app_class_init(ShellAppClass *klass)
                                                       SHELL_TYPE_APP_STATE,
                                                       SHELL_APP_STATE_STOPPED,
                                                       G_PARAM_READABLE));
+
+  /**
+   * ShellApp:id:
+   *
+   * The id of this application (a desktop filename, or a special string
+   * like window:0xabcd1234)
+   */
+  g_object_class_install_property (gobject_class,
+                                   PROP_ID,
+                                   g_param_spec_string ("id",
+                                                        "Application id",
+                                                        "The desktop file id of this ShellApp",
+                                                        NULL,
+                                                        G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * ShellApp:dbus-id:
+   *
+   * The DBus well-known name of the application, if one can be associated
+   * to this ShellApp (it means that the application is using GApplication)
+   */
+  g_object_class_install_property (gobject_class,
+                                   PROP_DBUS_ID,
+                                   g_param_spec_string ("dbus-id",
+                                                        "Application DBus Id",
+                                                        "The DBus well-known name of the application",
+                                                        NULL,
+                                                        G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
+  /**
+   * ShellApp:action-group:
+   *
+   * The #GDBusActionGroup associated with this ShellApp, if any. See the
+   * documentation of #GApplication and #GActionGroup for details.
+   */
+  g_object_class_install_property (gobject_class,
+                                   PROP_ACTION_GROUP,
+                                   g_param_spec_object ("action-group",
+                                                        "Application Action Group",
+                                                        "The action group exported by the remote application",
+                                                        G_TYPE_DBUS_ACTION_GROUP,
+                                                        G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+  /**
+   * ShellApp:menu:
+   *
+   * The #GMenuProxy associated with this ShellApp, if any. See the
+   * documentation of #GMenuModel for details.
+   */
+  g_object_class_install_property (gobject_class,
+                                   PROP_MENU,
+                                   g_param_spec_object ("menu",
+                                                        "Application Menu",
+                                                        "The primary menu exported by the remote application",
+                                                        G_TYPE_MENU_PROXY,
+                                                        G_PARAM_READABLE | G_PARAM_STATIC_STRINGS));
+
 }
diff --git a/src/shell-app.h b/src/shell-app.h
index 62022fe..4aaf975 100644
--- a/src/shell-app.h
+++ b/src/shell-app.h
@@ -13,6 +13,7 @@ G_BEGIN_DECLS
 typedef struct _ShellApp ShellApp;
 typedef struct _ShellAppClass ShellAppClass;
 typedef struct _ShellAppPrivate ShellAppPrivate;
+typedef struct _ShellAppAction ShellAppAction;
 
 #define SHELL_TYPE_APP              (shell_app_get_type ())
 #define SHELL_APP(object)           (G_TYPE_CHECK_INSTANCE_CAST ((object), SHELL_TYPE_APP, ShellApp))
@@ -36,9 +37,12 @@ typedef enum {
 GType shell_app_get_type (void) G_GNUC_CONST;
 
 const char *shell_app_get_id (ShellApp *app);
+
 GMenuTreeEntry *shell_app_get_tree_entry (ShellApp *app);
 GDesktopAppInfo *shell_app_get_app_info (ShellApp *app);
 
+const char *shell_app_get_dbus_id (ShellApp *app);
+
 ClutterActor *shell_app_create_icon_texture (ShellApp *app, int size);
 ClutterActor *shell_app_get_faded_icon (ShellApp *app, int size);
 const char *shell_app_get_name (ShellApp *app);
diff --git a/src/shell-window-tracker.c b/src/shell-window-tracker.c
index adb7fb7..cf87cec 100644
--- a/src/shell-window-tracker.c
+++ b/src/shell-window-tracker.c
@@ -49,6 +49,9 @@ struct _ShellWindowTracker
   /* <MetaWindow * window, ShellApp *app> */
   GHashTable *window_to_app;
 
+  /* <int, gchar *> */
+  GHashTable *pid_to_dbus_connection;
+
   /* <int, ShellApp *app> */
   GHashTable *launched_pid_to_app;
 };
@@ -436,6 +439,8 @@ track_window (ShellWindowTracker *self,
               MetaWindow      *window)
 {
   ShellApp *app;
+  GPid pid;
+  gchar *dbus_name;
 
   if (!shell_window_tracker_is_window_interesting (window))
     return;
@@ -451,6 +456,15 @@ track_window (ShellWindowTracker *self,
 
   _shell_app_add_window (app, window);
 
+  /* Try to associate this ShellApp with a GApplication id, if one exists */
+  pid = meta_window_get_pid (window);
+  dbus_name = g_hash_table_lookup (self->pid_to_dbus_connection, GINT_TO_POINTER ((int) pid));
+  if (dbus_name != NULL)
+    {
+      _shell_app_set_dbus_name (app, dbus_name);
+      g_hash_table_remove (self->pid_to_dbus_connection, GINT_TO_POINTER ((int) pid));
+    }
+
   g_signal_emit (self, signals[TRACKED_WINDOWS_CHANGED], 0);
 }
 
@@ -581,13 +595,161 @@ on_startup_sequence_changed (MetaScreen            *screen,
   g_signal_emit (G_OBJECT (self), signals[STARTUP_SEQUENCE_CHANGED], 0, sequence);
 }
 
+typedef struct {
+  ShellWindowTracker *tracker;
+  gchar *bus_name;
+} LookupAppDBusData;
+
+static void
+on_get_connection_unix_pid_reply (GObject      *connection,
+                                  GAsyncResult *result,
+                                  gpointer      user_data)
+{
+  LookupAppDBusData *data = user_data;
+  GError *error = NULL;
+  GVariant *reply;
+  guint32 pid;
+  ShellApp *app;
+
+  reply = g_dbus_connection_call_finish (G_DBUS_CONNECTION (connection), result, &error);
+  if (!reply)
+    {
+      if (!g_error_matches (error, G_DBUS_ERROR, G_DBUS_ERROR_NAME_HAS_NO_OWNER))
+          g_warning ("%s\n", error->message);
+
+      g_clear_error (&error);
+      goto out;
+    }
+  if (!g_variant_is_of_type (reply, G_VARIANT_TYPE ("(u)")))
+    {
+      g_variant_unref (reply);
+      goto out;
+    }
+  g_variant_get (reply, "(u)", &pid);
+  g_variant_unref (reply);
+
+  app = shell_window_tracker_get_app_from_pid (data->tracker, (int)pid);
+  if (app)
+    _shell_app_set_dbus_name (app, data->bus_name);
+  else
+    {
+      g_hash_table_insert (data->tracker->pid_to_dbus_connection,
+                           GINT_TO_POINTER ((int) pid),
+                           data->bus_name);
+      data->bus_name = NULL;
+    }
+
+ out:
+  g_object_unref (data->tracker);
+  g_free (data->bus_name);
+  g_slice_free (LookupAppDBusData, data);
+}
+
+static void
+lookup_application_from_name (ShellWindowTracker *self,
+                              GDBusConnection    *connection,
+                              const gchar        *bus_name)
+{
+  LookupAppDBusData *data;
+
+  data = g_slice_new0 (LookupAppDBusData);
+  data->tracker = g_object_ref (self);
+  data->bus_name = g_strdup (bus_name);
+
+  /*
+   * TODO: Add something to GtkApplication so it definitely knows the .desktop file.
+   */
+  g_dbus_connection_call (connection,
+                          "org.freedesktop.DBus",
+                          "/org/freedesktop/DBus",
+                          "org.freedesktop.DBus",
+                          "GetConnectionUnixProcessID",
+                          g_variant_new ("(s)", bus_name),
+                          NULL, 0, -1, NULL, on_get_connection_unix_pid_reply, data);
+}
+
+static void
+on_application_signal (GDBusConnection  *connection,
+                       const gchar      *sender_name,
+                       const gchar      *object_path,
+                       const gchar      *interface_name,
+                       const gchar      *signal_name,
+                       GVariant         *parameters,
+                       gpointer          user_data)
+{
+  ShellWindowTracker *tracker = SHELL_WINDOW_TRACKER (user_data);
+  gchar *bus_name = NULL;
+
+  g_variant_get (parameters, "(&s)", &bus_name);
+  lookup_application_from_name (tracker, connection, bus_name);
+
+  g_variant_unref (parameters);
+}
+
+static void
+on_list_names_end (GObject      *object,
+                   GAsyncResult *result,
+                   gpointer      user_data)
+{
+  GDBusConnection *connection = G_DBUS_CONNECTION (object);
+  ShellWindowTracker *self = SHELL_WINDOW_TRACKER (user_data);
+  GError *error = NULL;
+  GVariantIter iter;
+  gchar *bus_name = NULL;
+
+  GVariant *res = g_dbus_connection_call_finish (connection, result, &error);
+
+  if (!res)
+    {
+      g_warning ("ListNames failed: %s", error->message);
+      g_error_free (error);
+      return;
+    }
+
+  g_variant_iter_init (&iter, g_variant_get_child_value (res, 0));
+  while (g_variant_iter_loop (&iter, "s", &bus_name))
+    {
+      if (bus_name[0] == ':')
+        {
+          /* unique name, uninteresting */
+          continue;
+        }
+
+      lookup_application_from_name (self, connection, bus_name);
+    }
+
+  g_variant_unref (res);
+}
+
 static void
 shell_window_tracker_init (ShellWindowTracker *self)
 {
   MetaScreen *screen;
 
-  self->window_to_app = g_hash_table_new_full (g_direct_hash, g_direct_equal,
-                                               NULL, (GDestroyNotify) g_object_unref);
+  g_dbus_connection_signal_subscribe (g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, NULL),
+                                      NULL,
+                                      "org.gtk.Application",
+                                      "Hello",
+                                      NULL,
+                                      NULL,
+                                      G_DBUS_SIGNAL_FLAGS_NONE,
+                                      on_application_signal,
+                                      self, NULL);
+
+  g_dbus_connection_call (g_bus_get_sync (G_BUS_TYPE_SESSION, NULL, NULL),
+                          "org.freedesktop.DBus",
+                          "/org/freedesktop/DBus",
+                          "org.freedesktop.DBus",
+                          "ListNames",
+                          NULL, /* parameters */
+                          G_VARIANT_TYPE ("(as)"),
+                          G_DBUS_CALL_FLAGS_NONE,
+                          -1, /* timeout */
+                          NULL, /* cancellable */
+                          on_list_names_end, self);
+
+  self->window_to_app = g_hash_table_new_full (NULL, NULL, NULL, g_object_unref);
+  self->pid_to_dbus_connection = g_hash_table_new_full (NULL, NULL, NULL, g_free);
 
   self->launched_pid_to_app = g_hash_table_new_full (NULL, NULL, NULL, (GDestroyNotify) g_object_unref);
 
@@ -607,6 +769,7 @@ shell_window_tracker_finalize (GObject *object)
 
   g_hash_table_destroy (self->window_to_app);
   g_hash_table_destroy (self->launched_pid_to_app);
+  g_hash_table_destroy (self->pid_to_dbus_connection);
 
   G_OBJECT_CLASS (shell_window_tracker_parent_class)->finalize(object);
 }
diff --git a/src/test-gapplication.js b/src/test-gapplication.js
new file mode 100755
index 0000000..9872889
--- /dev/null
+++ b/src/test-gapplication.js
@@ -0,0 +1,99 @@
+#!/usr/bin/env gjs
+
+const Gdk = imports.gi.Gdk;
+const Gio = imports.gi.Gio;
+const GLib = imports.gi.GLib;
+const Gtk = imports.gi.Gtk;
+
+function do_action(action, parameter) {
+    print ("Action '" + action.name + "' invoked");
+}
+
+function do_action_param(action, parameter) {
+    print ("Action '" + action.name + "' invoked with parameter " + parameter.print(true));
+}
+
+function do_action_state_change(action) {
+    print ("Action '" + action.name + "' has now state '" + action.state.deep_unpack() + "'");
+}
+
+function main() {
+    Gtk.init(null, null);
+
+    let app = new Gtk.Application({ application_id: 'org.gnome.Shell.GtkApplicationTest' });
+    app.connect('activate', function() {
+	print ("Activated");
+    });
+
+    let group = new Gio.SimpleActionGroup();
+
+    let action = Gio.SimpleAction.new('one', null);
+    action.connect('activate', do_action);
+    group.insert(action);
+
+    let action = Gio.SimpleAction.new('two', null);
+    action.connect('activate', do_action);
+    group.insert(action);
+
+    let action = Gio.SimpleAction.new_stateful('toggle', null, GLib.Variant.new('b', false));
+    action.connect('activate', do_action);
+    action.connect('notify::state', do_action_state_change);
+    group.insert(action);
+
+    let action = Gio.SimpleAction.new('disable', null);
+    action.set_enabled(false);
+    action.connect('activate', do_action);
+    group.insert(action);
+
+    let action = Gio.SimpleAction.new('parameter-int', GLib.VariantType.new('u'));
+    action.connect('activate', do_action_param);
+    group.insert(action);
+
+    let action = Gio.SimpleAction.new('parameter-string', GLib.VariantType.new('s'));
+    action.connect('activate', do_action_param);
+    group.insert(action);
+
+    app.action_group = group;
+
+    let menu = new Gio.Menu();
+    menu.append('An action', 'one');
+
+    let section = new Gio.Menu();
+    section.append('Another action', 'two');
+    section.append('Same as above', 'two');
+    menu.append_section(null, section);
+
+    // another section, to check separators
+    section = new Gio.Menu();
+    section.append('Checkbox', 'toggle');
+    section.append('Disabled', 'disable');
+    menu.append_section(null, section);
+
+    // empty sections or submenus should be invisible
+    menu.append_section('Empty section', new Gio.Menu());
+    menu.append_submenu('Empty submenu', new Gio.Menu());
+
+    let submenu = new Gio.Menu();
+    submenu.append('Open c:\\', 'parameter-string::c:\\');
+    submenu.append('Open /home', 'parameter-string::/home');
+    menu.append_submenu('Recent files', submenu);
+
+    let item = Gio.MenuItem.new('Say 42', null);
+    item.set_action_and_target_value('parameter-int', GLib.Variant.new('u', 42));
+    menu.append_item(item);
+
+    let item = Gio.MenuItem.new('Say 43', null);
+    item.set_action_and_target_value('parameter-int', GLib.Variant.new('u', 43));
+    menu.append_item(item);
+
+    app.menu = menu;
+
+    app.connect('startup', function(app) {
+	let window = new Gtk.Window({ title: "Test Application", application: app });
+	window.present();
+    });
+
+    app.run(null);
+}
+
+main();



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