diff --git a/css/context.css b/css/context.css
new file mode 100644
index 0000000000000000000000000000000000000000..a531ef656f16992ab3bd03021ebaf23eb9e5a68e
--- /dev/null
+++ b/css/context.css
@@ -0,0 +1,11 @@
+/* $Id$ */
+.panels-context-holder .panels-context-title {
+  float: left;
+  width: 49%;
+  font-style: italic;
+}
+
+.panels-context-holder .panels-context-content {
+  float: right;
+  width: 49%;
+}
diff --git a/css/modal.css b/css/modal.css
new file mode 100644
index 0000000000000000000000000000000000000000..508b044e98416e174f17e7605a87850efcb3ae90
--- /dev/null
+++ b/css/modal.css
@@ -0,0 +1,98 @@
+/* $Id$ */
+div.ctools-modal-content {
+  background: #fff;
+  color: #000;
+  padding: 0;
+  margin: 2px;
+  border: 1px solid #000;
+  width: 600px;
+  text-align: left;
+} 
+
+div.ctools-modal-content .modal-title {
+  font-size: 120%;
+  font-weight: bold;
+  color: white;
+  overflow: hidden;
+  white-space: nowrap;
+}
+
+div.ctools-modal-content .modal-header {
+  background-color: #2385c2;
+  padding: 0 .25em 0 1em;
+}
+
+div.ctools-modal-content .modal-header a {
+  color: white;
+  float: right;
+}
+
+div.ctools-modal-content .modal-content {
+  padding: 0 1em;
+  overflow: auto;
+  width: 575px;
+  height: 400px;
+}
+
+div.ctools-modal-content .modal-form {
+}
+
+div.ctools-modal-content .form-checkboxes .form-item {
+  float: left;
+  width: 24%;
+}
+
+div.ctools-modal-content a.close {
+  color: white;
+}
+
+div.ctools-modal-content a.close:hover {
+  text-decoration: none;
+}
+
+div.ctools-modal-content a.close img {
+  position: relative;
+  top: 1px;
+}
+
+div.ctools-modal-content .modal-content .modal-throbber-wrapper {
+  width: 575px;
+  height: 400px;
+  text-align: center;
+}
+
+div.ctools-modal-content .modal-content .modal-throbber-wrapper img {
+  margin-top: 160px;
+}
+
+/** modal forms CSS **/
+div.ctools-modal-content .form-item label {
+  width: 8em;
+  float: left;
+}
+
+div.ctools-modal-content .form-item label.option {
+  width: auto;
+  float: none;
+}
+
+div.ctools-modal-content .form-item .description {
+  clear: left;
+}
+
+div.ctools-modal-content .form-item .description .tips {
+  margin-left: 2em;
+}
+
+div.ctools-modal-content .no-float .form-item * {
+  float: none;
+}
+
+div.ctools-modal-content .modal-form .no-float label  {
+  width: auto;
+}
+
+div.ctools-modal-content .modal-form fieldset,
+div.ctools-modal-content .modal-form .form-checkboxes {
+  clear: left;
+}
diff --git a/ctools.module b/ctools.module
index 7cccd2de4a7137b70b853478d9015c3ccecb4919..f0ce9a313067988a99093851f1173810b4882118 100644
--- a/ctools.module
+++ b/ctools.module
@@ -1,6 +1,14 @@
 <?php
 // $Id$
 
+/**
+ * @file
+ * CTools primary module file.
+ *
+ * Most of the CTools tools are in their own .inc files. This contains
+ * nothing more than a few convenience functions and some hooks that
+ * must be implemented in the module file.
+ */
 /**
  * Include ctools .inc files as necessary.
  */
@@ -14,54 +22,71 @@ function ctools_include($file) {
 }
 
 /**
- * Implementation of hook_theme().
+ * Provide the proper path to a CTools image
  */
-function ctools_theme() {
-  return array(
-    'ctools_collapsible' => array(
-      'arguments' => array('handle' => NULL, 'content' => NULL, 'collapsed' => FALSE),
-      'file' => 'includes/collapsible-div.inc',
-    ),
-  );
+function ctools_image_path($image) {
+  return drupal_get_path('module', 'ctools') . '/images/' . $image;
 }
 
 /**
- * Fetch a group of plugins by name.
- *
- * @param $module
- *   The name of the module that utilizes this plugin system. It will be
- *   used to call hook_ctools_plugin_$plugin() to get more data about the plugin.
- * @param $type
- *   The type identifier of the plugin.
- * @param $id
- *   If specified, return only information about plugin with this identifier.
- *   The system will do its utmost to load only plugins with this id.
- *
- * @return
- *   An array of information arrays about the plugins received. The contents
- *   of the array are specific to the plugin.
+ * Include views .css files.
  */
-function ctools_get_plugins($module, $type, $id = NULL) {
-  ctools_include('plugins');
-  return _ctools_get_plugins($module, $type, $id);
+function ctools_add_css($file) {
+  drupal_add_css(drupal_get_path('module', 'ctools') . "/css/$file.css");
 }
 
 /**
- * Provide a form for displaying an export.
+ * Include views .js files.
+ */
+function ctools_add_js($file) {
+  drupal_add_js(drupal_get_path('module', 'ctools') . "/js/$file.js");
+}
+
+/**
+ * Provide a hook passthrough to included files.
  *
- * This is a simple form that should be invoked like this:
- * @code
- *   $output = drupal_get_form('ctools_export_form', $code, $object_title);
- * @endcode
+ * To organize things neatly, each CTools tool gets its own toolname.$type.inc
+ * file. If it exists, it's loaded and ctools_$tool_$type() is executed.
+ * To save time we pass the $items array in so we don't need to do array
+ * addition. It modifies the array by reference and doesn't need to return it.
+ */
+function _ctools_passthrough(&$items, $type = 'theme') {
+  $files = drupal_system_listing('.' . $type . '.inc$', drupal_get_path('module', 'ctools') . '/includes', 'name', 0);
+  foreach ($files as $file) {
+    require_once './' . $file->filename;
+    list($tool) = explode('.', $file->name, 2);
+
+    $function = 'ctools_' . $tool . '_' . $type;
+    if (function_exists($function)) {
+      $function($items);
+    }
+  }
+}
+
+/**
+ * Implementation of hook_theme().
+ */
+function ctools_theme() {
+  $items = array();
+  _ctools_passthrough($items, 'theme');
+  return $items;
+}
+
+/**
+ * Implementation of hook_menu().
  */
-function ctools_export_form(&$form_state, $code, $title = '') {
-  $lines = substr_count($code, "\n");
-  $form['code'] = array(
-    '#type' => 'textarea',
-    '#title' => $title,
-    '#default_value' => $code,
-    '#rows' => $lines,
-  );
+function ctools_menu() {
+  $items = array();
+  _ctools_passthrough($items, 'menu');
+  return $items;
+}
 
-  return $form;
-}
\ No newline at end of file
+/**
+ * Implementation of hook_ctools_plugin_dierctory() to let the system know
+ * we implement task and task_handler plugins.
+ */
+function ctools_ctools_plugin_directory($module, $plugin) {
+  if ($module == 'ctools') {
+    return 'plugins/' . $plugin;
+  }
+}
diff --git a/delegator/delegator.admin.inc b/delegator/delegator.admin.inc
index e900879b1b0d856dd295df66102c3c8d8d7cb963..5cc2d4d8f60579894a4e3e663e32b1a8eb655ffe 100644
--- a/delegator/delegator.admin.inc
+++ b/delegator/delegator.admin.inc
@@ -874,6 +874,7 @@ function delegator_administer_task_handler_export($task_name, $name) {
   $title = delegator_get_handler_title($plugin, $handler, $task, $subtask_id);
   drupal_set_title(t('Export task handler "@title"', array('@title' => $title)));
 
+  ctools_include('export');
   return drupal_get_form('ctools_export_form', delegator_export_task_handler($handler), $title);
 }
 
diff --git a/delegator/delegator.module b/delegator/delegator.module
index 938d6d8f02eb2682a30275e71c9896879c97e291..13ab7c7c786ca3be3866bb9981e67c1957cf6b62 100644
--- a/delegator/delegator.module
+++ b/delegator/delegator.module
@@ -333,6 +333,7 @@ function delegator_update_task_handler_weight($handler, $weight) {
  * Shortcut function to get task plugins.
  */
 function delegator_get_tasks() {
+  ctools_include('plugins');
   return ctools_get_plugins('delegator', 'tasks');
 }
 
@@ -340,6 +341,7 @@ function delegator_get_tasks() {
  * Shortcut function to get a task plugin.
  */
 function delegator_get_task($id) {
+  ctools_include('plugins');
   return ctools_get_plugins('delegator', 'tasks', $id);
 }
 
@@ -347,6 +349,7 @@ function delegator_get_task($id) {
  * Shortcut function to get task handler plugins.
  */
 function delegator_get_task_handlers() {
+  ctools_include('plugins');
   return ctools_get_plugins('delegator', 'task_handlers');
 }
 
@@ -354,6 +357,7 @@ function delegator_get_task_handlers() {
  * Shortcut function to get a task handler plugin.
  */
 function delegator_get_task_handler($id) {
+  ctools_include('plugins');
   return ctools_get_plugins('delegator', 'task_handlers', $id);
 }
 
@@ -407,8 +411,8 @@ function delegator_get_handler_summary($plugin, $handler, $task, $subtask_id) {
  * Implementation of hook_ctools_plugin_dierctory() to let the system know
  * we implement task and task_handler plugins.
  */
-function delegator_ctools_plugin_directory($plugin) {
-  if ($plugin == 'tasks' || $plugin == 'task_handlers') {
+function delegator_ctools_plugin_directory($module, $plugin) {
+  if ($module == 'delegator') {
     return 'plugins/' . $plugin;
   }
 }
diff --git a/help/context-arguments.html b/help/context-arguments.html
new file mode 100644
index 0000000000000000000000000000000000000000..a5ff5ebd8fcb1673be2d93b65cab7a4c066aa339
--- /dev/null
+++ b/help/context-arguments.html
@@ -0,0 +1,12 @@
+<!-- $Id$ -->
+
+Arguments create a context from external input, which is assumed to be a
+string as though it came from a URL element.
+
+    'title' => title
+    'description' => Description
+    'keyword' => Default keyword for the context
+    'context' => Callback to create the context. Params: $arg = NULL, $conf = NULL, $empty = FALSE
+    'settings form' => params: $conf
+    'settings form validate' => params: $form, $form_state
+    'settings form submit' => params: $form, $form_state
diff --git a/help/context-context.html b/help/context-context.html
new file mode 100644
index 0000000000000000000000000000000000000000..8bfcf002258424104d5b9f35401cd88821172a25
--- /dev/null
+++ b/help/context-context.html
@@ -0,0 +1,13 @@
+<!-- $Id$ -->
+
+Context plugin data:
+
+    'title' => Visible title
+    'description' => Description of context
+    'context' => Callback to create a context. Params: $empty, $data = NULL, $conf = FALSE
+    'settings form' => Callback to show a context setting form. Params: ($conf, $external = FALSE)
+    'settings form validate' => params: ($form, &$form_values, &$form_state)
+    'settings form submit' => params: 'ctools_context_node_settings_form_submit',
+    'keyword' => The default keyword to use.
+    'context name' => The unique identifier for this context for use by required context checks.
+    'no ui' => if TRUE this context cannot be selected.
diff --git a/help/context-relationships.html b/help/context-relationships.html
new file mode 100644
index 0000000000000000000000000000000000000000..79e0841aa876017605360e35810d90906ac835c1
--- /dev/null
+++ b/help/context-relationships.html
@@ -0,0 +1,11 @@
+<!-- $Id$ -->
+
+    'title' => The title to display.
+    'description' => Description to display.
+    'keyword' => Default keyword for the context created by this relationship.
+    'required context' => One or more ctools_context_required/optional objects 
+      describing the context input.
+      new panels_required_context(t('Node'), 'node'),
+    'context' => The callback to create the context. Params: ($context = NULL, $conf)
+    'settings form' => Settings form. Params: $conf
+    'settings form validate' => Validate. 
diff --git a/help/context.html b/help/context.html
new file mode 100644
index 0000000000000000000000000000000000000000..0130693f768285491d06ac66c0686a902c1531a7
--- /dev/null
+++ b/help/context.html
@@ -0,0 +1 @@
+<!-- $Id$ -->
diff --git a/help/delegator.help.ini b/help/delegator.help.ini
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..0130693f768285491d06ac66c0686a902c1531a7 100644
--- a/help/delegator.help.ini
+++ b/help/delegator.help.ini
@@ -0,0 +1 @@
+<!-- $Id$ -->
diff --git a/help/plugins-creating.html b/help/plugins-creating.html
index ac4db84f8c5b816a746be1b31e2e85d5f701c46a..ca0204421aed89654709d6fe751c13d8239690d8 100644
--- a/help/plugins-creating.html
+++ b/help/plugins-creating.html
@@ -27,6 +27,14 @@ Automatically filled in data:
   path
   file
 
+Use of hook_ctools_plugin_TYPE to define info about your plugin.
+  $info += array(
+    'module' => $module,
+    'type' => $type,
+    'cache' => FALSE,
+    'defaults' => array(),
+    'hook' => $module . '_' . $type,
+  );
 
 General feature for callbacks:
   either 'function_name' or
@@ -36,4 +44,4 @@ General feature for callbacks:
     'function' => 'function_name'
   ),
 
-  Using ctools_plugin_get_function() of ctools_plugin_load_function() will take advantage.
\ No newline at end of file
+  Using ctools_plugin_get_function() or ctools_plugin_load_function() will take advantage.
\ No newline at end of file
diff --git a/help/plugins-implementing.html b/help/plugins-implementing.html
index 065bb2b78db93c5f4acca8703aff70c42ddf2c5e..5ced6e5a05cca74bdd576e0d42f3b8f4f6e0aea0 100644
--- a/help/plugins-implementing.html
+++ b/help/plugins-implementing.html
@@ -1,9 +1,11 @@
 <!-- $Id$ -->
 
-Implementing hook_ctools_plugin_directory()
+Implementing hook_ctools_plugin_directory($module, $plugin)
 
 Creating a pluginname.inc file
 
+Naming the function properly within this file:
+
 returning 1 or more plugins per file:
 
 Best practices
diff --git a/images/icon-close-window.png b/images/icon-close-window.png
new file mode 100644
index 0000000000000000000000000000000000000000..5f0cf695b0cac487efecfd7eae66e7492a2ba306
Binary files /dev/null and b/images/icon-close-window.png differ
diff --git a/images/icon-configure.png b/images/icon-configure.png
new file mode 100644
index 0000000000000000000000000000000000000000..e23d67cc04b84880d0437e23ffcba837d2dc4121
Binary files /dev/null and b/images/icon-configure.png differ
diff --git a/images/icon-delete.png b/images/icon-delete.png
new file mode 100644
index 0000000000000000000000000000000000000000..5f0cf695b0cac487efecfd7eae66e7492a2ba306
Binary files /dev/null and b/images/icon-delete.png differ
diff --git a/images/throbber.gif b/images/throbber.gif
new file mode 100644
index 0000000000000000000000000000000000000000..8a084b8447d506e9b655ad52405cbf7a73034550
Binary files /dev/null and b/images/throbber.gif differ
diff --git a/includes/ajax.inc b/includes/ajax.inc
new file mode 100644
index 0000000000000000000000000000000000000000..6bc383677549158d764c2cd7c36dd0301da8ebed
--- /dev/null
+++ b/includes/ajax.inc
@@ -0,0 +1,178 @@
+<?php
+// $Id$
+/**
+ * @file
+ * Utilize the CTools AJAX responder.
+ *
+ * The AJAX responder is a javascript tool to make it very easy to do complicated
+ * operations as a response to AJAX requests. When links are attached to the ajax
+ * responder, the server sends back a packet of JSON data; this packet is an
+ * array of commands to carry out.
+ *
+ * The command names correlate to functions in the responder space, making it
+ * relatively easy for applications to provide their own commands to do whatever
+ * spiffy functionality is necessary.
+ *
+ * Each command is an object. $object->command is the type of command and
+ * will be used to find the function (it will correllate directly to
+ * a function in the Drupal.CTools.AJAX.Command space). The object can
+ * contain any other data that the command needs to process.
+ *
+ * Built in commands:
+ * - replace
+ *   - selector: The CSS selector. This can be any selector jquery uses in $().
+ *   - data: The data to use with the jquery replace() function.
+ *
+ * - append
+ *   - selector: The CSS selector. This can be any selector jquery uses in $().
+ *   - data: The data to use with the jquery append() function.
+ *
+ * - addClass
+ *   - selector: The CSS selector. This can be any selector jquery uses in $().
+ *   - data: The class to add.
+ *
+ * - alert
+ *   - title: The title of the alert.
+ *   - data: The data in the alert.
+ */
+
+/**
+ * Render an image as a button link. This will automatically apply an AJAX class
+ * to the link and add the appropriate javascript to make this happen.
+ *
+ * @param $image
+ *   The path to an image to use that will be sent to theme('image') for rendering.
+ * @param $dest
+ *   The destination of the link.
+ * @param $alt
+ *   The alt text of the link.
+ * @param $class
+ *   Any class to apply to the link. @todo this should be a options array.
+ */
+function ctools_ajax_image_button($image, $dest, $alt, $class = '') {
+  return ctools_ajax_text_button(theme('image', $image), $dest, $alt, $class);
+}
+
+/**
+ * Render text as a link. This will automatically apply an AJAX class
+ * to the link and add the appropriate javascript to make this happen.
+ *
+ * Note: 'html' => true so be sure any text is vetted! Chances are these kinds of buttons will
+ * not use user input so this is a very minor concern.
+ *
+ * @param $image
+ *   The path to an image to use that will be sent to theme('image') for rendering.
+ * @param $dest
+ *   The destination of the link.
+ * @param $alt
+ *   The alt text of the link.
+ * @param $class
+ *   Any class to apply to the link. @todo this should be a options array.
+ * @param $type
+ *   A type to use, in case a different behavior should be attached. Defaults
+ *   to ctools-use-ajax.
+ */
+function ctools_ajax_text_button($text, $dest, $alt, $class = '', $type = 'ctools-use-ajax') {
+  return l($text, $dest, array('html' => TRUE, 'attributes' => array('class' => "$type $class", 'title' => $alt, 'alt' => $alt)));
+}
+
+/**
+ * Create a command array for the error case.
+ */
+function ctools_ajax_command_error($error = '') {
+  return array(
+    'command' => 'alert',
+    'title' => t('Error'),
+    'text' => $error ? $error : t('Server reports invalid input error.'),
+  );
+}
+
+/**
+ * Create a replace command for the AJAX responder.
+ *
+ * The replace command will replace a portion of the current document
+ * with the specified HTML.
+ *
+ * @param $selector
+ *   The CSS selector. This can be any selector jquery uses in $().
+ * @param $html
+ *   The data to use with the jquery replace() function.
+ */
+function ctools_ajax_command_replace($selector, $html) {
+  return array(
+    'command' => 'replace',
+    'selector' => $selector,
+    'data' => $html,
+  );
+}
+
+/**
+ * Create an append command for the AJAX responder.
+ *
+ * This will append the HTML to the specified selector.
+ *
+ * @param $selector
+ *   The CSS selector. This can be any selector jquery uses in $().
+ * @param $html
+ *   The data to use with the jquery append() function.
+ */
+function ctools_ajax_command_append($selector, $html) {
+  return array(
+    'command' => 'append',
+    'selector' => $selector,
+    'data' => $html,
+  );
+}
+
+/**
+ * Create a changed command for the AJAX responder.
+ *
+ * This will mark an item as 'changed'.
+ *
+ * @param $selector
+ *   The CSS selector. This can be any selector jquery uses in $().
+ * @param $star
+ *   An optional CSS selector which must be inside $selector. If specified,
+ *   a star will be appended.
+ */
+function ctools_ajax_command_changed($selector, $star = '') {
+  return array(
+    'command' => 'changed',
+    'selector' => $selector,
+    'star' => $star,
+  );
+}
+
+/**
+ * Force a table to be restriped.
+ *
+ * This is usually used after a table has been modifed by a replace or append
+ * command.
+ *
+ * @param $selector
+ *   The CSS selector. This can be any selector jquery uses in $().
+ */
+function ctools_ajax_command_restripe($selector) {
+  return array(
+    'command' => 'restripe',
+    'selector' => $selector,
+  );
+}
+
+/**
+ * Render a commands array into JSON and immediately hand this back
+ * to the AJAX requester.
+ */
+function ctools_ajax_render($commands = array()) {
+  drupal_json($commands);
+  exit;
+}
+
+/**
+ * Send an error response back via AJAX and immediately exit.
+ */
+function ctools_ajax_render_error($error = '') {
+  $commands = array();
+  $commands[] = ctools_ajax_command_error($error);
+  ctools_ajax_render($commands);
+}
diff --git a/includes/collapsible.theme.inc b/includes/collapsible.theme.inc
new file mode 100644
index 0000000000000000000000000000000000000000..6d35cbe7c031d1e64b203878f3288eee0ce0a076
--- /dev/null
+++ b/includes/collapsible.theme.inc
@@ -0,0 +1,13 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Theme registry for collapsible-div tool.
+ */
+function ctools_collapsible_theme($items) {
+  $items['ctools_collapsible'] = array(
+    'arguments' => array('handle' => NULL, 'content' => NULL, 'collapsed' => FALSE),
+    'file' => 'includes/collapsible-div.inc',
+  );
+}
diff --git a/includes/context-admin.inc b/includes/context-admin.inc
new file mode 100644
index 0000000000000000000000000000000000000000..e1256ca08cc2cea4664797bbf2f0b2b2b9437569
--- /dev/null
+++ b/includes/context-admin.inc
@@ -0,0 +1,944 @@
+<?php
+// $Id$
+
+/**
+ * @file includes/common-context.inc
+ * Provide API for adding contexts for modules that embed displays.
+ */
+
+/**
+ * Provide a list of the ways contexts can be embedded.
+ *
+ * This provides a full list of context types that the tool understands
+ * and can let modules utilize.
+ */
+function ctools_context_info($type = NULL) {
+  static $info = NULL;
+
+  // static doesn't work with functions like t().
+  if (empty($info)) {
+    $info = array(
+      'argument' => array(
+        'title' => t('Arguments'),
+        'singular title' => t('argument'),
+        'description' => '', /* t("Arguments are parsed from the URL and translated into contexts that may be added to the display via the 'content' tab. These arguments are parsed in the order received, and you may use % in your URL to hold the place of an object; the rest of the arguments will come after the URL. For example, if the URL is node/%/panel and your user visits node/1/panel/foo, the first argument will be 1, and the second argument will be foo."), */
+        'add button' => t('Add argument'),
+        'context function' => 'ctools_get_argument',
+        'form id' => 'ctools_edit_argument_form',
+        'key' => 'arguments', // the key that data will be stored on an object, eg $panel_page
+        'sortable' => TRUE,
+      ),
+      'relationship' => array(
+        'title' => t('Relationships'),
+        'singular title' => t('relationship'),
+        'description' => '', /* t('Relationships are contexts that are created from already existing contexts; the add relationship button will only appear once there is another context available. Relationships can load objects based upon how they are related to each other; for example, the author of a node, or a taxonomy term attached to a node, or the vocabulary of a taxonomy term.'), */
+        'add button' => t('Add relationship'),
+        'context function' => 'ctools_get_relationship',
+        'form id' => 'ctools_edit_relationship_form',
+        'key' => 'relationships',
+        'sortable' => FALSE,
+      ),
+      'context' => array(
+        'title' => t('Contexts'),
+        'singular title' => t('context'),
+        'description' => '', /* t('Contexts are embedded directly into the panel; you generally must select an object in the panel. For example, you could select node 5, or the term "animals" or the user "administrator"'), */
+        'add button' => t('Add context'),
+        'context function' => 'ctools_get_context',
+        'form id' => 'ctools_edit_context_form',
+        'key' => 'contexts',
+        'sortable' => FALSE,
+      ),
+      'requiredcontext' => array(
+        'title' => t('Required contexts'),
+        'singular title' => t('required context'),
+        'description' => '', /* t('Required contexts are passed in from some external source, such as a containing panel. If a mini panel has required contexts, it can only appear when that context is available, and therefore will not show up as a standard Drupal block.'), */
+        'add button' => t('Add required context'),
+        'context function' => 'ctools_get_context',
+        'form id' => 'ctools_edit_requiredcontext_form',
+        'key' => 'requiredcontexts',
+        'sortable' => TRUE,
+      ),
+    );
+  }
+
+  if ($type === NULL) {
+    return $info;
+  }
+
+  return $info[$type];
+}
+
+/**
+ * Get the data belonging to a particular context.
+ */
+function ctools_context_data($type, $name) {
+  $info = ctools_context_info($type);
+  if (function_exists($info['context function'])) {
+    return $info['context function']($name);
+  }
+}
+
+/**
+ * Add the argument table plus gadget plus javascript to the form.
+ */
+function ctools_context_add_argument_form($module, &$form, &$form_state, &$form_location, $object) {
+  $form_location = array(
+    '#prefix' => '<div id="ctools-arguments-table">',
+    '#suffix' => '</div>',
+    '#theme' => 'ctools_context_item_form',
+    '#object_name' => $object->name,
+    '#ctools_context_type' => 'argument',
+    '#ctools_context_module' => $module,
+  );
+
+  $args = ctools_get_arguments();
+  $choices = array();
+  foreach ($args as $name => $arg) {
+    $choices[$name] = $arg['title'];
+  }
+
+  asort($choices);
+
+  if (!empty($choices) || !empty($object->arguments)) {
+    ctools_context_add_item_table('argument', $form_location, $choices, $object->arguments);
+  }
+}
+
+function ctools_context_add_context_form($module, &$form, &$form_state, &$form_location, $object) {
+  $form_location = array(
+    '#prefix' => '<div id="ctools-contexts-table">',
+    '#suffix' => '</div>',
+    '#theme' => 'ctools_context_item_form',
+    '#object_name' => $object->name,
+    '#ctools_context_type' => 'context',
+    '#ctools_context_module' => $module,
+  );
+
+  // Store the order the choices are in so javascript can manipulate it.
+  $form_location['markup'] = array(
+    '#value' => '&nbsp;',
+  );
+
+  $choices = array();
+  foreach (ctools_get_contexts() as $name => $arg) {
+    if (empty($arg['no ui'])) {
+      $choices[$name] = $arg['title'];
+    }
+  }
+
+  asort($choices);
+
+  if (!empty($choices) || !empty($object->contexts)) {
+    ctools_context_add_item_table('context', $form_location, $choices, $object->contexts);
+  }
+}
+
+function ctools_context_add_required_context_form($module, &$form, &$form_state, &$form_location, $object) {
+  $form_location = array(
+    '#prefix' => '<div id="ctools-requiredcontexts-table">',
+    '#suffix' => '</div>',
+    '#theme' => 'ctools_context_item_form',
+    '#object_name' => $object->name,
+    '#ctools_context_type' => 'requiredcontext',
+    '#ctools_context_module' => $module,
+  );
+
+  // Store the order the choices are in so javascript can manipulate it.
+  $form_location['markup'] = array(
+    '#value' => '&nbsp;',
+  );
+
+  $choices = array();
+  foreach (ctools_get_contexts() as $name => $arg) {
+    $choices[$name] = $arg['title'];
+  }
+
+  asort($choices);
+
+  if (!empty($choices) || !empty($object->contexts)) {
+    ctools_context_add_item_table('requiredcontext', $form_location, $choices, $object->requiredcontexts);
+  }
+}
+
+function ctools_context_add_relationship_form($module, &$form, &$form_state, &$form_location, $object) {
+  $form_location = array(
+    '#prefix' => '<div id="ctools-relationships-table">',
+    '#suffix' => '</div>',
+    '#theme' => 'ctools_context_item_form',
+    '#object_name' => $object->name,
+    '#ctools_context_type' => 'relationship',
+    '#ctools_context_module' => $module,
+  );
+
+  // Store the order the choices are in so javascript can manipulate it.
+  $form_location['markup'] = array(
+    '#value' => '&nbsp;',
+  );
+
+  $base_contexts = isset($object->base_contexts) ? $object->base_contexts : array();
+  $available_relationships = ctools_context_get_relevant_relationships(ctools_context_load_contexts($object, TRUE, $base_contexts));
+
+  ctools_context_add_item_table('relationship', $form_location, $available_relationships, $object->relationships);
+
+}
+
+/**
+ * Include all context administrative include files, css, javascript.
+ */
+function ctools_context_admin_includes() {
+  ctools_include('modal');
+  ctools_include('ajax');
+  ctools_include('object-cache');
+  ctools_modal_add_js();
+}
+
+/**
+ * Add the context table to the page.
+ */
+function ctools_context_add_item_table($type, &$form, $available_contexts, $items) {
+  $form[$type] = array(
+    '#tree' => TRUE,
+  );
+
+  $module = $form['#ctools_context_module'];
+  $name   = $form['#object_name'];
+
+  if (isset($items) && is_array($items)) {
+    foreach ($items as $position => $context) {
+      ctools_context_add_item_to_form($module, $type, $name, $form[$type][$position], $position, $context);
+    }
+  }
+
+  $type_info = ctools_context_info($type);
+  $form['description'] = array(
+    '#prefix' => '<div class="description">',
+    '#suffix' => '</div>',
+    '#value' => $type_info['description'],
+  );
+
+  ctools_context_add_item_table_buttons($type, $module, $form, $available_contexts);
+}
+
+function ctools_context_add_item_table_buttons($type, $module, &$form, $available_contexts) {
+  $form['buttons'] = array(
+    '#tree' => TRUE,
+  );
+
+  if (!empty($available_contexts)) {
+    $type_info = ctools_context_info($type);
+
+    $module = $form['#ctools_context_module'];
+    $name   = $form['#object_name'];
+
+    // The URL for this ajax button
+    $form['buttons'][$type]['add-url'] = array(
+      '#attributes' => array('class' => "ctools-$type-add-url"),
+      '#type' => 'hidden',
+      '#value' => url("ctools/context/ajax/add/$module/$type/$name", array('absolute' => TRUE)),
+    );
+
+    // This also will be in the URL.
+    $form['buttons'][$type]['item'] = array(
+      '#attributes' => array('class' => "ctools-$type-add-url"),
+      '#type' => 'select',
+      '#options' => $available_contexts,
+    );
+
+    $form['buttons'][$type]['add'] = array(
+      '#type' => 'submit',
+      '#attributes' => array('class' => 'ctools-use-modal'),
+      '#id' => "ctools-$type-add",
+      '#value' => $type_info['add button'],
+    );
+  }
+}
+
+/**
+ * Add a row to the form. Used both in the main form and by
+ * the ajax to add an item.
+ */
+function ctools_context_add_item_to_form($module, $type, $name, &$form, $position, $item) {
+  // This is the single function way to load any plugin by variable type.
+  $info = ctools_context_data($type, $item['name']);
+  $form['title'] = array(
+    '#value' => check_plain($item['identifier']),
+  );
+
+  // Relationships not sortable.
+  $type_info = ctools_context_info($type);
+
+  if (!empty($type_info['sortable'])) {
+    $form['position'] = array(
+      '#type' => 'weight',
+      '#default_value' => $position,
+      '#attributes' => array('class' => 'drag-position'),
+    );
+  }
+
+  $form['remove'] = array(
+    '#value' => ctools_ajax_image_button(ctools_image_path('icon-delete.png'), "ctools/context/ajax/delete/$module/$type/$name/$position", t('Remove this item.'))
+  );
+
+  $form['settings'] = array(
+    '#value' => ctools_modal_image_button(ctools_image_path('icon-configure.png'), "ctools/context/ajax/configure/$module/$type/$name/$position", t('Configure settings for this item.'))
+  );
+}
+
+
+// ---------------------------------------------------------------------------
+// AJAX forms and stuff.
+
+/**
+ * Ajax entry point to add an context
+ */
+function ctools_context_ajax_item_add($module = NULL, $type = NULL, $object_name = NULL, $name = NULL) {
+  ctools_include('ajax');
+  ctools_include('modal');
+  ctools_include('context');
+  ctools_include('object-cache');
+
+  if (!$name) {
+    return ctools_ajax_render_error();
+  }
+
+  // Load stored object from cache.
+  if (!($object = ctools_object_cache_get("context_object:$module", $object_name))) {
+    ctools_ajax_render_error(t('Invalid object name.'));
+  }
+
+  // Get info about what we're adding.
+  $info = ctools_context_data($type, $name);
+  if (empty($info)) {
+    return ctools_ajax_render_error();
+  }
+  $type_info = ctools_context_info($type);
+
+  // Create a reference to the place our context lives. Since this is fairly
+  // generic, this is the easiest way to get right to the place of the
+  // object without knowing precisely what data we're poking at.
+  $ref = &$object->{$type_info['key']};
+
+  // Give this item an id, which is really just the nth version
+  // of this particular context.
+  $id = ctools_get_arg_id($ref, $name) + 1;
+
+  // Figure out the position for our new context.
+  $position = empty($ref) ? 0 : max(array_keys($ref)) + 1;
+
+  // Create the basis for our new context.
+  $ref[$position] = array(
+    'identifier' => $info['title'] . ($id > 1 ? ' ' . $id : ''),
+    'keyword' => ctools_get_keyword($object, $info['keyword']),
+    'id' => $id,
+    'name' => $name,
+  );
+
+  $base_contexts = isset($object->base_contexts) ? $object->base_contexts : array();
+  $contexts = ctools_context_load_contexts($object, TRUE, $base_contexts);
+
+  $form_state = array(
+    'object' => &$object,
+    'module' => $module,
+    'type' => $type,
+    'name' => $name,
+    'ajax' => TRUE,
+    'info' => $info,
+    'position' => $position,
+    'contexts' => $contexts,
+    'ref' => &$ref,
+    'title' => t('Add @type "@context"', array('@type' => $type_info['singular title'], '@context' => $info['title'])),
+  );
+
+  $output = ctools_modal_form_wrapper($type_info['form id'], $form_state);
+
+  if (empty($output)) {
+    // successful submit
+    ctools_object_cache_set("context_object:$module", $object_name, $object);
+
+    $arg_form_state = array();
+
+    $arg_form = array(
+      '#post' => array(),
+      '#programmed' => FALSE,
+      '#tree' => FALSE,
+    );
+
+    // Build a chunk of the form to merge into the displayed form
+    $arg_form[$type] = array(
+      '#tree' => TRUE,
+    );
+    $arg_form[$type][$position] = array(
+      '#tree' => TRUE,
+    );
+
+    ctools_context_add_item_to_form($module, $type, $object_name, $arg_form[$type][$position], $position, $ref[$position]);
+    $arg_form = form_builder($type_info['form id'], $arg_form, $arg_form_state);
+
+    // Build the relationships table so we can ajax it in.
+    // This is an additional thing that goes in here.
+    $rel_form = array(
+      '#theme' => 'ctools_context_item_form',
+      '#object_name' => $object_name,
+      '#ctools_context_type' => 'relationship',
+      '#ctools_context_module' => $module,
+      '#only_buttons' => TRUE,
+      '#post' => array(),
+      '#programmed' => FALSE,
+      '#tree' => FALSE,
+    );
+
+    $rel_form['relationship'] = array(
+      '#tree' => TRUE,
+    );
+
+    // Allow an object to set some 'base' contexts that come from elsewhere.
+    $rel_contexts = isset($object->base_contexts) ? $object->base_contexts : array();
+    $all_contexts = ctools_context_load_contexts($object, TRUE, $rel_contexts);
+    $available_relationships = ctools_context_get_relevant_relationships($all_contexts);
+
+    $output = array();
+    if (!empty($available_relationships)) {
+      ctools_context_add_item_table_buttons('relationship', $module, $rel_form, $available_relationships);
+      $rel_form = form_builder('dummy_form_id', $rel_form, $arg_form_state);
+      $output[] = ctools_ajax_command_replace('div#ctools-relationships-table div.buttons', drupal_render($rel_form));
+    }
+
+    $text = theme('ctools_context_item_row', $type, $arg_form[$type][$position], $position, $position);
+    $output[] = ctools_ajax_command_append('#' . $type . '-table tbody', $text);
+    $output[] = ctools_ajax_command_changed('#' . $type . '-row-' . $position, '.title');
+    $output[] = ctools_modal_command_dismiss();
+  }
+
+  ctools_ajax_render($output);
+}
+
+/**
+ * Ajax entry point to edit an item
+ */
+function ctools_context_ajax_item_edit($module = NULL, $type = NULL, $object_name = NULL, $position = NULL) {
+  ctools_include('ajax');
+  ctools_include('modal');
+  ctools_include('context');
+  ctools_include('object-cache');
+
+  if (!isset($position)) {
+    return ctools_ajax_render_error();
+  }
+
+  // Load stored object from cache.
+  if (!($object = ctools_object_cache_get("context_object:$module", $object_name))) {
+    ctools_ajax_render_error(t('Invalid object name.'));
+  }
+
+  $type_info = ctools_context_info($type);
+
+  // Create a reference to the place our context lives. Since this is fairly
+  // generic, this is the easiest way to get right to the place of the
+  // object without knowing precisely what data we're poking at.
+  $ref = &$object->{$type_info['key']};
+
+  $name = $ref[$position]['name'];
+  if (empty($name)) {
+    ctools_ajax_render_error();
+  }
+
+  // load the context
+  $info = ctools_context_data($type, $name);
+  if (empty($info)) {
+    ctools_ajax_render_error(t('Invalid context type'));
+  }
+
+  $base_contexts = isset($object->base_contexts) ? $object->base_contexts : array();
+  $contexts = ctools_context_load_contexts($object, TRUE, $base_contexts);
+
+  // Remove this context, because we can't really allow circular contexts.
+  unset($contexts[ctools_context_id($ref[$position])]);
+
+  $form_state = array(
+    'object' => &$object,
+    'module' => $module,
+    'type' => $type,
+    'name' => $name,
+    'ajax' => TRUE,
+    'info' => $info,
+    'position' => $position,
+    'contexts' => $contexts,
+    'ref' => &$ref,
+    'title' => t('Edit @type "@context"', array('@type' => $type_info['singular title'], '@context' => $info['title'])),
+  );
+
+  $output = ctools_modal_form_wrapper($type_info['form id'], $form_state);
+
+  if (empty($output)) {
+    // successful submit
+    ctools_object_cache_set("context_object:$module", $object_name, $object);
+
+    $output = array();
+    $output[] = ctools_modal_command_dismiss();
+
+    $arg_form = array(
+      '#post' => array(),
+      '#programmed' => FALSE,
+      '#tree' => FALSE,
+    );
+
+    // Build a chunk of the form to merge into the displayed form
+    $arg_form[$type] = array(
+      '#tree' => TRUE,
+    );
+    $arg_form[$type][$position] = array(
+      '#tree' => TRUE,
+    );
+
+    ctools_context_add_item_to_form($module, $type, $object_name, $arg_form[$type][$position], $position, $ref[$position]);
+    $arg_form = form_builder($type_info['form id'], $arg_form, $arg_form_state);
+
+    $output[] = ctools_ajax_command_replace('#' . $type . '-row-' . $position, theme('ctools_context_item_row', $type, $arg_form[$type][$position], $position, $position));
+    $output[] = ctools_ajax_command_changed('#' . $type . '-row-' . $position, '.title');
+  }
+  ctools_ajax_render($output);
+}
+
+/**
+ * Form (for ajax use) to add a context
+ */
+function ctools_edit_context_form(&$form_state) {
+  $object = $form_state['object'];
+  $context = $form_state['info'];
+  $position = $form_state['position'];
+  $contexts = $form_state['contexts'];
+
+  $ctext = $object->contexts[$position];
+
+  $form['description'] = array(
+    '#prefix' => '<div class="description">',
+    '#suffix' => '</div>',
+    '#value' => check_plain($context['description']),
+  );
+
+  // Basic context values
+  $form['context']['#tree'] = TRUE;
+
+  $form['context']['name'] = array(
+    '#type' => 'hidden',
+    '#value' => $context['name'],
+  );
+
+  $form['context']['id'] = array(
+    '#type' => 'hidden',
+    '#value' => $ctext['id'],
+  );
+
+  $form['context']['identifier'] = array(
+    '#type' => 'textfield',
+    '#title' => t('Identifier'),
+    '#description' => t('Enter a name to identify this !type on administrative screens.', array('!type' =>t('context'))),
+    '#default_value' => $ctext['identifier'],
+  );
+
+  $form['context']['keyword'] = array(
+    '#type' => 'textfield',
+    '#title' => t('Keyword'),
+    '#description' => t('Enter a keyword to use for substitution in titles.'),
+    '#default_value' => $ctext['keyword'],
+  );
+
+  // Settings particular to this context
+  $context_settings = array();
+  if (isset($ctext['context_settings'])) {
+    $context_settings = $ctext['context_settings'];
+  }
+
+  if (isset($context['settings form']) && function_exists($context['settings form'])) {
+    $form['context']['context_settings'] = $context['settings form']($context_settings);
+    $form['context']['context_settings']['#tree'] = TRUE;
+  }
+
+  $form['next'] = array(
+    '#type' => 'submit',
+    '#value' => t('Save'),
+  );
+  return $form;
+}
+
+/**
+ * validate a  context edited/added via ajax
+ */
+function ctools_edit_context_form_validate($form, &$form_state) {
+  $context = $form_state['info'];
+
+  if (isset($context['settings form validate']) && function_exists($context['settings form validate'])) {
+    $context['settings form validate']($form['context']['context_settings'], $form_state['values']['context']['context_settings'], $form_state);
+  }
+}
+
+/**
+ * Updates an context edited/added via ajax
+ */
+function ctools_edit_context_form_submit($form, &$form_state) {
+  $info = $form_state['info'];
+
+  if (isset($info['settings form submit']) && function_exists($info['settings form submit'])) {
+    $info['settings form submit']($form, $form_state['values']['context']['context_settings'], $form_state);
+  }
+
+  $context = $form_state['values']['context'];
+  $form_state['ref'][$form_state['position']] = $context;
+}
+
+/**
+ * Form (for ajax use) to add a context
+ */
+function ctools_edit_requiredcontext_form(&$form_state) {
+  $object = $form_state['object'];
+  $context = $form_state['info'];
+  $position = $form_state['position'];
+  $contexts = $form_state['contexts'];
+
+  $ctext = $object->requiredcontexts[$position];
+  $form['start_form'] = array('#value' => '<div class="modal-form clear-block">');
+
+  $form['description'] = array(
+    '#prefix' => '<div class="description">',
+    '#suffix' => '</div>',
+    '#value' => check_plain($context['description']),
+  );
+
+  // Basic context values
+  $form['requiredcontext']['#tree'] = TRUE;
+
+  $form['requiredcontext']['name'] = array(
+    '#type' => 'hidden',
+    '#value' => $context['name'],
+  );
+
+  $form['requiredcontext']['id'] = array(
+    '#type' => 'hidden',
+    '#value' => $ctext['id'],
+  );
+
+  $form['requiredcontext']['identifier'] = array(
+    '#type' => 'textfield',
+    '#title' => t('Identifier'),
+    '#description' => t('Enter a name to identify this !type on administrative screens.', array('!type' =>t('required context'))),
+    '#default_value' => $ctext['identifier'],
+  );
+
+  $form['requiredcontext']['keyword'] = array(
+    '#type' => 'textfield',
+    '#title' => t('Keyword'),
+    '#description' => t('Enter a keyword to use for substitution in titles.'),
+    '#default_value' => $ctext['keyword'],
+  );
+
+  $form['end_form'] = array('#value' => '</div>');
+
+  $form['next'] = array(
+    '#type' => 'submit',
+    '#value' => t('Save'),
+  );
+  return $form;
+}
+
+/**
+ * Updates a required context edited/added via ajax
+ */
+function ctools_edit_requiredcontext_form_submit($form, &$form_state) {
+  $form_state['ref'][$form_state['position']] = $form_state['values']['requiredcontext'];
+}
+
+/**
+ * Form (for ajax use) to add a relationship
+ */
+function ctools_edit_relationship_form(&$form_state) {
+  $object = $form_state['object'];
+  $relationship = $form_state['info'];
+  $position = $form_state['position'];
+  $contexts = $form_state['contexts'];
+
+  $rel = $object->relationships[$position];
+
+  $form['description'] = array(
+    '#prefix' => '<div class="description">',
+    '#suffix' => '</div>',
+    '#value' => check_plain($relationship['description']),
+  );
+
+  // Basic relationship values
+  $form['relationship']['#tree'] = TRUE;
+
+  $form['relationship']['context'] = ctools_context_selector($contexts, $relationship['required context'], isset($rel['context']) ? $rel['context'] : '');
+
+  $form['relationship']['name'] = array(
+    '#type' => 'hidden',
+    '#value' => $relationship['name'],
+  );
+
+  $form['relationship']['id'] = array(
+    '#type' => 'hidden',
+    '#value' => $rel['id'],
+  );
+
+  $form['relationship']['identifier'] = array(
+    '#type' => 'textfield',
+    '#title' => t('Identifier'),
+    '#description' => t('Enter a name to identify this !type on administrative screens.', array('!type' =>t('relationship'))),
+    '#default_value' => $rel['identifier'],
+  );
+
+  $form['relationship']['keyword'] = array(
+    '#type' => 'textfield',
+    '#title' => t('Keyword'),
+    '#description' => t('Enter a keyword to use for substitution in titles.'),
+    '#default_value' => $rel['keyword'],
+  );
+
+  // Settings particular to this relationship
+  $relationship_settings = array();
+  if (isset($rel['relationship_settings'])) {
+    $relationship_settings = $rel['relationship_settings'];
+  }
+
+  if (isset($relationship['settings form']) && function_exists($relationship['settings form'])) {
+    $form['relationship']['relationship_settings'] = $relationship['settings form']($relationship_settings);
+    $form['relationship']['relationship_settings']['#tree'] = TRUE;
+  }
+
+  $form['next'] = array(
+    '#type' => 'submit',
+    '#value' => t('Save'),
+  );
+  return $form;
+}
+
+/**
+ * validate an relationship edited/added via ajax
+ */
+function ctools_edit_relationship_form_validate($form, &$form_state) {
+  $relationship = $form_state['info'];
+
+  if (isset($relationship['settings form validate']) && function_exists($relationship['settings form validate'])) {
+    $relationship['settings form validate']($form['relationship']['relationship_settings'], $form_state['values']['relationship']['relationship_settings'], $form_state);
+  }
+}
+
+/**
+ * Updates an relationship edited/added via ajax
+ */
+function ctools_edit_relationship_form_submit($form, &$form_state) {
+  $relationship = $form_state['info'];
+
+  if (isset($relationship['settings form submit']) && function_exists($relationship['settings form submit'])) {
+    $relationship['settings form submit']($form, $form_state['values']['relationship_settings'], $form_state);
+  }
+
+  $form_state['ref'][$form_state['position']] = $form_state['values']['relationship'];
+}
+
+/**
+ * Form (for ajax use) to add an argument
+ */
+function ctools_edit_argument_form(&$form_state) {
+  // Basic values required to orient ourselves
+  $object = $form_state['object'];
+  $argument = $form_state['info'];
+  $position = $form_state['position'];
+  $contexts = $form_state['contexts'];
+
+  $arg = $object->arguments[$position];
+
+  if (!isset($arg['default'])) {
+    $arg['default'] = 'ignore';
+    $arg['title'] = '';
+  }
+
+  $form['description'] = array(
+    '#prefix' => '<div class="description">',
+    '#suffix' => '</div>',
+    '#value' => check_plain($argument['description']),
+  );
+
+  // Basic argument values
+  $form['argument']['#tree'] = TRUE;
+
+  $form['argument']['name'] = array(
+    '#type' => 'hidden',
+    '#value' => $argument['name'],
+  );
+
+  $form['argument']['id'] = array(
+    '#type' => 'hidden',
+    '#value' => $arg['id'],
+  );
+
+  $form['argument']['default'] = array(
+    '#type' => 'select',
+    '#title' => t('Default'),
+    '#options' => array(
+      'ignore' => t('Ignore it; content that requires this context will not be available.'),
+      '404' => t('Display page not found or display nothing at all.'),
+    ),
+    '#default_value' => $arg['default'],
+    '#description' => t('If the argument is missing or is not valid, select how this should behave.'),
+  );
+
+  $form['argument']['title'] = array(
+    '#type' => 'textfield',
+    '#title' => t('Title'),
+    '#default_value' => $arg['title'],
+    '#description' => t('Enter a title to use when this argument is present. You may use %KEYWORD substitution, where the keyword is specified by the administrator.'),
+  );
+
+  $form['argument']['identifier'] = array(
+    '#type' => 'textfield',
+    '#title' => t('Identifier'),
+    '#description' => t('Enter a name to identify this !type on administrative screens.', array('!type' =>t('argument'))),
+    '#default_value' => $arg['identifier'],
+  );
+
+  $form['argument']['keyword'] = array(
+    '#type' => 'textfield',
+    '#title' => t('Keyword'),
+    '#description' => t('Enter a keyword to use for substitution in titles.'),
+    '#default_value' => $arg['keyword'],
+  );
+
+  // Settings particular to this argument
+  $argument_settings = array();
+  if (isset($arg['argument_settings'])) {
+    $argument_settings = $arg['argument_settings'];
+  }
+
+
+  if (isset($argument['settings form']) && function_exists($argument['settings form'])) {
+    $form['argument']['argument_settings'] = $argument['settings form']($argument_settings);
+  }
+  $form['argument']['argument_settings']['#tree'] = TRUE;
+
+  $form['next'] = array(
+    '#type' => 'submit',
+    '#value' => t('Save'),
+  );
+
+  return $form;
+}
+
+/**
+ * validate an argument edited/added via ajax
+ */
+function ctools_edit_argument_form_validate($form, &$form_state) {
+  $argument = $form_state['info'];
+
+  if (isset($argument['settings form validate']) && function_exists($argument['settings form validate'])) {
+    $argument['settings form validate']($form, $form_state['values']['argument_settings'], $form_state);
+  }
+}
+
+/**
+ * Updates an argument edited/added via ajax
+ */
+function ctools_edit_argument_form_submit($form, &$form_state) {
+  $argument = $form_state['info'];
+
+  if (!isset($form_state['values']['argument_settings'])) {
+    $form_state['values']['argument_settings'] = array();
+  }
+
+  if (isset($argument['settings form submit']) && function_exists($argument['settings form submit'])) {
+    $argument['settings form submit']($form, $form_state['values']['argument_settings'], $form_state);
+  }
+
+  $form_state['ref'][$form_state['position']] = $form_state['values']['argument'];
+}
+
+/**
+ * Ajax entry point to edit an item
+ */
+function ctools_context_ajax_item_delete($module = NULL, $type = NULL, $object_name = NULL, $position = NULL) {
+  ctools_include('ajax');
+  ctools_include('context');
+  ctools_include('object-cache');
+
+  if (!isset($position)) {
+    return ctools_ajax_render_error();
+  }
+
+  // Load stored object from cache.
+  if (!($object = ctools_object_cache_get("context_object:$module", $object_name))) {
+    ctools_ajax_render_error(t('Invalid object name.'));
+  }
+
+  $type_info = ctools_context_info($type);
+
+  // Create a reference to the place our context lives. Since this is fairly
+  // generic, this is the easiest way to get right to the place of the
+  // object without knowing precisely what data we're poking at.
+  $ref = &$object->{$type_info['key']};
+
+  if (!array_key_exists($position, $ref)) {
+    ctools_ajax_render_error(t('Unable to delete missing item!'));
+  }
+
+  unset($ref[$position]);
+  ctools_object_cache_set("context_object:$module", $object_name, $object);
+
+  $output = array();
+  $output[] = ctools_ajax_command_replace('#' . $type . '-row-' . $position, '');
+  $output[] = ctools_ajax_command_restripe("#$type-table");
+  ctools_ajax_render($output);
+}
+
+// --- End of contexts
+
+function ctools_save_context($type, &$ref, $form_values) {
+  $type_info = ctools_context_info($type);
+
+  // Organize arguments
+  $new = array();
+  $order = array();
+
+  foreach ($ref as $id => $context) {
+    $position = $form_values[$type][$id]['position'];
+    $order[$position] = $id;
+  }
+
+  ksort($order);
+  foreach ($order as $id) {
+    $new[] = $ref[$id];
+  }
+  $ref = $new;
+}
+
+// TODO: Move this somewhere more appropriate
+function ctools_get_arg_id($arguments, $name) {
+  // Figure out which instance of this argument we're creating
+  $id = 0;
+  foreach ($arguments as $arg) {
+    if ($arg['name'] == $name) {
+      if ($arg['id'] > $id) {
+        $id = $arg['id'];
+      }
+    }
+  }
+  return $id;
+}
+
+function ctools_get_keyword($page, $word) {
+  // Create a complete set of keywords
+  $keywords = array();
+  foreach (array('arguments', 'relationships', 'contexts', 'requiredcontexts') as $type) {
+    if (!empty($page->$type) && is_array($page->$type)) {
+      foreach ($page->$type as $info) {
+        $keywords[$info['keyword']] = TRUE;
+      }
+    }
+  }
+
+  $keyword = $word;
+  $count = 0;
+  while (!empty($keywords[$keyword])) {
+    $keyword = $word . '_' . ++$count;
+  }
+  return $keyword;
+}
+
diff --git a/includes/context.inc b/includes/context.inc
new file mode 100644
index 0000000000000000000000000000000000000000..1e5c34a14cb8c89e335b225a7ed7bc5d485a186d
--- /dev/null
+++ b/includes/context.inc
@@ -0,0 +1,828 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ *
+ * Contains code related to the ctools system of 'context'.
+ *
+ * Context, originally from Panels, is a method of packaging objects into
+ * a more generic bundle and providing a plugin system so that a UI can
+ * take advantage of them. The idea is that the context objects
+ * represent 'the context' that a given operation (usually a page view)
+ * is operating in or on.
+ *
+ * For example, when viewing a page, the 'context' is a node object. When
+ * viewing a user, the 'context' is a user object. Contexs can also
+ * have related contexts. For example, when viewing a 'node' you may need
+ * to know something about the node author. Therefore, the node author
+ * is a related context.
+ */
+
+/**
+ * The context object is largely a wrapper around some other object, with
+ * an interface to finding out what is contained and getting to both
+ * the object and information about the object.
+ *
+ * Each context object has its own information, but some things are very
+ * common, such as titles, data, keywords, etc. In particulare, the 'type'
+ * of the context is important.
+ */
+class ctools_context {
+  var $type = NULL;
+  var $data = NULL;
+  // The title of this object.
+  var $title = '';
+  // The title of the page if this object exists
+  var $page_title = '';
+  // The identifier (in the UI) of this object
+  var $identifier = '';
+  var $argument = NULL;
+  var $keyword = '';
+  var $original_argument = NULL;
+
+  function ctools_context($type = 'none', $data = NULL) {
+    $this->type  = $type;
+    $this->data  = $data;
+    $this->title = t('Unknown context');
+  }
+
+  function is_type($type) {
+    if ($type == 'any' || $this->type == 'any') {
+      return TRUE;
+    }
+
+    $a = is_array($type) ? $type : array($type);
+    $b = is_array($this->type) ? $this->type : array($this->type);
+    return (bool) array_intersect($a, $b);
+  }
+
+  function get_argument() {
+    return $this->argument;
+  }
+
+  function get_original_argument() {
+    if (!is_null($this->original_argument)) {
+      return $this->original_argument;
+    }
+    return $this->argument;
+  }
+
+  function get_keyword() {
+    return $this->keyword;
+  }
+
+  function get_identifier() {
+    return $this->identifier;
+  }
+
+  function get_title() {
+    return $this->title;
+  }
+
+  function get_page_title() {
+    return $this->page_title;
+  }
+}
+
+/**
+ * Used to create a method of comparing if a list of contexts
+ * match a required context type.
+ */
+class ctools_context_required {
+  var $keywords = '';
+
+  /**
+   * If set, the title will be used in the selector to identify
+   * the context. This is very useful when multiple contexts
+   * are required to inform the user will be used for what.
+   */
+  var $title = NULL;
+
+  /**
+   * @param $title
+   *   The first parameter should be the 'title' of the context for use
+   *   in UYI selectors when multiple contexts qualify.
+   * @param ...
+   *   One or more keywords to use for matching which contexts are allowed.
+   */
+  function ctools_context_required($title) {
+    $args = func_get_args();
+    $this->title = array_shift($args);
+    if (count($args) == 1) {
+      $args = array_shift($args);
+    }
+    $this->keywords = $args;
+  }
+
+  function filter($contexts) {
+    $result = array();
+
+    // See which of these contexts are valid
+    foreach ((array) $contexts as $cid => $context) {
+      if ($context->is_type($this->keywords)) {
+        $result[$cid] = $context;
+      }
+    }
+
+    return $result;
+  }
+
+  function select($contexts, $context) {
+    if (empty($context) || empty($contexts[$context])) {
+      return FALSE;
+    }
+    return $contexts[$context];
+  }
+}
+
+/**
+ * Used to compare to see if a list of contexts match an optional context. This
+ * can produce empty contexts to use as placeholders.
+ */
+class ctools_context_optional extends ctools_context_required {
+  function ctools_context_optional() {
+    $args = func_get_args();
+    call_user_func_array(array($this, 'ctools_context_required'), $args);
+  }
+
+  /**
+   * Add the 'empty' context which is possible for optional
+   */
+  function add_empty(&$contexts) {
+    $context = new ctools_context('any');
+    $context->title      = t('No context');
+    $context->identifier = t('No context');
+    $contexts = array_merge(array('empty' => $context), $contexts);
+  }
+
+  function filter($contexts) {
+    $this->add_empty($contexts);
+    return parent::filter($contexts);
+  }
+
+  function select($contexts, $context) {
+    $this->add_empty($contexts);
+    if (empty($context)) {
+      return $contexts['empty'];
+    }
+
+    $result = parent::select($contexts, $context);
+
+    // Don't flip out if it can't find the context; this is optional, put
+    // in an empty.
+    if ($result == FALSE) {
+      $result = $contexts['empty'];
+    }
+    return $result;
+  }
+}
+
+/**
+ * Return a keyed array of context that match the given 'required context'
+ * filters.
+ *
+ * Functions or systems that require contexts of a particular type provide a
+ * ctools_context_required or ctools_context_optional object. This function
+ * examines that object and an array of contexts to determine which contexts
+ * match the filter.
+ *
+ * Since multiple contexts can be required, this function will accept either
+ * an array of all required contexts, or just a single required context object.
+ *
+ * @param $contexts
+ *   A keyed array of all available contexts.
+ * @param $required
+ *   A ctools_context_required or ctools_context_optional object, or an array
+ *   of such objects.
+ *
+ * @return
+ *   A keyed array of contexts that match the filter.
+ */
+function ctools_context_filter($contexts, $required) {
+  if (is_array($required)) {
+    $result = array();
+    foreach ($required as $r) {
+      $result = array_merge($result, _ctools_context_filter($contexts, $r));
+    }
+    return $result;
+  }
+
+  return _ctools_context_filter($contexts, $required);
+}
+
+function _ctools_context_filter($contexts, $required) {
+  $result = array();
+
+  if (is_object($required)) {
+    $result = $required->filter($contexts);
+  }
+
+  return $result;
+}
+
+/**
+ * Create a select box to choose possible contexts.
+ *
+ * This only creates a selector if there is actually a choice; if there
+ * is only one possible context, that one is silently assigned.
+ *
+ * If an array of required contexts is provided, one selector will be
+ * provided for each context.
+ *
+ * @param $contexts
+ *   A keyed array of all available contexts.
+ * @param $required
+ *   The required context object or array of objects.
+ *
+ * @return
+ *   A form element, or NULL if there are no contexts that satisfy the
+ *   requirements.
+ */
+function ctools_context_selector($contexts, $required, $default) {
+  if (is_array($required)) {
+    $result = array();
+    $count = 1;
+    foreach ($required as $id => $r) {
+      $result[] = _ctools_context_selector($contexts, $r, $default[$id], $count++);
+    }
+    return $result;
+  }
+
+  return _ctools_context_selector($contexts, $required, $default);
+}
+
+function _ctools_context_selector($contexts, $required, $default, $num = 0) {
+  $filtered = ctools_context_filter($contexts, $required);
+  $count = count($filtered);
+
+  $form = array();
+
+  if ($count == 1) {
+    $keys = array_keys($filtered);
+    return array(
+      '#type' => 'value',
+      '#value' => $keys[0],
+    );
+  }
+
+  if ($count > 1) {
+    // If there's more than one to choose from, create a select widget.
+    foreach ($filtered as $cid => $context) {
+      $options[$cid] = $context->get_identifier();
+    }
+    if (!empty($required->title)) {
+      $title = $required->title;
+    }
+    else {
+      $title = $num ? t('Context %count', array('%count' => $num)) : t('Context');
+    }
+
+    return array(
+      '#type' => 'select',
+      '#options' => $options,
+      '#title' => $title,
+      '#description' => t('Multiple contexts are valid for this pane; one must be chosen.'),
+      '#default_value' => $default,
+    );
+  }
+}
+
+/**
+ * Choose a context or contexts based upon the selection made via
+ * ctools_context_filter.
+ *
+ * @param $contexts
+ *   A keyed array of all available contexts
+ * @param $required
+ *   The required context object provided by the plugin
+ * @param $context
+ *   The selection made using ctools_context_selector
+ */
+function ctools_context_select($contexts, $required, $context) {
+  if (is_array($required)) {
+    $result = array();
+    foreach ($required as $id => $r) {
+      if (($result[] = _ctools_context_select($contexts, $r, $context[$id])) == FALSE) {
+        return FALSE;
+      }
+    }
+    return $result;
+  }
+
+  return _ctools_context_select($contexts, $required, $context);
+}
+
+function _ctools_context_select($contexts, $required, $context) {
+  if (!is_object($required)) {
+    return FALSE;
+  }
+
+  return $required->select($contexts, $context);
+}
+
+/**
+ * Create a new context object.
+ *
+ * @param $type
+ *   The type of context to create; this loads a plugin.
+ * @param $data
+ *   The data to put into the context.
+ * @param $empty
+ *   Whether or not this context is specifically empty.
+ * @param $conf
+ *   A configuration structure if this context was created via UI.
+ *
+ * @return
+ *   A $context or NULL if one could not be created.
+ */
+function ctools_context_create($type, $data = NULL, $conf = FALSE) {
+  if ($function = ctools_plugin_load_function('ctools', 'contexts', $type, 'context')) {
+    return $function(FALSE, $data, $conf);
+  }
+}
+
+/**
+ * Create an empty context object.
+ *
+ * Empty context objects are primarily used as placeholders in the UI where
+ * the actual contents of a context object may not be known. It may have
+ * additional text embedded to give the user clues as to how the context
+ * is used.
+ *
+ * @param $type
+ *   The type of context to create; this loads a plugin.
+ *
+ * @return
+ *   A $context or NULL if one could not be created.
+ */
+function ctools_context_create_empty($type) {
+  if ($function = ctools_plugin_load_function('ctools', 'contexts', $type, 'context')) {
+    return $function(TRUE);
+  }
+}
+
+/**
+ * Fetch keywords for use in string substitutions.
+ *
+ * @param $contexts
+ *   An array of contexts.
+ *
+ * @return
+ *   An array of keyword substitutions suitable for @code{strtr()}
+ */
+function ctools_context_get_keywords($contexts) {
+  $keywords = array();
+  if (!empty($contexts)) {
+    foreach ($contexts as $id => $context) {
+      if ($keyword = $context->get_keyword()) {
+        $keywords["%$keyword"] = $context->get_title();
+      }
+    }
+  }
+  return $keywords;
+}
+
+/**
+ * Determine a unique context ID for a context
+ *
+ * Often contexts of many different types will be placed into a list. This
+ * ensures that even though contexts of multiple types may share IDs, they
+ * are unique in the final list.
+ */
+function ctools_context_id($context, $type = 'context') {
+  return $type . '_' . $context['name'] . '_' . $context['id'];
+}
+
+// ---------------------------------------------------------------------------
+// Functions related to contexts from arguments.
+
+/**
+ * Fetch metadata on a specific argument plugin.
+ *
+ * @param $argument
+ *   Name of an argument plugin.
+ *
+ * @return
+ *   An array with information about the requested argument plugin.
+ */
+function ctools_get_argument($argument) {
+  ctools_include('plugins');
+  return ctools_get_plugins('ctools', 'arguments', $argument);
+}
+
+/**
+ * Fetch metadata for all argument plugins.
+ *
+ * @return
+ *   An array of arrays with information about all available argument plugins.
+ */
+function ctools_get_arguments() {
+  ctools_include('plugins');
+  return ctools_get_plugins('ctools', 'arguments');
+}
+
+/**
+ * Get a context from an argument.
+ *
+ * @param $argument
+ *   The configuration of an argument. It must contain the following data:
+ *   - name: The name of the argument plugin being used.
+ *   - argument_settings: The configuration based upon the plugin forms.
+ *   - identifier: The human readable identifier for this argument, usually
+ *     defined by the UI.
+ *   - keyword: The keyword used for this argument for substitutions.
+ *
+ * @param $arg
+ *   The actual argument received. This is expected to be a string from a URL but
+ *   this does not have to be the only source of arguments.
+ * @param $empty
+ *   If true, the $arg will not be used to load the context. Instead, an empty
+ *   placeholder context will be loaded.
+ *
+ * @return
+ *   A context object if one can be loaded.
+ */
+function ctools_context_get_context_from_argument($argument, $arg, $empty = FALSE) {
+  ctools_include('plugins');
+  if ($function = ctools_plugin_load_function('ctools', 'arguments', $argument['name'], 'context')) {
+    if (!isset($argument['argument_settings'])) {
+      $argument['argument_settings'] = array();
+    }
+
+    $context = $function($arg, $argument['argument_settings'], $empty);
+
+    if (is_object($context)) {
+      $context->identifier = $argument['identifier'];
+      $context->page_title = isset($argument['title']) ? $argument['title'] : '';
+      $context->keyword    = $argument['keyword'];
+    }
+    return $context;
+  }
+}
+
+/**
+ * Retrieve a list of empty contexts for all arguments.
+ */
+function ctools_context_get_placeholders_from_argument($arguments) {
+  $contexts = array();
+  foreach ($arguments as $argument) {
+    $context = ctools_context_get_context_from_argument($argument, NULL, TRUE);
+    if ($context) {
+      $contexts[ctools_context_id($argument, 'argument')] = $context;
+    }
+  }
+  return $contexts;
+}
+
+/**
+ * Load the contexts for a given list of arguments.
+ *
+ * @param $arguments
+ *   The array of argument definitions.
+ * @param &$contexts
+ *   The array of existing contexts. New contexts will be added to this array.
+ * @param $args
+ *   The arguments to load.
+ *
+ * @return
+ *   FALSE if an argument wants to 404.
+ */
+function ctools_context_get_context_from_arguments($arguments, &$contexts, $args) {
+  foreach ($arguments as $argument) {
+    // pull the argument off the list.
+    $arg = array_shift($args);
+    $id = ctools_context_id($argument, 'argument');
+
+    // For % arguments embedded in the URL, our context is already loaded.
+    // There is no need to go and load it again.
+    if (empty($contexts[$id])) {
+      if ($context = ctools_context_get_context_from_argument($argument, $arg)) {
+        $contexts[$id] = $context;
+      }
+    }
+    else {
+      $context = $contexts[$id];
+    }
+
+    if ((empty($context) || empty($context->data)) && $argument['default'] == '404') {
+      return FALSE;
+    }
+  }
+  return TRUE;
+}
+
+// ---------------------------------------------------------------------------
+// Functions related to contexts from relationships.
+
+/**
+ * Fetch metadata on a specific relationship plugin.
+ *
+ * @param $content type
+ *   Name of a panel content type.
+ *
+ * @return
+ *   An array with information about the requested relationship.
+ */
+function ctools_get_relationship($relationship) {
+  ctools_include('plugins');
+  return ctools_get_plugins('ctools', 'relationships', $relationship);
+}
+
+/**
+ * Fetch metadata for all relationship plugins.
+ *
+ * @return
+ *   An array of arrays with information about all available relationships.
+ */
+function ctools_get_relationships() {
+  ctools_include('plugins');
+  return ctools_get_plugins('ctools', 'relationships');
+}
+
+/**
+ * @param $relationship
+ *   The configuration of a relationship. It must contain the following data:
+ *   - name: The name of the relationship plugin being used.
+ *   - relationship_settings: The configuration based upon the plugin forms.
+ *   - identifier: The human readable identifier for this relationship, usually
+ *     defined by the UI.
+ *   - keyword: The keyword used for this relationship for substitutions.
+ * @param $source_context
+ *   The context this relationship is based upon.
+ *
+ * @return
+ *   A context object if one can be loaded.
+ */
+function ctools_context_get_context_from_relationship($relationship, $source_context) {
+  ctools_include('plugins');
+  if ($function = ctools_plugin_load_function('ctools', 'relationships', $relationship['name'], 'context')) {
+    if (!isset($relationship['relationship_settings'])) {
+      $relationship['relationship_settings'] = array();
+    }
+
+    $context = $function($source_context, $relationship['relationship_settings']);
+    if ($context) {
+      $context->identifier = $relationship['identifier'];
+      $context->page_title = isset($relationship['title']) ? $relationship['title'] : '';
+      $context->keyword    = $relationship['keyword'];
+      return $context;
+    }
+  }
+}
+
+/**
+ * Fetch all relevant relationships.
+ *
+ * Relevant relationships are any relationship that can be created based upon
+ * the list of existing contexts. For example, the 'node author' relationship
+ * is relevant if there is a 'node' context, but makes no sense if there is
+ * not one.
+ *
+ * @param $contexts
+ *   An array of contexts used to figure out which relationships are relevant.
+ *
+ * @return
+ *   An array of relationship keys that are relevant for the given set of
+ *   contexts.
+ */
+function ctools_context_get_relevant_relationships($contexts) {
+  $relevant = array();
+  $relationships = ctools_get_relationships();
+
+  // Go through each relationship
+  foreach ($relationships as $rid => $relationship) {
+    // For each relationship, see if there is a context that satisfies it.
+    if (ctools_context_filter($contexts, $relationship['required context'])) {
+      $relevant[$rid] = $relationship['title'];
+    }
+  }
+
+  return $relevant;
+}
+
+/**
+ * Fetch all active relationships
+ *
+ * @param $relationships
+ *   An keyed array of relationship data including:
+ *   - name: name of relationship
+ *   - context: context id relationship belongs to. This will be used to
+ *     identify which context in the $contexts array to use to create the
+ *     relationship context.
+ * @param $contexts
+ *   A keyed array of contexts used to figure out which relationships
+ *   are relevant. New contexts will be added to this.
+ */
+function ctools_context_get_context_from_relationships($relationships, &$contexts) {
+  $return = array();
+
+  foreach ($relationships as $rdata) {
+    if (!isset($rdata['context'])) {
+      continue;
+    }
+
+    if (empty($contexts[$rdata['context']])) {
+      continue;
+    }
+    $relationship = ctools_context_get_context_from_relationship($rdata['name']);
+    // If the relationship can't be found or its context can't be found,
+    // ignore.
+    if (!$relationship) {
+      continue;
+    }
+
+    $cid = ctools_context_id($rdata, 'relationship');
+    if ($context = ctools_context_get_context_from_relationship($rdata, $contexts[$rdata['context']])) {
+      $contexts[$cid] = $context;
+    }
+  }
+}
+
+// ---------------------------------------------------------------------------
+// Functions related to loading contexts from simple context definitions.
+
+/**
+ * Fetch metadata on a specific context plugin.
+ *
+ * @param $context
+ *   Name of a context.
+ *
+ * @return
+ *   An array with information about the requested panel context.
+ */
+function ctools_get_context($context) {
+  ctools_include('plugins');
+  return ctools_get_plugins('ctools', 'contexts', $context);
+}
+
+/**
+ * Fetch metadata for all context plugins.
+ *
+ * @return
+ *   An array of arrays with information about all available panel contexts.
+ */
+function ctools_get_contexts() {
+  ctools_include('plugins');
+  return ctools_get_plugins('ctools', 'contexts');
+}
+
+/**
+ * @param $context
+ *   The configuration of a context. It must contain the following data:
+ *   - name: The name of the context plugin being used.
+ *   - context_settings: The configuration based upon the plugin forms.
+ *   - identifier: The human readable identifier for this context, usually
+ *     defined by the UI.
+ *   - keyword: The keyword used for this context for substitutions.
+ * @param $type
+ *   This is either 'context' which indicates the context will be loaded
+ *   from data in the settings, or 'required_context' which means the
+ *   context must be acquired from an external source. This is the method
+ *   used to pass pure contexts from one system to another.
+ *
+ * @return
+ *   A context object if one can be loaded.
+ */
+function ctools_context_get_context_from_context($context, $type = 'context') {
+  ctools_include('plugins');
+  if ($function = ctools_plugin_load_function('ctools', 'contexts', $context['name'], 'context')) {
+    if (!isset($context['context_settings'])) {
+      $context['context_settings'] = array();
+    }
+
+    $return = $function($type == 'requiredcontext', $context['context_settings'], TRUE);
+    if ($return) {
+      $return->identifier = $context['identifier'];
+      $return->page_title = isset($context['title']) ? $context['title'] : '';
+      $return->keyword    = $context['keyword'];
+      return $return;
+    }
+  }
+}
+
+/**
+ * Retrieve a list of base contexts based upon a simple 'contexts' definition.
+ *
+ * For required contexts this will always retrieve placeholders.
+ *
+ * @param $contexts
+ *   The list of contexts defined in the UI.
+ * @param $type
+ *   Either 'context' or 'requiredcontext', which indicates whether the contexts
+ *   are loaded from internal data or copied from an external source.
+ */
+function ctools_context_get_context_from_contexts($contexts, $type = 'context') {
+  $return = array();
+  foreach ($contexts as $context) {
+    $ctext = ctools_context_get_context_from_context($context, $type);
+    if ($ctext) {
+      $return[ctools_context_id($context, $type)] = $ctext;
+    }
+  }
+  return $return;
+}
+
+/**
+ * Match up external contexts to our required contexts.
+ *
+ * This function is used to create a list of contexts with proper
+ * IDs based upon a list of required contexts.
+ *
+ * These contexts passed in should match the numeric positions of the
+ * required contexts. The caller must ensure this has already happened
+ * correctly as this function will not detect errors here.
+ *
+ * @param $required
+ *   A list of required contexts as defined by the UI.
+ * @param $contexts
+ *   A list of matching contexts as passed in from the calling system.
+ */
+function ctools_context_match_required_contexts($required, $contexts) {
+  $return = array();
+  if (!is_array($required)) {
+    return $return;
+  }
+
+  foreach ($required as $r) {
+    $return[ctools_context_id($r, 'requiredcontext')] = array_shift($contexts);
+  }
+
+  return $return;
+}
+
+/**
+ * Load a full array of contexts for an object.
+ *
+ * Not all of the types need to be supported by this object.
+ *
+ * This function is not used to load contexts from external data, but may
+ * be used to load internal contexts and relationships. Otherwise it can also
+ * be used to generate a full set of placeholders for UI purposes.
+ *
+ * @param $object
+ *   An object that contains some or all of the following variables:
+ *
+ * - requiredcontexts: A list of UI configured contexts that are required
+ *   from an external source. Since these require external data, they will
+ *   only be added if $placeholders is set to TRUE, and empty contexts will
+ *   be created.
+ * - arguments: A list of UI configured arguments that will create contexts.
+ *   Since these require external data, they will only be added if $placeholders
+ *   is set to TRUE.
+ * - contexts: A list of UI configured contexts that have no external source,
+ *   and are essentially hardcoded. For example, these might configure a
+ *   particular node or a particular taxonomy term.
+ * - relationships: A list of UI configured contexts to be derived from other
+ *   contexts that already exist from other sources. For example, these might
+ *   be used to get a user object from a node via the node author relationship.
+ * @param $placeholders
+ *   If TRUE, this will generate placeholder objects for types this function
+ *   cannot load.
+ * @param $contexts
+ *   An array of pre-existing contexts that will be part of the return value.
+ */
+function ctools_context_load_contexts($object, $placeholders = TRUE, $contexts = array()) {
+  if ($placeholders) {
+    // This will load empty contexts as placeholders for arguments that come
+    // from external sources. If this isn't set, it's assumed these context
+    // will already have been matched up and loaded.
+    if (!empty($object->requiredcontexts) && is_array($object->requiredcontexts)) {
+      $contexts += ctools_context_get_context_from_contexts($object->requiredcontexts, 'requiredcontext');
+    }
+
+    if (!empty($object->arguments) && is_array($object->arguments)) {
+      $contexts += ctools_context_get_placeholders_from_argument($object->arguments);
+    }
+  }
+
+  if (!empty($object->contexts) && is_array($object->contexts)) {
+    $contexts += ctools_context_get_context_from_contexts($object->contexts);
+  }
+
+  // add contexts from relationships
+  if (!empty($object->relationships) && is_array($object->relationships)) {
+    ctools_context_get_context_from_relationships($object->relationships, $contexts);
+  }
+
+  return $contexts;
+}
+
+/**
+ * Return the first context with a form id from a list of contexts.
+ *
+ * This function is used to figure out which contexts represents 'the form'
+ * from a list of contexts. Only one contexts can actually be 'the form' for
+ * a given page, since the @code{<form>} tag can not be embedded within
+ * itself.
+ */
+function ctools_context_get_form($contexts) {
+  if (!empty($contexts)) {
+    foreach ($contexts as $context) {
+      if (!empty($context->form_id)) {
+        return $context;
+      }
+    }
+  }
+}
+
diff --git a/includes/context.menu.inc b/includes/context.menu.inc
new file mode 100644
index 0000000000000000000000000000000000000000..584deaaf35119a993f843366b2dedcb205253be8
--- /dev/null
+++ b/includes/context.menu.inc
@@ -0,0 +1,27 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Contains menu item registration for the context tool.
+ *
+ * The menu items registered are AJAX callbacks for the context configuration
+ * popups. They are kept separately for organizational purposes.
+ */
+
+function ctools_context_menu(&$items) {
+  $base = array(
+    'access arguments' => array('access content'),
+    'type' => MENU_CALLBACK,
+    'file' => 'includes/context-admin.inc',
+  );
+  $items['ctools/context/ajax/add'] = array(
+    'page callback' => 'ctools_context_ajax_item_add',
+  ) + $base;
+  $items['ctools/context/ajax/configure'] = array(
+    'page callback' => 'ctools_context_ajax_item_edit',
+  ) + $base;
+  $items['ctools/context/ajax/delete'] = array(
+    'page callback' => 'ctools_context_ajax_item_delete',
+  ) + $base;
+}
diff --git a/includes/context.theme.inc b/includes/context.theme.inc
new file mode 100644
index 0000000000000000000000000000000000000000..1a15547d57a9c82996507b37b4951cecc7ca1f3d
--- /dev/null
+++ b/includes/context.theme.inc
@@ -0,0 +1,266 @@
+<?php
+// $Id$
+/**
+ * @file
+ * Contains theme registry and theme implementations for the context tool.
+ */
+
+function ctools_context_theme(&$theme) {
+  $theme['ctools_context_list'] = array(
+    'arguments' => array('object'),
+    'file' => 'includes/context.theme.inc',
+  );
+  $theme['ctools_context_list_no_table'] = array(
+    'arguments' => array('object'),
+    'file' => 'includes/context.theme.inc',
+  );
+  $theme['ctools_context_item_form'] = array(
+    'arguments' => array('form'),
+    'file' => 'includes/context.theme.inc',
+  );
+  $theme['ctools_context_item_row'] = array(
+    'arguments' => array('type', 'form', 'position', 'count', 'with_tr' => TRUE),
+    'file' => 'includes/context.theme.inc',
+  );
+}
+
+/**
+ * Theme the form item for the context entry.
+ */
+function theme_ctools_context_item_row($type, $form, $position, $count, $with_tr = TRUE) {
+  $output = '<td class="title">&nbsp;' . drupal_render($form['title']) . '</td>';
+  if (!empty($form['position'])) {
+    $output .= '<td class="position">&nbsp;' . drupal_render($form['position']) . '</td>';
+  }
+  $output .= '<td class="operation">' . drupal_render($form['settings']);
+  $output .= drupal_render($form['remove']) . '</td>';
+
+  if ($with_tr) {
+    $output = '<tr id="' . $type . '-row-' . $position . '" class="draggable ' . $type . '-row ' . ($count % 2 ? 'even' : 'odd') . '">' . $output . '</tr>';
+  }
+  return $output;
+}
+
+/**
+ * Display the context item.
+ */
+function theme_ctools_context_item_form($form) {
+  $output = '';
+  $type   = $form['#ctools_context_type'];
+  $module = $form['#ctools_context_module'];
+  $name   = $form['#object_name'];
+
+  $type_info = ctools_context_info($type);
+
+  if (!empty($form[$type]) && empty($form['#only_buttons'])) {
+    $count = 0;
+    $rows = '';
+    foreach (array_keys($form[$type]) as $id) {
+      if (!is_numeric($id)) {
+        continue;
+      }
+      $rows .= theme('ctools_context_item_row', $type, $form[$type][$id], $id, $count++);
+    }
+
+    $output .= '<table id="' . $type . '-table">';
+    $output .= '<thead>';
+    $output .= '<tr>';
+    $output .= '<th class="title">' . $type_info['title'] . '</th>';
+    if (!empty($type_info['sortable']) && $count) {
+      $output .= '<th class="position">' . t('Weight') . '</th>';
+    }
+    $output .= '<th class="operation">' . t('Operation') . '</th>';
+    $output .= '</tr>';
+    $output .= '</thead>';
+    $output .= '<tbody>';
+
+    $output .= $rows;
+
+    $output .= '</tbody>';
+    $output .= '</table>';
+  }
+
+  if (!empty($form['buttons'])) {
+    // Display the add context item.
+    $row   = array();
+    $row[] = array('data' => drupal_render($form['buttons'][$type]['item']), 'class' => 'title');
+    $row[] = array('data' => drupal_render($form['buttons'][$type]['add']), 'class' => 'add', 'width' => "60%");
+    $output .= '<div class="buttons">';
+    $output .= drupal_render($form['buttons'][$type]);
+    $output .= theme('table', array(), array($row), array('id' => $type . '-add-table'));
+    $output .= '</div>';
+  }
+  if (!empty($form['description'])) {
+    $output .= drupal_render($form['description']);
+  }
+
+  if (!empty($type_info['sortable'])) {
+    drupal_add_tabledrag($type . '-table', 'order', 'sibling', 'drag-position');
+  }
+
+  return $output;
+}
+
+/**
+ * Create a visible list of all the contexts available on an object.
+ * Assumes arguments, relationships and context objects.
+ *
+ * Contexts must be preloaded.
+ */
+function theme_ctools_context_list($object, $header = '') {
+  $titles = array();
+  $output = '';
+  $count  = 1;
+  // Describe 'built in' contexts.
+  if (!empty($object->base_contexts)) {
+    foreach ($object->base_contexts as $id => $context) {
+      $output .= '<tr>';
+      $output .= '<td valign="top"><em>' . t('Built in context') . '</em></td>';
+      $desc = check_plain($context->identifier);
+      if (isset($context->keyword)) {
+        $desc .= '<div class="description">' . t('Keyword: %@keyword', array('@keyword' => $context->keyword)) . '</div>';
+      }
+      if (isset($context->description)) {
+        $desc .= '<div class="description">' . filter_xss_admin($context->description) . '</div>';
+      }
+      $output .= '<td>' . $desc . '</td>';
+      $output .= '</tr>';
+      $titles[$id] = $context->identifier;
+      $count++;
+    }
+  }
+
+  // First, make a list of arguments. Arguments are pretty simple.
+  if (!empty($object->arguments)) {
+    foreach ($object->arguments as $argument) {
+      $output .= '<tr>';
+      $output .= '<td valign="top"><em>' . t('Argument @count', array('@count' => $count)) . '</em></td>';
+      $desc = check_plain($argument['identifier']);
+      if (isset($argument['keyword'])) {
+        $desc .= '<div class="description">' . t('Keyword: %@keyword', array('@keyword' => $argument['keyword'])) . '</div>';
+      }
+      $output .= '<td>' . $desc . '</td>';
+      $output .= '</tr>';
+      $titles[ctools_context_id($argument, 'argument')] = $argument['identifier'];
+      $count++;
+    }
+  }
+  $count = 1;
+  // Then, make a nice list of contexts.
+  if (!empty($object->contexts)) {
+    foreach ($object->contexts as $context) {
+      $output .= '<tr>';
+      $output .= '<td valign="top"><em>' . t('Context @count', array('@count' => $count)) . '</em></td>';
+      $desc = check_plain($context['identifier']);
+      if (isset($context['keyword'])) {
+        $desc .= '<div class="description">' . t('Keyword: %@keyword', array('@keyword' => $context['keyword'])) . '</div>';
+      }
+      $output .= '<td>' . $desc . '</td>';
+      $output .= '</tr>';
+      $titles[ctools_context_id($context)] = $context['identifier'];
+      $count++;
+    }
+  }
+  // And relationships
+  if (!empty($object->relationships)) {
+    foreach ($object->relationships as $relationship) {
+      $output .= '<tr>';
+      $output .= '<td valign="top"><em>' . t('From "@title"', array('@title' => $titles[$relationship['context']])) . '</em></td>';
+      $desc = check_plain($relationship['identifier']);
+      if (isset($relationship['keyword'])) {
+        $desc .= '<div class="description">' . t('Keyword: %@keyword', array('@keyword' => $relationship['keyword'])) . '</div>';
+      }
+      $output .= '<td>' . $desc . '</td>';
+      $output .= '</tr>';
+      $titles[ctools_context_id($relationship, 'relationship')] = $relationship['identifier'];
+      $count++;
+    }
+  }
+  if ($output) {
+    $head = '';
+    if ($header) {
+      $head .= '<thead><tr>';
+      $head .= '<th colspan="2">' . $header . '</th>';
+      $head .= '</tr></thead>';
+    }
+    return "<table>$head<tbody>$output</tbody></table>\n";
+  }
+}
+
+/**
+ * ctools_context_list() but not in a table format because tabledrag
+ * won't let us have tables within tables and still drag.
+ */
+function theme_ctools_context_list_no_table($object) {
+  ctools_add_css('context');
+  $titles = array();
+  $output = '';
+  $count  = 1;
+  // Describe 'built in' contexts.
+  if (!empty($object->base_contexts)) {
+    foreach ($object->base_contexts as $id => $context) {
+      $output .= '<div class="ctools-context-holder clear-block">';
+      $output .= '<div class="ctools-context-title">' . t('Built in context') . '</div>';
+      $desc = check_plain($context->identifier);
+      if (isset($context->keyword)) {
+        $desc .= '<div class="description">' . t('Keyword: %@keyword', array('@keyword' => $context->keyword)) . '</div>';
+      }
+      if (isset($context->description)) {
+        $desc .= '<div class="description">' . filter_xss_admin($context->description) . '</div>';
+      }
+      $output .= '<div class="ctools-context-content">' . $desc . '</div>';
+      $output .= '</div>';
+      $titles[$id] = $context->identifier;
+      $count++;
+    }
+  }
+
+  // First, make a list of arguments. Arguments are pretty simple.
+  if (!empty($object->arguments)) {
+    foreach ($object->arguments as $argument) {
+      $output .= '<div class="ctools-context-holder clear-block">';
+      $output .= '<div class="ctools-context-title">' . t('Argument @count', array('@count' => $count)) . '</div>';
+      $desc = check_plain($argument['identifier']);
+      if (isset($argument['keyword'])) {
+        $desc .= '<div class="description">' . t('Keyword: %@keyword', array('@keyword' => $argument['keyword'])) . '</div>';
+      }
+      $output .= '<div class="ctools-context-content">' . $desc . '</div>';
+      $output .= '</div>';
+      $titles[ctools_context_id($argument, 'argument')] = $argument['identifier'];
+      $count++;
+    }
+  }
+  $count = 1;
+  // Then, make a nice list of contexts.
+  if (!empty($object->contexts)) {
+    foreach ($object->contexts as $context) {
+      $output .= '<div class="ctools-context-holder clear-block">';
+      $output .= '<div class="ctools-context-title">' . t('Context @count', array('@count' => $count)) . '</div>';
+      $desc = check_plain($context['identifier']);
+      if (isset($context['keyword'])) {
+        $desc .= '<div class="description">' . t('Keyword: %@keyword', array('@keyword' => $context['keyword'])) . '</div>';
+      }
+      $output .= '<div class="ctools-context-content">' . $desc . '</div>';
+      $output .= '</div>';
+      $titles[ctools_context_id($context)] = $context['identifier'];
+      $count++;
+    }
+  }
+  // And relationships
+  if (!empty($object->relationships)) {
+    foreach ($object->relationships as $relationship) {
+      $output .= '<div class="ctools-context-holder clear-block">';
+      $output .= '<div class="ctools-context-title">' . t('From "@title"', array('@title' => $titles[$relationship['context']])) . '</div>';
+      $desc = check_plain($relationship['identifier']);
+      if (isset($relationship['keyword'])) {
+        $desc .= '<div class="description">' . t('Keyword: %@keyword', array('@keyword' => $relationship['keyword'])) . '</div>';
+      }
+      $output .= '<div class="ctools-context-content">' . $desc . '</div>';
+      $output .= '</div>';
+      $titles[ctools_context_id($relationship, 'relationship')] = $relationship['identifier'];
+      $count++;
+    }
+  }
+
+  return $output;
+}
diff --git a/includes/export.inc b/includes/export.inc
index 0d49f31e98d6892744bbe25fa4a891cb8e6882cd..4b667943005b64c665d67f546ae9dc6084341e89 100644
--- a/includes/export.inc
+++ b/includes/export.inc
@@ -397,3 +397,23 @@ function ctools_export_set_status($table, $name, $new_status = TRUE) {
   $status[$name] = $new_status;
   variable_set($schema['export']['status'], $status);
 }
+
+/**
+ * Provide a form for displaying an export.
+ *
+ * This is a simple form that should be invoked like this:
+ * @code
+ *   $output = drupal_get_form('ctools_export_form', $code, $object_title);
+ * @endcode
+ */
+function ctools_export_form(&$form_state, $code, $title = '') {
+  $lines = substr_count($code, "\n");
+  $form['code'] = array(
+    '#type' => 'textarea',
+    '#title' => $title,
+    '#default_value' => $code,
+    '#rows' => $lines,
+  );
+
+  return $form;
+}
\ No newline at end of file
diff --git a/includes/modal.inc b/includes/modal.inc
new file mode 100644
index 0000000000000000000000000000000000000000..2eacedd18181fd32807c3d00feb004fc522bc522
--- /dev/null
+++ b/includes/modal.inc
@@ -0,0 +1,161 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ * Implement a modal form using AJAX.
+ *
+ * The modal form is implemented primarily from mc.js; this contains the
+ * Drupal specific stuff to use it. The modal is fairly generic and can
+ * be activated mostly by setting up the right classes, but if you are
+ * using the modal you must include links to the images in settings,
+ * because the javascript does not inherently know where the images are
+ * at.
+ *
+ * You can accomplish this with this PHP code:
+ * @code {
+ *   ctools_include('modal');
+ *   ctools_modal_add_js();
+ * }
+ *
+ * You can have links and buttons bound to use the modal by adding the
+ * class ctools-use-modal.
+ *
+ * For links, the href of the link will be the destination, with any
+ * appearance of /nojs/ converted to /ajax/.
+ *
+ * For submit buttons, however, the URL is found a different, slightly
+ * more complex way. The ID of the item is taken and -url is appended to
+ * it to derive a class name. Then, all form elements that contain that
+ * class name are founded and their values put together to form a
+ * URL.
+ *
+ * For example, let's say you have an 'add' button, and it has a select
+ * form item that tells your system what widget it is adding. If the id
+ * of the add button is edit-add, you would place a hidden input with
+ * the base of your URL in the form and give it a class of 'edit-add-url'.
+ * You would then add 'edit-add-url' as a class to the select widget
+ * allowing you to convert this value to the form without posting.
+ *
+ * If no URL is found, the action of the form will be used and the entire
+ * form posted to it.
+ */
+
+function ctools_modal_add_js() {
+  // Provide a gate so we only do this once.
+  static $done = FALSE;
+  if ($done) {
+    return;
+  }
+
+  $settings = array('CToolsModal' => array(
+    'closeText' => t('Close Window'),
+    'closeImage' => theme('image', ctools_image_path('icon-close-window.png'), t('Close window'), t('Close window')),
+    'throbber' => theme('image', ctools_image_path('throbber.gif'), t('Loading...'), t('Loading')),
+  ));
+
+  drupal_add_js($settings, 'setting');
+  ctools_add_js('dimensions');
+  ctools_add_js('mc');
+  ctools_add_js('ajax-responder');
+  ctools_add_js('modal');
+
+  ctools_add_css('modal');
+  $done = TRUE;
+}
+
+/**
+ * Place HTML within the modal.
+ *
+ * @param $title
+ *   The title of the modal.
+ * @param $html
+ *   The html to place within the modal.
+ */
+function ctools_modal_command_display($title, $html) {
+  return array(
+    'command' => 'modal_display',
+    'title' => $title,
+    'output' => $html,
+  );
+}
+
+/**
+ * Dismiss the modal.
+ */
+function ctools_modal_command_dismiss() {
+  return array(
+    'command' => 'modal_dismiss',
+  );
+}
+
+/**
+ * Render an image as a button link. This will automatically apply an AJAX class
+ * to the link and add the appropriate javascript to make this happen.
+ *
+ * @param $image
+ *   The path to an image to use that will be sent to theme('image') for rendering.
+ * @param $dest
+ *   The destination of the link.
+ * @param $alt
+ *   The alt text of the link.
+ * @param $class
+ *   Any class to apply to the link. @todo this should be a options array.
+ */
+function ctools_modal_image_button($image, $dest, $alt, $class = '') {
+  return ctools_ajax_text_button(theme('image', $image), $dest, $alt, $class, 'ctools-use-modal');
+}
+
+/**
+ * Render text as a link. This will automatically apply an AJAX class
+ * to the link and add the appropriate javascript to make this happen.
+ *
+ * Note: 'html' => true so be sure any text is vetted! Chances are these kinds of buttons will
+ * not use user input so this is a very minor concern.
+ *
+ * @param $image
+ *   The path to an image to use that will be sent to theme('image') for rendering.
+ * @param $dest
+ *   The destination of the link.
+ * @param $alt
+ *   The alt text of the link.
+ * @param $class
+ *   Any class to apply to the link. @todo this should be a options array.
+ */
+function ctools_modal_text_button($text, $dest, $alt, $class = '') {
+  return ctools_ajax_text_button($text, $dest, $alt, $class, 'ctools-use-modal');
+}
+
+/**
+ * Wrap a form so that we can use it properly with AJAX. Essentially if the
+ * form wishes to render, it automatically does that, otherwise it returns
+ * so we can see submission results.
+ */
+function ctools_modal_form_wrapper($form_id, &$form_state) {
+  ctools_include('form');
+  ctools_include('modal');
+  // This won't override settings already in.
+  $form_state += array(
+    're_render' => FALSE,
+    'no_redirect' => !empty($form_state['ajax']),
+  );
+
+  $output = ctools_build_form($form_id, $form_state);
+  if (!empty($form_state['ajax']) && empty($form_state['executed'])) {
+    $title = empty($form_state['title']) ? '' : $form_state['title'];
+
+    // If there are messages for the form, render them.
+    if ($messages = theme('status_messages')) {
+      $output = '<div class="views-messages">' . $messages . '</div>' . $output;
+    }
+
+    $output = array(ctools_modal_command_display($title, $output));
+  }
+
+  // These forms have the title built in, so set the title here:
+  if (empty($form_state['ajax']) && !empty($form_state['title'])) {
+    drupal_set_title($form_state['title']);
+  }
+
+  return $output;
+}
diff --git a/includes/plugins.inc b/includes/plugins.inc
index d5b843ebb7ad2035c43cd261964abee9d69e61cf..4c910603e379483732b8ab9aca11e921b389a582 100644
--- a/includes/plugins.inc
+++ b/includes/plugins.inc
@@ -9,6 +9,89 @@
  * necessary.
  */
 
+/**
+ * Fetch a group of plugins by name.
+ *
+ * @param $module
+ *   The name of the module that utilizes this plugin system. It will be
+ *   used to call hook_ctools_plugin_$plugin() to get more data about the plugin.
+ * @param $type
+ *   The type identifier of the plugin.
+ * @param $id
+ *   If specified, return only information about plugin with this identifier.
+ *   The system will do its utmost to load only plugins with this id.
+ *
+ * @return
+ *   An array of information arrays about the plugins received. The contents
+ *   of the array are specific to the plugin.
+ */
+function ctools_get_plugins($module, $type, $id = NULL) {
+  static $plugins = array();
+  static $all_hooks = array();
+  static $all_files = array();
+  static $info = array();
+
+  if (!isset($info[$type])) {
+    $info[$type] = ctools_plugin_get_info($module, $type);
+  }
+
+  // If the plugin info says this can be cached, check cache first.
+  if ($info[$type]['cache'] && !isset($plugins['cache'])) {
+    // @todo Maybe this should use our own table but free wiping
+    // with content updates is convenient.
+    $cache = cache_get("plugins:$module:$type");
+
+    // if cache load successful, set $all_hooks and $all_files to true.
+    if (!empty($cache->data)) {
+      $plugins[$type] = $cache->data;
+      $all_hooks[$type] = TRUE;
+      $all_files[$type] = TRUE;
+    }
+    else {
+      $write_cache = TRUE;
+    }
+  }
+
+  // Always load all hooks if we need them.
+  if (!isset($all_hooks[$type])) {
+    $all_hooks[$type] = TRUE;
+    $plugins[$type] = ctools_plugin_load_hooks($info[$type]);
+  }
+
+  // First, see if it's in our hooks before we even bother.
+  if ($id && array_key_exists($id, $plugins[$type])) {
+    return $plugins[$type][$id];
+  }
+
+  // Then see if we should load all files. We only do this if we
+  // want a list of all plugins.
+  if ((!$id || $info[$type]['cache']) && empty($all_files[$type])) {
+    $all_files[$type] = TRUE;
+    $plugins[$type] = array_merge($plugins[$type], ctools_plugin_load_includes($info[$type]));
+  }
+
+  // If we were told earlier that this is cacheable and the cache was
+  // empty, give something back.
+  if (!empty($write_cache)) {
+    cache_set("plugins:$module:$type", $plugins[$type]);
+  }
+
+  // If no id was requested, we are finished here:
+  if (!$id) {
+    return $plugins[$type];
+  }
+
+  // Check to see if we need to look for the file
+  if (!array_key_exists($id, $plugins[$type])) {
+    $result = ctools_plugin_load_includes($info[$type], $id);
+    // Set to either what was returned or NULL.
+    $plugins[$type][$id] = isset($result[$id]) ? $result[$id] : NULL;
+  }
+
+  // At this point we should either have the plugin, or a NULL.
+  return $plugins[$type][$id];
+}
+
 /**
  * Load plugins from a directory.
  *
@@ -65,7 +148,7 @@ function ctools_plugin_get_directories($info) {
 
   foreach (module_implements('ctools_plugin_directory') as $module) {
     $function = $module . '_ctools_plugin_directory';
-    $result = $function($info['type']);
+    $result = $function($info['module'], $info['type']);
     if ($result && is_string($result)) {
       $directories[$module] = drupal_get_path('module', $module) . '/' . $result;
     }
@@ -158,89 +241,6 @@ function ctools_plugin_get_info($module, $type) {
   return $info;
 }
 
-/**
- * Fetch a group of plugins by name.
- *
- * @param $module
- *   The name of the module that utilizes this plugin system. It will be
- *   used to call hook_ctools_plugin_$plugin() to get more data about the plugin.
- * @param $type
- *   The type identifier of the plugin.
- * @param $id
- *   If specified, return only information about plugin with this identifier.
- *   The system will do its utmost to load only plugins with this id.
- *
- * @return
- *   An array of information arrays about the plugins received. The contents
- *   of the array are specific to the plugin.
- */
-function _ctools_get_plugins($module, $type, $id = NULL) {
-  static $plugins = array();
-  static $all_hooks = array();
-  static $all_files = array();
-  static $info = array();
-
-  if (!isset($info[$type])) {
-    $info[$type] = ctools_plugin_get_info($module, $type);
-  }
-
-  // If the plugin info says this can be cached, check cache first.
-  if ($info[$type]['cache'] && !isset($plugins['cache'])) {
-    // @todo Maybe this should use our own table but free wiping
-    // with content updates is convenient.
-    $cache = cache_get("plugins:$module:$type");
-
-    // if cache load successful, set $all_hooks and $all_files to true.
-    if (!empty($cache->data)) {
-      $plugins[$type] = $cache->data;
-      $all_hooks[$type] = TRUE;
-      $all_files[$type] = TRUE;
-    }
-    else {
-      $write_cache = TRUE;
-    }
-  }
-
-  // Always load all hooks if we need them.
-  if (!isset($all_hooks[$type])) {
-    $all_hooks[$type] = TRUE;
-    $plugins[$type] = ctools_plugin_load_hooks($info[$type]);
-  }
-
-  // First, see if it's in our hooks before we even bother.
-  if ($id && array_key_exists($id, $plugins[$type])) {
-    return $plugins[$type][$id];
-  }
-
-  // Then see if we should load all files. We only do this if we
-  // want a list of all plugins.
-  if ((!$id || $info[$type]['cache']) && empty($all_files[$type])) {
-    $all_files[$type] = TRUE;
-    $plugins[$type] = array_merge($plugins[$type], ctools_plugin_load_includes($info[$type]));
-  }
-
-  // If we were told earlier that this is cacheable and the cache was
-  // empty, give something back.
-  if (!empty($write_cache)) {
-    cache_set("plugins:$module:$type", $plugins[$type]);
-  }
-
-  // If no id was requested, we are finished here:
-  if (!$id) {
-    return $plugins[$type];
-  }
-
-  // Check to see if we need to look for the file
-  if (!array_key_exists($id, $plugins[$type])) {
-    $result = ctools_plugin_load_includes($info[$type], $id);
-    // Set to either what was returned or NULL.
-    $plugins[$type][$id] = isset($result[$id]) ? $result[$id] : NULL;
-  }
-
-  // At this point we should either have the plugin, or a NULL.
-  return $plugins[$type][$id];
-}
-
 /**
  * Get a function from a plugin, if it exists. If the plugin is not already
  * loaded, try ctools_plugin_load_function() instead.
@@ -258,6 +258,7 @@ function ctools_plugin_get_function($plugin, $function_name) {
   // If cached the .inc file may not have been loaded. require_once is quite safe
   // and fast so it's okay to keep calling it.
   if (isset($plugin['file'])) {
+    if (!is_array($plugin)) { vpr_trace(); }
     require_once './' . $plugin['path'] . '/' . $plugin['file'];
   }
 
@@ -302,6 +303,6 @@ function ctools_plugin_get_function($plugin, $function_name) {
  *   does not exist.
  */
 function ctools_plugin_load_function($module, $type, $id, $function_name) {
-  $plugin = _ctools_get_plugins($module, $type, $id);
+  $plugin = ctools_get_plugins($module, $type, $id);
   return ctools_plugin_get_function($plugin, $function_name);
 }
diff --git a/js/ajax-responder.js b/js/ajax-responder.js
new file mode 100644
index 0000000000000000000000000000000000000000..6633c4f88912c135c3942ec21eb9356885c49b5d
--- /dev/null
+++ b/js/ajax-responder.js
@@ -0,0 +1,160 @@
+// $Id $
+
+/**
+ * @file
+ *
+ * CTools flexible AJAX responder object.
+ */
+
+Drupal.CTools = Drupal.CTools || {};
+Drupal.CTools.AJAX = Drupal.CTools.AJAX || {};
+
+/**
+ * Success callback for an ajax request.
+ *
+ * This function expects to receive a packet of data from a JSON object
+ * which is essentially a list of commands. Each commands must have a
+ * 'command' setting and this setting must resolve to a function in the
+ * Drupal.CTools.AJAX.commands space.
+ */
+Drupal.CTools.AJAX.respond = function(data) {
+  for (i in data) {
+    if (data[i]['command'] && Drupal.CTools.AJAX.commands[data[i]['command']]) {
+      Drupal.CTools.AJAX.commands[data[i]['command']](data[i]);
+    }
+  }
+};
+
+/**
+ * Generic replacement click handler to open the modal with the destination
+ * specified by the href of the link.
+ */
+Drupal.CTools.AJAX.clickAJAXLink = function() {
+  var url = $(this).attr('href');
+  url.replace('/nojs/', '/ajax/');
+  $.ajax({
+    type: "POST",
+    url: url,
+    data: '',
+    global: true,
+    success: Drupal.CTools.AJAX.respond,
+    error: function() { 
+      alert("An error occurred while attempting to process " + url); 
+    },
+    dataType: 'json'
+  });
+  return false;
+};
+
+/**
+ * Generic replacement click handler to open the modal with the destination
+ * specified by the href of the link.
+ */
+Drupal.CTools.AJAX.clickAJAXButton = function() {
+  // @todo -- if no url we should take the form action and submit the
+  // form.
+  var url = Drupal.CTools.AJAX.findURL(this);
+  if (url) {
+    url.replace('/nojs/', '/ajax/');
+    $.ajax({
+      type: "POST",
+      url: url,
+      data: '',
+      global: true,
+      success: Drupal.CTools.AJAX.respond,
+      error: function() { 
+        alert("An error occurred while attempting to process " + url); 
+      },
+      dataType: 'json'
+    });
+  }
+  else {
+    var form = $(this).parents('form');
+    url = $(form).attr('action');
+    url.replace('/nojs/', '/ajax/');
+    $(form).ajaxSubmit({
+      type: "POST",
+      url: url,
+      data: '',
+      global: true,
+      success: Drupal.CTools.AJAX.respond,
+      error: function() { 
+        alert("An error occurred while attempting to process " + url); 
+      },
+      dataType: 'json'
+    });
+  }
+  return false;
+};
+
+/**
+ * Find a URL for an AJAX button.
+ *
+ * The URL for this gadget will be composed of the values of items by
+ * taking the ID of this item and adding -url and looking for that
+ * class. They need to be in the form in order since we will
+ * concat them all together using '/'.
+ */
+Drupal.CTools.AJAX.findURL = function(item) {
+  var url = '';
+  var url_class = '.' + $(item).attr('id') + '-url';
+  $(url_class).each(
+    function() { 
+      if (url && $(this).val()) { 
+        url += '/'; 
+      }
+      url += $(this).val(); 
+    });
+  return url;
+};
+
+Drupal.CTools.AJAX.commands = {
+  append: function(data) {
+    $(data.selector).append(data.data);
+    Drupal.attachBehaviors($(data.selector));
+  },
+
+  replace: function(data) {
+    $(data.selector).replaceWith(data.data);
+    Drupal.attachBehaviors($(data.selector));
+  }, 
+
+  changed: function(data) {
+    $(data.selector).addClass('changed');
+    if (data.star) {
+      $(data.selector).find(data.star).append(' <span class="star">*</span> ');
+    }
+  },
+
+  alert: function(data) {
+    alert(data.text, data.title);
+  }, 
+
+  restripe: function(data) {
+    // :even and :odd are reversed because jquery counts from 0 and
+    // we count from 1, so we're out of sync.
+    $('tbody tr:not(:hidden)', $(data.selector))
+      .removeClass('even')
+      .removeClass('odd')
+      .filter(':even')
+        .addClass('odd')
+      .end()
+      .filter(':odd')
+        .addClass('even');
+  }
+};
+
+/**
+ * Bind links that will open modals to the appropriate function.
+ */
+Drupal.behaviors.CToolsAJAX = function(context) {
+  // Bind links
+  $('a.ctools-use-ajax:not(.ctools-use-ajax-processed)', context)
+    .addClass('ctools-use-ajax-processed')
+    .click(Drupal.CTools.AJAX.clickAJAXLink);
+
+  // Bind buttons
+  $('input.ctools-use-ajax:not(.ctools-use-ajax-processed)', context)
+    .addClass('ctools-use-ajax-processed')
+    .click(Drupal.CTools.AJAX.clickAJAXButton);
+};
diff --git a/js/dimensions.js b/js/dimensions.js
new file mode 100644
index 0000000000000000000000000000000000000000..0bfb1b7559b4711b81d9178a2ce045b1625f350d
--- /dev/null
+++ b/js/dimensions.js
@@ -0,0 +1,320 @@
+/*
+ * Dual licensed under the MIT (http://www.opensource.org/licenses/mit-license.php)
+ * and GPL (http://www.opensource.org/licenses/gpl-license.php) licenses.
+ *
+ * $LastChangedDate: 2007-03-27 16:29:43 -0500 (Tue, 27 Mar 2007) $
+ * $Rev: 1601 $
+ */
+
+jQuery.fn._height = jQuery.fn.height;
+jQuery.fn._width  = jQuery.fn.width;
+
+/**
+ * If used on document, returns the document's height (innerHeight)
+ * If used on window, returns the viewport's (window) height
+ * See core docs on height() to see what happens when used on an element.
+ *
+ * @example $("#testdiv").height()
+ * @result 200
+ *
+ * @example $(document).height()
+ * @result 800
+ *
+ * @example $(window).height()
+ * @result 400
+ *
+ * @name height
+ * @type Object
+ * @cat Plugins/Dimensions
+ */
+jQuery.fn.height = function() {
+	if ( this[0] == window )
+		return self.innerHeight ||
+			jQuery.boxModel && document.documentElement.clientHeight ||
+			document.body.clientHeight;
+
+	if ( this[0] == document )
+		return Math.max( document.body.scrollHeight, document.body.offsetHeight );
+
+	return this._height(arguments[0]);
+};
+
+/**
+ * If used on document, returns the document's width (innerWidth)
+ * If used on window, returns the viewport's (window) width
+ * See core docs on height() to see what happens when used on an element.
+ *
+ * @example $("#testdiv").width()
+ * @result 200
+ *
+ * @example $(document).width()
+ * @result 800
+ *
+ * @example $(window).width()
+ * @result 400
+ *
+ * @name width
+ * @type Object
+ * @cat Plugins/Dimensions
+ */
+jQuery.fn.width = function() {
+	if ( this[0] == window )
+		return self.innerWidth ||
+			jQuery.boxModel && document.documentElement.clientWidth ||
+			document.body.clientWidth;
+
+	if ( this[0] == document )
+		return Math.max( document.body.scrollWidth, document.body.offsetWidth );
+
+	return this._width(arguments[0]);
+};
+
+/**
+ * Returns the inner height value (without border) for the first matched element.
+ * If used on document, returns the document's height (innerHeight)
+ * If used on window, returns the viewport's (window) height
+ *
+ * @example $("#testdiv").innerHeight()
+ * @result 800
+ *
+ * @name innerHeight
+ * @type Number
+ * @cat Plugins/Dimensions
+ */
+jQuery.fn.innerHeight = function() {
+	return this[0] == window || this[0] == document ?
+		this.height() :
+		this.css('display') != 'none' ?
+		 	this[0].offsetHeight - (parseInt(this.css("borderTopWidth")) || 0) - (parseInt(this.css("borderBottomWidth")) || 0) :
+			this.height() + (parseInt(this.css("paddingTop")) || 0) + (parseInt(this.css("paddingBottom")) || 0);
+};
+
+/**
+ * Returns the inner width value (without border) for the first matched element.
+ * If used on document, returns the document's Width (innerWidth)
+ * If used on window, returns the viewport's (window) width
+ *
+ * @example $("#testdiv").innerWidth()
+ * @result 1000
+ *
+ * @name innerWidth
+ * @type Number
+ * @cat Plugins/Dimensions
+ */
+jQuery.fn.innerWidth = function() {
+	return this[0] == window || this[0] == document ?
+		this.width() :
+		this.css('display') != 'none' ?
+			this[0].offsetWidth - (parseInt(this.css("borderLeftWidth")) || 0) - (parseInt(this.css("borderRightWidth")) || 0) :
+			this.height() + (parseInt(this.css("paddingLeft")) || 0) + (parseInt(this.css("paddingRight")) || 0);
+};
+
+/**
+ * Returns the outer height value (including border) for the first matched element.
+ * Cannot be used on document or window.
+ *
+ * @example $("#testdiv").outerHeight()
+ * @result 1000
+ *
+ * @name outerHeight
+ * @type Number
+ * @cat Plugins/Dimensions
+ */
+jQuery.fn.outerHeight = function() {
+	return this[0] == window || this[0] == document ?
+		this.height() :
+		this.css('display') != 'none' ?
+			this[0].offsetHeight :
+			this.height() + (parseInt(this.css("borderTopWidth")) || 0) + (parseInt(this.css("borderBottomWidth")) || 0)
+				+ (parseInt(this.css("paddingTop")) || 0) + (parseInt(this.css("paddingBottom")) || 0);
+};
+
+/**
+ * Returns the outer width value (including border) for the first matched element.
+ * Cannot be used on document or window.
+ *
+ * @example $("#testdiv").outerWidth()
+ * @result 1000
+ *
+ * @name outerWidth
+ * @type Number
+ * @cat Plugins/Dimensions
+ */
+jQuery.fn.outerWidth = function() {
+	return this[0] == window || this[0] == document ?
+		this.width() :
+		this.css('display') != 'none' ?
+			this[0].offsetWidth :
+			this.height() + (parseInt(this.css("borderLeftWidth")) || 0) + (parseInt(this.css("borderRightWidth")) || 0)
+				+ (parseInt(this.css("paddingLeft")) || 0) + (parseInt(this.css("paddingRight")) || 0);
+};
+
+/**
+ * Returns how many pixels the user has scrolled to the right (scrollLeft).
+ * Works on containers with overflow: auto and window/document.
+ *
+ * @example $("#testdiv").scrollLeft()
+ * @result 100
+ *
+ * @name scrollLeft
+ * @type Number
+ * @cat Plugins/Dimensions
+ */
+jQuery.fn.scrollLeft = function() {
+	if ( this[0] == window || this[0] == document )
+		return self.pageXOffset ||
+			jQuery.boxModel && document.documentElement.scrollLeft ||
+			document.body.scrollLeft;
+
+	return this[0].scrollLeft;
+};
+
+/**
+ * Returns how many pixels the user has scrolled to the bottom (scrollTop).
+ * Works on containers with overflow: auto and window/document.
+ *
+ * @example $("#testdiv").scrollTop()
+ * @result 100
+ *
+ * @name scrollTop
+ * @type Number
+ * @cat Plugins/Dimensions
+ */
+jQuery.fn.scrollTop = function() {
+	if ( this[0] == window || this[0] == document )
+		return self.pageYOffset ||
+			jQuery.boxModel && document.documentElement.scrollTop ||
+			document.body.scrollTop;
+
+	return this[0].scrollTop;
+};
+
+/**
+ * Returns the location of the element in pixels from the top left corner of the viewport.
+ *
+ * For accurate readings make sure to use pixel values for margins, borders and padding.
+ *
+ * @example $("#testdiv").offset()
+ * @result { top: 100, left: 100, scrollTop: 10, scrollLeft: 10 }
+ *
+ * @example $("#testdiv").offset({ scroll: false })
+ * @result { top: 90, left: 90 }
+ *
+ * @example var offset = {}
+ * $("#testdiv").offset({ scroll: false }, offset)
+ * @result offset = { top: 90, left: 90 }
+ *
+ * @name offset
+ * @param Object options A hash of options describing what should be included in the final calculations of the offset.
+ *                       The options include:
+ *                           margin: Should the margin of the element be included in the calculations? True by default.
+ *                                   If set to false the margin of the element is subtracted from the total offset.
+ *                           border: Should the border of the element be included in the calculations? True by default.
+ *                                   If set to false the border of the element is subtracted from the total offset.
+ *                           padding: Should the padding of the element be included in the calculations? False by default.
+ *                                    If set to true the padding of the element is added to the total offset.
+ *                           scroll: Should the scroll offsets of the parent elements be included in the calculations?
+ *                                   True by default. When true, it adds the total scroll offsets of all parents to the
+ *                                   total offset and also adds two properties to the returned object, scrollTop and
+ *                                   scrollLeft. If set to false the scroll offsets of parent elements are ignored.
+ *                                   If scroll offsets are not needed, set to false to get a performance boost.
+ * @param Object returnObject An object to store the return value in, so as not to break the chain. If passed in the
+ *                            chain will not be broken and the result will be assigned to this object.
+ * @type Object
+ * @cat Plugins/Dimensions
+ * @author Brandon Aaron (brandon.aaron@gmail.com || http://brandonaaron.net)
+ */
+jQuery.fn.offset = function(options, returnObject) {
+	var x = 0, y = 0, elem = this[0], parent = this[0], absparent=false, relparent=false, op, sl = 0, st = 0, options = jQuery.extend({ margin: true, border: true, padding: false, scroll: true }, options || {});
+	do {
+		x += parent.offsetLeft || 0;
+		y += parent.offsetTop  || 0;
+
+		// Mozilla and IE do not add the border
+		if (jQuery.browser.mozilla || jQuery.browser.msie) {
+			// get borders
+			var bt = parseInt(jQuery.css(parent, 'borderTopWidth')) || 0;
+			var bl = parseInt(jQuery.css(parent, 'borderLeftWidth')) || 0;
+
+			// add borders to offset
+			x += bl;
+			y += bt;
+
+			// Mozilla removes the border if the parent has overflow property other than visible
+			if (jQuery.browser.mozilla && parent != elem && jQuery.css(parent, 'overflow') != 'visible') {
+				x += bl;
+				y += bt;
+			}
+			
+			// Mozilla does not include the border on body if an element isn't positioned absolute and is without an absolute parent
+			if (jQuery.css(parent, 'position') == 'absolute') absparent = true;
+			// IE does not include the border on the body if an element is position static and without an absolute or relative parent
+			if (jQuery.css(parent, 'position') == 'relative') relparent = true;
+		}
+
+		if (options.scroll) {
+			// Need to get scroll offsets in-between offsetParents
+			op = parent.offsetParent;
+			do {
+				sl += parent.scrollLeft || 0;
+				st += parent.scrollTop  || 0;
+
+				parent = parent.parentNode;
+
+				// Mozilla removes the border if the parent has overflow property other than visible
+				if (jQuery.browser.mozilla && parent != elem && parent != op && jQuery.css(parent, 'overflow') != 'visible') {
+					x += parseInt(jQuery.css(parent, 'borderLeftWidth')) || 0;
+					y += parseInt(jQuery.css(parent, 'borderTopWidth')) || 0;
+				}
+			} while (op && parent != op);
+		} else
+			parent = parent.offsetParent;
+
+		if (parent && (parent.tagName.toLowerCase() == 'body' || parent.tagName.toLowerCase() == 'html')) {
+			// Safari and IE Standards Mode doesn't add the body margin for elments positioned with static or relative
+			if ((jQuery.browser.safari || (jQuery.browser.msie && jQuery.boxModel)) && jQuery.css(elem, 'position') != 'absolute') {
+				x += parseInt(jQuery.css(parent, 'marginLeft')) || 0;
+				y += parseInt(jQuery.css(parent, 'marginTop'))  || 0;
+			}
+			// Mozilla does not include the border on body if an element isn't positioned absolute and is without an absolute parent
+			// IE does not include the border on the body if an element is positioned static and without an absolute or relative parent
+			if ( (jQuery.browser.mozilla && !absparent) || 
+			     (jQuery.browser.msie && jQuery.css(elem, 'position') == 'static' && (!relparent || !absparent)) ) {
+				x += parseInt(jQuery.css(parent, 'borderLeftWidth')) || 0;
+				y += parseInt(jQuery.css(parent, 'borderTopWidth'))  || 0;
+			}
+			break; // Exit the loop
+		}
+	} while (parent);
+
+	if ( !options.margin) {
+		x -= parseInt(jQuery.css(elem, 'marginLeft')) || 0;
+		y -= parseInt(jQuery.css(elem, 'marginTop'))  || 0;
+	}
+
+	// Safari and Opera do not add the border for the element
+	if ( options.border && (jQuery.browser.safari || jQuery.browser.opera) ) {
+		x += parseInt(jQuery.css(elem, 'borderLeftWidth')) || 0;
+		y += parseInt(jQuery.css(elem, 'borderTopWidth'))  || 0;
+	} else if ( !options.border && !(jQuery.browser.safari || jQuery.browser.opera) ) {
+		x -= parseInt(jQuery.css(elem, 'borderLeftWidth')) || 0;
+		y -= parseInt(jQuery.css(elem, 'borderTopWidth'))  || 0;
+	}
+
+	if ( options.padding ) {
+		x += parseInt(jQuery.css(elem, 'paddingLeft')) || 0;
+		y += parseInt(jQuery.css(elem, 'paddingTop'))  || 0;
+	}
+
+	// Opera thinks offset is scroll offset for display: inline elements
+	if (options.scroll && jQuery.browser.opera && jQuery.css(elem, 'display') == 'inline') {
+		sl -= elem.scrollLeft || 0;
+		st -= elem.scrollTop  || 0;
+	}
+
+	var returnValue = options.scroll ? { top: y - st, left: x - sl, scrollTop:  st, scrollLeft: sl }
+	                                 : { top: y, left: x };
+
+	if (returnObject) { jQuery.extend(returnObject, returnValue); return this; }
+	else              { return returnValue; }
+};
\ No newline at end of file
diff --git a/js/mc.js b/js/mc.js
new file mode 100644
index 0000000000000000000000000000000000000000..a9978b4400ce58ea95ca6b7c147dcee408fe1763
--- /dev/null
+++ b/js/mc.js
@@ -0,0 +1,203 @@
+/**
+ * modalContent jQuery Plugin
+ *
+ * @version   0.11
+ * @since     2006-11-28
+ * @copyright Copyright (c) 2006 Glyphix Studio, Inc. http://www.glyphix.com
+ * @author    Gavin M. Roy <gmr@glyphix.com>
+ * @license   MIT http://www.opensource.org/licenses/mit-license.php
+ * @requires  >= jQuery 1.0.3 http://jquery.com/
+ * @requires  dimensions.js http://jquery.com/dev/svn/trunk/plugins/dimensions/dimensions.js?format=raw
+ *
+ * History:
+ *  0.11:
+ *   2006-12-19 patch from Tim Saker <tjsaker@yahoo.com>
+ *    1) Keyboard events are now only permitted on visible elements belonging to the modal layer (child elements). Attempting to place focus on any other page element will cause focus to be transferred back to the first (ordinal) visible child element of the modal layer.
+ *    2) The modal overlay shading now covers the entire height of the document except for a small band at the bottom, which is the height of a scrollbar (a separate thread to be opened on this problem with dimension.js).
+ *    3) I removed the code that disables and reenables the scrollbars.  This is just a suggestion really, realizing it could be one of those little things that causes fellow developers to become unnecessary foes ;=).  Personally, I found it an annoying behaviour to remove a visible scrollbar causing the page elements to shift right upon modal popup, then back after closure. If the intent was to prevent scrolling it doesn't anyway since you can still page down with the keyboard. Maybe it should be a boolean option passed in the function signature?
+ *   2007-01-03 gmr
+ *    1) Updated to set the top of the background div to 0
+ *    2) Add 50px to the background div (ugly hack until dimensions.js returns the proper height
+ *    3) Removed the .focus from the $('#modalContent .focus') selector since that required something with a class of focus.
+ *    4) Created a function for reaize and bound and unbound that so it doesnt clobber other resize functions on unbind
+ *    5) Created a function for binding the .close class and bound/unbound click using it.  Close now will work on any clickable element including a map area.
+ *    6) Renamed animation commands to match jQuery's.
+ *
+ * Call modalContent() on a DOM object and it will make a centered modal box over a div overlay preventing access to the page.
+ * Create an element (anchor/img/etc) with the class "close" in your content to close the modal box on click.
+ */
+
+/**
+ * modalContent
+ * @param content string to display in the content box
+ * @param css obj of css attributes
+ * @param animation (fadeIn, slideDown, show)
+ * @param speed (valid animation speeds slow, medium, fast or # in ms)
+ */
+jQuery.modalContent = function(content, css, animation, speed) {
+
+  // if we already ahve a modalContent, remove it
+  if ( $('#modalBackdrop') ) $('#modalBackdrop').remove();
+  if ( $('#modalContent') ) $('#modalContent').remove();
+
+  // position code lifted from http://www.quirksmode.org/viewport/compatibility.html
+  if (self.pageYOffset) { // all except Explorer
+  var wt = self.pageYOffset;
+  } else if (document.documentElement && document.documentElement.scrollTop) { // Explorer 6 Strict
+    var wt = document.documentElement.scrollTop;
+  } else if (document.body) { // all other Explorers
+    var wt = document.body.scrollTop;
+  }
+
+  // Get our dimensions
+
+  // Get the docHeight and (ugly hack) add 50 pixels to make sure we dont have a *visible* border below our div
+  var docHeight = $(document).outerHeight() + 50;
+  var docWidth = $(document).innerWidth();
+  var winHeight = $(window).height();
+  var winWidth = $(window).innerWidth();
+  if( docHeight < winHeight ) docHeight = winHeight;
+
+  // Create our divs
+  $('body').append('<div id="modalBackdrop" style="z-index: 1000; display: none;"></div><div id="modalContent" style="z-index: 1001; position: absolute;">' + $(content).html() + '</div>');
+
+  // Keyboard and focus event handler ensures focus stays on modal elements only
+  modalEventHandler = function( event ) {
+    target = null;
+    if ( event ) { //Mozilla
+      target = event.target;
+    } else { //IE
+      event = window.event;
+      target = event.srcElement;
+    }
+    if( $(target).filter('*:visible').parents('#modalContent').size() ) {
+      // allow the event only if target is a visible child node of #modalContent
+      return true;
+    }
+    if ( $('#modalContent') ) $('#modalContent').get(0).focus();
+    return false;
+  };
+  $('body').bind( 'focus', modalEventHandler );
+  $('body').bind( 'keypress', modalEventHandler );
+
+  // Create our content div, get the dimensions, and hide it
+  var modalContent = $('#modalContent').css('top','-1000px');
+  var mdcTop = wt + ( winHeight / 2 ) - (  modalContent.outerHeight() / 2);
+  var mdcLeft = ( winWidth / 2 ) - ( modalContent.outerWidth() / 2);
+  $('#modalBackdrop').css('top', 0).css('height', docHeight + 'px').css('width', docWidth + 'px').show();
+  modalContent.css({top: mdcTop + 'px', left: mdcLeft + 'px'}).hide()[animation](speed);
+
+  // Bind a click for closing the modalContent
+  modalContentClose = function(){close(); return false;};
+  $('.close').bind('click', modalContentClose);
+
+  // Close the open modal content and backdrop
+  function close() {
+    // Unbind the events
+    $(window).unbind('resize',  modalContentResize);
+    $('body').unbind( 'focus', modalEventHandler);
+    $('body').unbind( 'keypress', modalEventHandler );
+    $('.close').unbind('click', modalContentClose);
+
+    // Set our animation parameters and use them
+    if ( animation == 'fadeIn' ) animation = 'fadeOut';
+    if ( animation == 'slideDown' ) animation = 'slideUp';
+    if ( animation == 'show' ) animation = 'hide';
+
+    // Close the content
+    modalContent.hide()[animation](speed);
+
+    // Remove the content
+    $('#modalContent').remove();
+    $('#modalBackdrop').remove();
+  };
+
+  // Move and resize the modalBackdrop and modalContent on resize of the window
+   modalContentResize = function(){
+    // Get our heights
+    var docHeight = $(document).outerHeight();
+    var docWidth = $(document).innerWidth();
+    var winHeight = $(window).height();
+    var winWidth = $(window).width();
+    if( docHeight < winHeight ) docHeight = winHeight;
+
+    // Get where we should move content to
+    var modalContent = $('#modalContent');
+    var mdcTop = ( winHeight / 2 ) - (  modalContent.outerHeight() / 2);
+    var mdcLeft = ( winWidth / 2 ) - ( modalContent.outerWidth() / 2);
+
+    // Apply the changes
+    $('#modalBackdrop').css('height', docHeight + 'px').css('width', docWidth + 'px').show();
+    modalContent.css('top', mdcTop + 'px').css('left', mdcLeft + 'px').show();
+  };
+  $(window).bind('resize', modalContentResize);
+
+  $('#modalContent').focus();
+};
+
+/**
+ * jQuery function init
+ */
+jQuery.fn.modalContent = function(css, animation, speed)
+{
+  // If our animation isn't set, make it just show/pop
+  if (!animation) { var animation = 'show'; } else {
+    // If our animation isn't "fadeIn" or "slideDown" then it always is show
+    if ( ( animation != 'fadeIn' ) && ( animation != 'slideDown') ) animation = 'show';
+  }
+
+  if ( !speed ) var speed = 'fast';
+
+  // Build our base attributes and allow them to be overriden
+  css = jQuery.extend({
+    position: 'absolute',
+    left: '0px',
+    margin: '0px',
+    background: '#000',
+    opacity: '.55'
+  }, css);
+
+  // jQuery mojo
+  this.each(function(){
+    $(this).hide();
+    new jQuery.modalContent($(this), css, animation, speed);
+  });
+
+  // return this object
+  return this;
+};
+
+/**
+ * unmodalContent
+ * @param animation (fadeOut, slideUp, show)
+ * @param speed (valid animation speeds slow, medium, fast or # in ms)
+ */
+jQuery.fn.unmodalContent = function(animation, speed)
+{
+  // If our animation isn't set, make it just show/pop
+  if (!animation) { var animation = 'show'; } else {
+    // If our animation isn't "fade" then it always is show
+    if ( ( animation != 'fadeOut' ) && ( animation != 'slideUp') ) animation = 'show';
+  }
+  // Set a speed if we dont have one
+  if ( !speed ) var speed = 'fast';
+
+  // Unbind the events we bound
+  $(window).unbind('resize', modalContentResize);
+  $('body').unbind( 'focus', modalEventHandler);
+  $('body').unbind( 'keypress', modalEventHandler);
+  $('.close').unbind('click', modalContentClose);
+
+  // jQuery magic loop through the instances and run the animations or removal.
+  this.each(function(){
+    if ( animation == 'fade' ) {
+      $('#modalContent').fadeOut(speed,function(){$('#modalBackdrop').fadeOut(speed, function(){$(this).remove();});$(this).remove();});
+    } else {
+      if ( animation == 'slide' ) {
+        $('#modalContent').slideUp(speed,function(){$('#modalBackdrop').slideUp(speed, function(){$(this).remove();});$(this).remove();});
+      } else {
+        $('#modalContent').remove();$('#modalBackdrop').remove();
+      }
+    }
+  });
+};
diff --git a/js/modal.js b/js/modal.js
new file mode 100644
index 0000000000000000000000000000000000000000..a9ad9b507f52ac7136cf8c09702b6ed18f016327
--- /dev/null
+++ b/js/modal.js
@@ -0,0 +1,154 @@
+// $Id$
+/**
+ * @file 
+ *
+ * Implement a modal form.
+ *
+ * @see modal.inc for documentation.
+ *
+ * This javascript relies on the CTools ajax responder.
+ */
+
+// Make sure our objects are defined.
+Drupal.CTools = Drupal.CTools || {};
+Drupal.CTools.Modal = Drupal.CTools.Modal || {};
+
+/**
+ * Display the modal
+ */
+Drupal.CTools.Modal.show = function() {
+  if (!Drupal.CTools.Modal.modal) {
+    Drupal.CTools.Modal.modal = $(Drupal.theme('CToolsModalDialog'));
+  }
+
+  $('span.modal-title', Drupal.CTools.Modal.modal).html(Drupal.t('Loading...'));
+  Drupal.CTools.Modal.modal.modalContent({
+    // @todo this should be elsewhere.
+    opacity: '.40', 
+    background: '#fff'
+  });
+  $('#modalContent .modal-content').html(Drupal.theme('CToolsModalThrobber'));
+};
+
+/**
+ * Hide the modal
+ */
+Drupal.CTools.Modal.dismiss = function() {
+  if (Drupal.CTools.Modal.modal) {
+    Drupal.CTools.Modal.modal.unmodalContent();
+  }
+};
+
+/**
+ * Provide the HTML to create the modal dialog.
+ */
+Drupal.theme.prototype.CToolsModalDialog = function () {
+  var html = ''
+  html += '  <div id="ctools-modal">'
+  html += '    <div class="ctools-modal-content">' // panels-modal-content
+  html += '      <div class="modal-header">';
+  html += '        <a class="close" href="#">';
+  html +=            Drupal.settings.CToolsModal.closeText + Drupal.settings.CToolsModal.closeImage;
+  html += '        </a>';
+  html += '        <span id="modal-title" class="modal-title">&nbsp;</span>';
+  html += '      </div>';
+  html += '      <div id="modal-content" class="modal-content">';
+  html += '      </div>';
+  html += '    </div>';
+  html += '  </div>';
+
+  return html;
+}
+
+/**
+ * Provide the HTML to create the throbber.
+ */
+Drupal.theme.prototype.CToolsModalThrobber = function () {
+  var html = '';
+  html += '  <div id="modal-throbber">';
+  html += '    <div class="modal-throbber-wrapper">';
+  html +=        Drupal.settings.CToolsModal.throbber;
+  html += '    </div>';
+  html += '  </div>';
+
+  return html;
+};
+
+/**
+ * Generic replacement click handler to open the modal with the destination
+ * specified by the href of the link.
+ */
+Drupal.CTools.Modal.clickAjaxLink = function() {
+  // show the empty dialog right away.
+  Drupal.CTools.Modal.show();
+  return Drupal.CTools.AJAX.clickAJAXLink.apply(this);
+};
+
+/**
+ * Generic replacement click handler to open the modal with the destination
+ * specified by the href of the link.
+ */
+Drupal.CTools.Modal.clickAjaxButton = function() {
+  Drupal.CTools.Modal.show();
+  return Drupal.CTools.AJAX.clickAJAXButton.apply(this);
+};
+
+/**
+ * Submit responder to do an AJAX submit on all modal forms.
+ */
+Drupal.CTools.Modal.submitAjaxForm = function() {
+  url = $(this).attr('action');
+  url.replace('/nojs/', '/ajax/');
+  $(this).ajaxSubmit({
+    type: "POST",
+    url: url,
+    data: '',
+    global: true,
+    success: Drupal.CTools.AJAX.respond,
+    error: function() { 
+      alert("An error occurred while attempting to process " + url); 
+    },
+    dataType: 'json'
+  });
+  return false;
+}
+/**
+ * Bind links that will open modals to the appropriate function.
+ */
+Drupal.behaviors.CToolsModal = function(context) {
+  // Bind links
+  $('a.ctools-use-modal:not(.ctools-use-modal-processed)', context)
+    .addClass('ctools-use-modal-processed')
+    .click(Drupal.CTools.Modal.clickAjaxLink);
+
+  // Bind buttons
+  $('input.ctools-use-modal:not(.ctools-use-modal-processed)', context)
+    .addClass('ctools-use-modal-processed')
+    .click(Drupal.CTools.Modal.clickAjaxButton);
+
+  if ($(context).attr('id') == 'modal-content') {
+    console.log($('form', context));
+    // Bind submit links in the modal form.
+    $('form', context)
+      .addClass('ctools-use-modal-processed')
+      .submit(Drupal.CTools.Modal.submitAjaxForm);
+  }
+};
+
+// The following are implementations of AJAX responder commands.
+
+/**
+ * AJAX responder command to place HTML within the modal.
+ */
+Drupal.CTools.AJAX.commands.modal_display = function(command) {
+  $('#modal-title').html(command.title);
+  $('#modal-content').html(command.output);
+  Drupal.attachBehaviors($('#modal-content'));
+}
+
+/**
+ * AJAX responder command to dismiss the modal.
+ */
+Drupal.CTools.AJAX.commands.modal_dismiss = function(command) {
+  Drupal.CTools.Modal.dismiss();
+}
diff --git a/plugins/arguments/nid.inc b/plugins/arguments/nid.inc
new file mode 100644
index 0000000000000000000000000000000000000000..1036390c3b5c20ee97b77ecc31e897ccbee0f26e
--- /dev/null
+++ b/plugins/arguments/nid.inc
@@ -0,0 +1,89 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ *
+ * Plugin to provide an argument handler for a node id
+ */
+
+/**
+ * Implementation of specially named hook_ctools_arguments().
+ */
+function ctools_nid_ctools_arguments() {
+  $args['nid'] = array(
+    'title' => t("Node ID"),
+    'keyword' => 'node',
+    'description' => t('Restricts the argument to a node id.'),
+    'context' => 'ctools_nid_context',
+    'settings form' => 'ctools_nid_settings_form',
+    'settings form submit' => 'ctools_nid_settings_form_submit',
+  );
+  return $args;
+}
+
+/**
+ * Discover if this argument gives us the node we crave.
+ */
+function ctools_nid_context($node = NULL, $conf = NULL, $empty = FALSE) {
+  // If unset it wants a generic, unfilled context.
+  if ($empty) {
+    return ctools_context_create_empty('node');
+  }
+
+  if (!is_object($node)) {
+    return NULL;
+  }
+
+  if (array_filter($conf['types']) && empty($conf['types'][$node->type])) {
+    return NULL;
+  }
+
+  return ctools_context_create('node', $node);
+}
+
+/**
+ * Settings form for the argument
+ */
+function ctools_nid_settings_form($conf) {
+  if (empty($conf)) {
+    $conf = array(
+      'types' => array(),
+      'own_default' => FALSE,
+      'displays' => array(),
+    );
+  }
+
+  $options = array();
+  foreach (node_get_types() as $type => $info) {
+    $options[$type] = $info->name;
+  }
+
+  $form['types'] = array(
+    '#title' => t('Node types'),
+    '#type' => 'checkboxes',
+    '#options' => $options,
+    '#description' => t('You can restrict this argument to use the checked node types. Arguments from non-conforming node types will be ignored, and ctools will behave as if no argument were given. Leave all unchecked to impose no restriction.'),
+    '#default_value' => $conf['types'],
+    '#prefix' => '<div class="clear-block">',
+    '#suffix' => '</div>',
+  );
+
+  return $form;
+}
+
+/**
+ * There appears to be a bit of a bug with the way we're handling forms; it causes
+ * 'checkboxes' to get invalid values added to them when empty. This takes care
+ * of that.
+ */
+function ctools_nid_settings_form_submit(&$values) {
+  $types = node_get_types();
+  if (!empty($values['types'])) {
+    foreach ($values['types'] as $type => $value) {
+      if (empty($types[$type])) {
+        unset($values['types'][$type]);
+      }
+    }
+  }
+}
diff --git a/plugins/arguments/node_add.inc b/plugins/arguments/node_add.inc
new file mode 100644
index 0000000000000000000000000000000000000000..0e04aece740c5d0d9d2ed6d4d9b45a95d0fb5e82
--- /dev/null
+++ b/plugins/arguments/node_add.inc
@@ -0,0 +1,77 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ *
+ * Plugin to provide an argument handler for a Node add form
+ */
+
+/**
+ * Implementation of specially named hook_ctools_arguments().
+ */
+function ctools_node_add_ctools_arguments() {
+  $args['node_add'] = array(
+    'title' => t("Node add form"),
+    // keyword to use for %substitution
+    'keyword' => 'node_type',
+    'description' => t('Displays the node add form for a content type.'),
+    'context' => 'ctools_node_add_context',
+    'settings form' => 'ctools_node_add_settings_form',
+    'settings form submit' => 'ctools_node_add_settings_form_submit',
+  );
+  return $args;
+}
+
+/**
+ * Discover if this argument gives us the node we crave.
+ */
+function ctools_node_add_context($arg = NULL, $conf = NULL, $empty = FALSE) {
+  // If unset it wants a generic, unfilled context.
+  if (!isset($arg)) {
+    return ctools_context_create_empty('node_add_form');
+  }
+
+  if (array_filter($conf['types']) && empty($conf['types'][$arg])) {
+    return NULL;
+  }
+
+  return ctools_context_create('node_add_form', $arg);
+}
+
+/**
+ * Settings form for the argument
+ */
+function ctools_node_add_settings_form($conf) {
+  $options = array();
+  foreach (node_get_types() as $type => $info) {
+    $options[$type] = $info->name;
+  }
+  $form['types'] = array(
+    '#title' => t('Node types'),
+    '#type' => 'checkboxes',
+    '#options' => $options,
+    '#description' => t('You can restrict this argument to use the checked node types. Arguments from non-conforming node types will be ignored, and ctools will behave as if no argument were given. Leave all unchecked to impose no restriction.'),
+    '#default_value' => $conf['types'],
+    '#prefix' => '<div class="clear-block">',
+    '#suffix' => '</div>',
+  );
+
+  return $form;
+}
+
+/**
+ * There appears to be a bit of a bug with the way we're handling forms; it causes
+ * 'checkboxes' to get invalid values added to them when empty. This takes care
+ * of that.
+ */
+function ctools_node_add_settings_form_submit(&$values) {
+  $types = node_get_types();
+  if (!empty($values['types'])) {
+    foreach ($values['types'] as $type => $value) {
+      if (empty($types[$type])) {
+        unset($values['types'][$type]);
+      }
+    }
+  }
+}
diff --git a/plugins/arguments/node_edit.inc b/plugins/arguments/node_edit.inc
new file mode 100644
index 0000000000000000000000000000000000000000..87562e73e09359354feabf21391327c0d134f079
--- /dev/null
+++ b/plugins/arguments/node_edit.inc
@@ -0,0 +1,87 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ *
+ * Plugin to provide an argument handler for a Node edit form
+ */
+
+/**
+ * Implementation of specially named hook_ctools_arguments().
+ */
+function ctools_node_edit_ctools_arguments() {
+  $args['node_edit'] = array(
+    'title' => t("Node edit form"),
+    // keyword to use for %substitution
+    'keyword' => 'node',
+    'description' => t('Displays the node edit form for a node.'),
+    'context' => 'ctools_node_edit_context',
+    'settings form' => 'ctools_node_edit_settings_form',
+    'settings form submit' => 'ctools_node_edit_settings_form_submit',
+  );
+  return $args;
+}
+
+/**
+ * Discover if this argument gives us the node we crave.
+ */
+function ctools_node_edit_context($arg = NULL, $conf = NULL, $empty = FALSE) {
+  // If unset it wants a generic, unfilled context.
+  if ($empty) {
+    return ctools_context_create_empty('node_edit_form');
+  }
+
+  if (!is_numeric($arg)) {
+    return NULL;
+  }
+
+  $node = node_load($arg);
+  if (!$node) {
+    return NULL;
+  }
+
+  if (array_filter($conf['types']) && empty($conf['types'][$node->type])) {
+    return NULL;
+  }
+
+  // This will perform a node_access check, so we don't have to.
+  return ctools_context_create('node_edit_form', $node);
+}
+
+/**
+ * Settings form for the argument
+ */
+function ctools_node_edit_settings_form($conf) {
+  $options = array();
+  foreach (node_get_types() as $type => $info) {
+    $options[$type] = $info->name;
+  }
+  $form['types'] = array(
+    '#title' => t('Node types'),
+    '#type' => 'checkboxes',
+    '#options' => $options,
+    '#description' => t('You can restrict this argument to use the checked node types. Arguments from non-conforming node types will be ignored, and ctools will behave as if no argument were given. Leave all unchecked to impose no restriction.'),
+    '#default_value' => $conf['types'],
+    '#prefix' => '<div class="clear-block">',
+    '#suffix' => '</div>',
+  );
+
+  return $form;
+}
+
+/**
+ * There appears to be a bit of a bug with the way we're handling forms; it causes
+ * 'checkboxes' to get invalid values added to them when empty. This takes care
+ * of that.
+ */
+function ctools_node_edit_settings_form_submit(&$values) {
+  $types = node_get_types();
+  if (!empty($values['types'])) {
+    foreach ($values['types'] as $type => $value) {
+      if (empty($types[$type])) {
+        unset($values['types'][$type]);
+      }
+    }
+  }
+}
diff --git a/plugins/arguments/term.inc b/plugins/arguments/term.inc
new file mode 100644
index 0000000000000000000000000000000000000000..d4d633a782d22a2c28ea0dd795acd694a4c7a253
--- /dev/null
+++ b/plugins/arguments/term.inc
@@ -0,0 +1,126 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ *
+ * Plugin to provide an argument handler for a Taxonomy term
+ */
+
+/**
+ * Implementation of specially named hook_ctools_arguments().
+ */
+function ctools_term_ctools_arguments() {
+  $args['term'] = array(
+    'title' => t("Taxonomy term"),
+    // keyword to use for %substitution
+    'keyword' => 'term',
+    'description' => t('Restricts the argument to a taxonomy term.'),
+    'context' => 'ctools_term_context',
+    'settings form' => 'ctools_term_settings_form',
+    'settings form submit' => 'ctools_term_settings_form_submit',
+  );
+  return $args;
+}
+
+/**
+ * Discover if this argument gives us the term we crave.
+ */
+function ctools_term_context($arg = NULL, $conf = NULL, $empty = FALSE) {
+  // If unset it wants a generic, unfilled context.
+  if ($empty) {
+    return ctools_context_create_empty('term');
+  }
+
+  switch ($conf['input_form']) {
+    case 'tid':
+    default:
+      if (!is_numeric($arg)) {
+        return FALSE;
+      }
+      $term = taxonomy_get_term($arg);
+      break;
+
+    case 'term':
+      $terms = taxonomy_get_term_by_name($arg);
+      if (count($terms) != 1) {
+        foreach ($terms as $potential) {
+          foreach ($conf['vids'] as $vid => $active) {
+            if ($active == 1 && $potential->vid == $vid) {
+              $term = $potential;
+              // break out of the foreaches AND the case
+              break 3;
+            }
+          }
+        }
+      }
+      $term = array_shift($terms);
+      break;
+  }
+
+  if (empty($term)) {
+    return NULL;
+  }
+
+  if (!empty($conf['vids']) && array_filter($conf['vids']) && empty($conf['vids'][$term->vid])) {
+    return NULL;
+  }
+
+  $context = ctools_context_create('term', $term);
+  $context->original_argument = $arg;
+  return $context;
+}
+
+/**
+ * Settings form for the argument
+ */
+function ctools_term_settings_form($conf) {
+  if (empty($conf)) {
+    $conf = array(
+      'input_form' => 'tid',
+    );
+  }
+
+  $options = array();
+  foreach (taxonomy_get_vocabularies() as $vid => $vocabulary) {
+    $options[$vid] = $vocabulary->name;
+  }
+
+  $form['input_form'] = array(
+    '#title' => t('Argument type'),
+    '#type' => 'radios',
+    '#options' => array('tid' => t('Term ID'), 'term' => t('Term name')),
+    '#default_value' => $conf['input_form'],
+    '#prefix' => '<div class="clear-block">',
+    '#suffix' => '</div>',
+  );
+
+
+  $form['vids'] = array(
+    '#title' => t('Vocabularies'),
+    '#type' => 'checkboxes',
+    '#options' => $options,
+    '#description' => t('You can restrict this argument to use the checked vocabularies. Arguments from non-conforming vocabularies will be ignored, and ctools will behave as if no argument were given. Leave all unchecked to impose no restriction.'),
+    '#default_value' => $conf['vids'],
+    '#prefix' => '<div class="clear-block">',
+    '#suffix' => '</div>',
+  );
+
+  return $form;
+}
+
+/**
+ * There appears to be a bit of a bug with the way we're handling forms; it causes
+ * 'checkboxes' to get invalid values added to them when empty. This takes care
+ * of that.
+ */
+function ctools_term_settings_form_submit(&$values) {
+  $vocs = taxonomy_get_vocabularies();
+  if (!empty($values['vids'])) {
+    foreach ($values['vids'] as $vid => $value) {
+      if (empty($vocs[$vid])) {
+        unset($values['vids'][$vid]);
+      }
+    }
+  }
+}
diff --git a/plugins/arguments/uid.inc b/plugins/arguments/uid.inc
new file mode 100644
index 0000000000000000000000000000000000000000..dcd7b890cb9db455331aab530d35b1ab9ad26bab
--- /dev/null
+++ b/plugins/arguments/uid.inc
@@ -0,0 +1,43 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ *
+ * Plugin to provide an argument handler for a user id
+ */
+
+/**
+ * Implementation of specially named hook_ctools_arguments().
+ */
+function ctools_uid_ctools_arguments() {
+  $args['uid'] = array(
+    'title' => t("User ID"),
+    // keyword to use for %substitution
+    'keyword' => 'user',
+    'description' => t('Creates a user object from the argument.'),
+    'context' => 'ctools_uid_context',
+  );
+  return $args;
+}
+
+/**
+ * Discover if this argument gives us the user we crave.
+ */
+function ctools_uid_context($arg = NULL, $conf = NULL, $empty = FALSE) {
+  // If unset it wants a generic, unfilled context.
+  if ($empty) {
+    return ctools_context_create_empty('user');
+  }
+
+  if (!is_numeric($arg)) {
+    return NULL;
+  }
+
+  $account = user_load(array('uid' => $arg));
+  if (!$account) {
+    return NULL;
+  }
+
+  return ctools_context_create('user', $account);
+}
diff --git a/plugins/arguments/vid.inc b/plugins/arguments/vid.inc
new file mode 100644
index 0000000000000000000000000000000000000000..0a96daf0030ffff926779098fe2eaef2aafe9bb5
--- /dev/null
+++ b/plugins/arguments/vid.inc
@@ -0,0 +1,43 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ *
+ * Plugin to provide an argument handler for a vocabulary id
+ */
+
+/**
+ * Implementation of specially named hook_ctools_arguments().
+ */
+function ctools_vid_ctools_arguments() {
+  $args['vid'] = array(
+    'title' => t("Vocabulary ID"),
+    // keyword to use for %substitution
+    'keyword' => 'vocabulary',
+    'description' => t('Loads a vocabulary object from the argument.'),
+    'context' => 'ctools_vid_context',
+  );
+  return $args;
+}
+
+/**
+ * Discover if this argument gives us the vocabulary we crave.
+ */
+function ctools_vid_context($arg = NULL, $conf = NULL, $empty = FALSE) {
+  // If unset it wants a generic, unfilled context.
+  if ($empty) {
+    return ctools_context_create_empty('vocabulary');
+  }
+
+  if (!is_numeric($arg)) {
+    return NULL;
+  }
+
+  $vocabulary = taxonomy_vocabulary_load($arg);
+  if (!$vocabulary) {
+    return NULL;
+  }
+
+  return ctools_context_create('vocabulary', $vocabulary);
+}
diff --git a/plugins/contexts/node.inc b/plugins/contexts/node.inc
new file mode 100644
index 0000000000000000000000000000000000000000..758d8b8445d5e140a5dea9ca7a8aae2101775af7
--- /dev/null
+++ b/plugins/contexts/node.inc
@@ -0,0 +1,175 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ *
+ * Plugin to provide a node context. A node context is a node wrapped in a
+ * context object that can be utilized by anything that accepts contexts.
+ */
+
+/**
+ * Implementation of specially named hook_ctools_contexts().
+ */
+function ctools_node_ctools_contexts() {
+  $args['node'] = array(
+    'title' => t("Node"),
+    'description' => t('A node object.'),
+    'context' => 'ctools_context_create_node',
+    'settings form' => 'ctools_context_node_settings_form',
+    'settings form validate' => 'ctools_context_node_settings_form_validate',
+    'settings form submit' => 'ctools_context_node_settings_form_submit',
+    'keyword' => 'node',
+    'context name' => 'node',
+  );
+  return $args;
+}
+
+/**
+ * It's important to remember that $conf is optional here, because contexts
+ * are not always created from the UI.
+ */
+function ctools_context_create_node($empty, $data = NULL, $conf = FALSE) {
+  $types = array('node');
+  // FIXME this is broken when called from ctools_page's render process, through ctools_context_load_contexts(); it passes in $conf as TRUE.
+  if (!empty($conf['types'])) {
+    foreach ($conf['types'] as $type) {
+      if ($type) {
+        $types[] = 'node-' . $type;
+      }
+    }
+  }
+  $context = new ctools_context($types);
+  $context->plugin = 'node';
+
+  if ($empty) {
+    return $context;
+  }
+
+  if ($conf) {
+    $nid = is_array($data) && isset($data['nid']) ? $data['nid'] : (is_object($data) ? $data->nid : 0);
+
+    if (module_exists('translation')) {
+       if ($translation = module_invoke('translation', 'node_nid', $nid, $GLOBALS['language']->language)) {
+        $nid = $translation;
+        $reload = TRUE;
+      }
+    }
+
+    if (is_array($data) || !empty($reload)) {
+      $data = node_load($nid);
+    }
+  }
+
+  if (!empty($data)) {
+    $context->data     = $data;
+    $context->title    = $data->title;
+    $context->argument = $data->nid;
+    if (is_array($context->type)) {
+      $context->type[] = 'node-' . $data->type;
+    }
+    else {
+      $context->type = array($context->type, 'node-' . $data->type);
+    }
+    return $context;
+  }
+}
+
+function ctools_context_node_settings_form($conf, $external = FALSE) {
+  if (empty($conf)) {
+    $conf = array(
+      'nid' => '',
+    );
+  }
+
+  if ($external) {
+    $form['external'] = array(
+      '#type' => 'checkbox',
+      '#default_value' => $conf['external'],
+      '#title' => t('Require this context from an external source (such as a containing panel page).'),
+      '#description' => t('If selected, node selection (below) will be ignored.'),
+    );
+  }
+
+  $form['node'] = array(
+    '#prefix' => '<div class="no-float">',
+    '#suffix' => '</div>',
+    '#title' => t('Enter the title or NID of a post'),
+    '#type' => 'textfield',
+    '#maxlength' => 512,
+    '#autocomplete_path' => 'ctools/node/autocomplete',
+    '#weight' => -10,
+  );
+
+  if (!empty($conf['nid'])) {
+    $info = db_fetch_object(db_query("SELECT * FROM {node} n WHERE n.nid = %d", $conf['nid']));
+    if ($info) {
+      $link = l(t("'%title' [node id %nid]", array('%title' => $info->title, '%nid' => $info->nid)), "node/$info->nid", array('target' => '_blank', 'title' => t('Open in new window')));
+      $form['node']['#description'] = t('Currently set to !link', array('!link' => $link));
+    }
+  }
+
+  $form['nid'] = array(
+    '#type' => 'value',
+    '#value' => $conf['nid'],
+  );
+
+  $form['set_identifier'] = array(
+    '#type' => 'checkbox',
+    '#default_value' => FALSE,
+    '#title' => t('Reset identifier to node title'),
+    '#description' => t('If checked, the identifier will be reset to the node title of the selected node.'),
+  );
+
+  return $form;
+}
+
+/**
+ * Validate a node.
+ */
+function ctools_context_node_settings_form_validate($form, &$form_values, &$form_state) {
+  // Validate the autocomplete
+  if (empty($form_values['external']) && empty($form_values['nid']) && empty($form_values['node'])) {
+    form_error($form['node'], t('You must select a node.'));
+    return;
+  }
+
+  if (empty($form_values['node'])) {
+    return;
+  }
+
+  $nid = $form_values['node'];
+  $preg_matches = array();
+  $match = preg_match('/\[nid: (\d+)\]/', $nid, $preg_matches);
+  if (!$match) {
+    $match = preg_match('/^nid: (\d+)/', $nid, $preg_matches);
+  }
+
+  if ($match) {
+    $nid = $preg_matches[1];
+  }
+  if (is_numeric($nid)) {
+    $node = db_fetch_object(db_query(db_rewrite_sql("SELECT n.nid FROM {node} n WHERE n.nid = %d"), $nid));
+  }
+  else {
+    $node = db_fetch_object(db_query(db_rewrite_sql("SELECT n.nid FROM {node} n WHERE LOWER(n.title) = LOWER('%s')"), $nid));
+  }
+
+  if (!$node) {
+    form_error($form['node'], t('Invalid node selected.'));
+  }
+  else {
+    form_set_value($form['nid'], $node->nid, $form_state);
+    // $form_values['nid'] = $node->nid;
+  }
+}
+
+function ctools_context_node_settings_form_submit($form, &$form_values, &$form_state) {
+  if ($form_values['set_identifier']) {
+    $node = node_load($form_values['nid']);
+    $form_state['values']['context']['identifier'] = $node->title;
+  }
+
+  // Don't let this be stored.
+  unset($form_values['set_identifier']);
+}
diff --git a/plugins/contexts/node_add_form.inc b/plugins/contexts/node_add_form.inc
new file mode 100644
index 0000000000000000000000000000000000000000..63c6f6f8b94aa294579bf344533b3738c7f05e73
--- /dev/null
+++ b/plugins/contexts/node_add_form.inc
@@ -0,0 +1,92 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ *
+ * Plugin to provide a node_add_form context
+ */
+
+/**
+ * Implementation of specially named hook_ctools_contexts().
+ */
+function ctools_node_add_form_ctools_contexts() {
+  $args['node_add_form'] = array(
+    'title' => t("Node add form"),
+    'description' => t('A node add form.'),
+    'context' => 'ctools_context_create_node_add_form',
+    'settings form' => 'ctools_context_node_add_form_settings_form',
+    'keyword' => 'node_add',
+    'context name' => 'node_add_form',
+  );
+  return $args;
+}
+
+/**
+ * It's important to remember that $conf is optional here, because contexts
+ * are not always created from the UI.
+ */
+function ctools_context_create_node_add_form($empty, $data = NULL, $conf = FALSE) {
+  $context = new ctools_context(array('form', 'node_add', 'node_form'));
+  $context->plugin = 'node_add_form';
+
+  if ($empty) {
+    return $context;
+  }
+
+  if ($conf) {
+    $data = $data['type'];
+  }
+
+  if (!empty($data)) {
+    $types = node_get_types();
+    $type = str_replace('-', '_', $data);
+
+    // Validate the node type exists.
+    if (isset($types[$type]) && node_access('create', $type)) {
+      // Initialize settings:
+      global $user;
+      $node = array('uid' => $user->uid, 'name' => $user->name, 'type' => $type);
+
+      $form = drupal_retrieve_form($type . '_node_form', $node);
+      drupal_process_form($type . '_node_form', $form);
+      // In a form, $data is the object being edited.
+      $context->data     = $type;
+      $context->title    = $types[$type]->name;
+      $context->argument = $type;
+
+      // These are specific pieces of data to this form.
+      // All forms should place the form here.
+      $context->form       = $form;
+      $context->form_id    = $type . '_node_form';
+      $context->form_title = t('Submit @name', array('@name' => $types[$type]->name));
+      $context->node_type  = $type;
+      return $context;
+    }
+  }
+}
+
+function ctools_context_node_add_form_settings_form($conf, $external = FALSE) {
+  if ($external) {
+    $options[0] = t('External source');
+  }
+
+  foreach (node_get_types() as $type => $info) {
+    $options[$type] = $info->name;
+  }
+
+  $form['types'] = array(
+    '#title' => t('Node type'),
+    '#type' => 'select',
+    '#options' => $options,
+    '#default_value' => $conf['types'],
+    '#description' => t('Select the node type for this form.'),
+  );
+
+  if ($external) {
+    $form['types']['#description'] .= ' ' . t('Select external to require this from an external source (such as a containing panel page).');
+  }
+
+  return $form;
+}
+
diff --git a/plugins/contexts/node_edit_form.inc b/plugins/contexts/node_edit_form.inc
new file mode 100644
index 0000000000000000000000000000000000000000..37ec931770a52deb6d074cf05906de91bcd58d2c
--- /dev/null
+++ b/plugins/contexts/node_edit_form.inc
@@ -0,0 +1,138 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ *
+ * Plugin to provide a node_edit_form context
+ */
+
+/**
+ * Implementation of specially named hook_ctools_contexts().
+ */
+function ctools_node_edit_form_ctools_contexts() {
+  $args['node_edit_form'] = array(
+    'title' => t("Node edit form"),
+    'description' => t('A node edit form.'),
+    'context' => 'ctools_context_create_node_edit_form',
+    'settings form' => 'ctools_context_node_edit_form_settings_form',
+    'settings form validate' => 'ctools_context_node_edit_form_settings_form_validate',
+    'keyword' => 'node_edit',
+    'context name' => 'node_edit_form',
+  );
+  return $args;
+}
+
+/**
+ * It's important to remember that $conf is optional here, because contexts
+ * are not always created from the UI.
+ */
+function ctools_context_create_node_edit_form($empty, $node = NULL, $conf = FALSE) {
+  $context = new ctools_context(array('form', 'node_edit', 'node_form', 'node'));
+  $context->plugin = 'node_edit_form';
+
+  if ($empty) {
+    return $context;
+  }
+
+  if ($conf) {
+    // In this case, $node is actually our $conf array.
+    $node = node_load($node['nid']);
+  }
+
+  if (!empty($node) && node_access('update', $node)) {
+    // This is from node_edit_page cause Drupal still doesn't use fapi right.
+    if ($_POST['op'] == t('Delete')) {
+      // Note: we redirect from node/nid/edit to node/nid/delete to make the tabs disappear.
+      if ($_REQUEST['destination']) {
+        $destination = drupal_get_destination();
+        unset($_REQUEST['destination']);
+      }
+      drupal_goto('node/'. $node->nid .'/delete', $destination);
+    }
+
+    $form = drupal_retrieve_form($node->type . '_node_form', $node);
+    drupal_process_form($node->type . '_node_form', $form);
+    // Fill in the 'node' portion of the context
+    $context->data     = $node;
+    $context->title    = $node->title;
+    $context->argument = $node->nid;
+
+    $context->form       = $form;
+    $context->form_id    = $node->type . '_node_form';
+    $context->form_title = $node->title;
+    $context->node_type  = $node->type;
+    return $context;
+  }
+}
+
+function ctools_context_node_edit_form_settings_form($conf, $external = FALSE) {
+  if ($external) {
+    $form['external'] = array(
+      '#type' => 'checkbox',
+      '#default_value' => $conf['external'],
+      '#title' => t('Require this context from an external source (such as a containing panel page).'),
+      '#description' => t('If selected, node selection (below) will be ignored.'),
+    );
+  }
+
+  $form['node'] = array(
+    '#prefix' => '<div class="no-float">',
+    '#suffix' => '</div>',
+    '#title' => t('Enter the title or NID of a post'),
+    '#type' => 'textfield',
+    '#maxlength' => 512,
+    '#autocomplete_path' => 'ctools/node/autocomplete',
+    '#weight' => -10,
+  );
+
+  if (!empty($conf['nid'])) {
+    $info = db_fetch_object(db_query("SELECT * FROM {node} WHERE nid = %d", $conf['nid']));
+    if ($info) {
+      $link = l(t("'%title' [node id %nid]", array('%title' => $info->title, '%nid' => $info->nid)), "node/$info->nid", array('target' => '_blank', 'title' => t('Open in new window')));
+      $form['node']['#description'] = t('Currently set to !link', array('!link' => $link));
+    }
+  }
+
+  $form['nid'] = array(
+    '#type' => 'value',
+    '#value' => $conf['nid'],
+  );
+
+  $form['external'] = array(
+    '#type' => 'value',
+    '#value' => $external,
+  );
+
+  return $form;
+}
+
+/**
+ * Validate a node.
+ */
+function ctools_context_node_edit_form_settings_form_validate($form, &$form_values, &$form_state) {
+  if (empty($form_values['external']) && empty($form_values['nid']) && empty($form_values['node'])) {
+    form_error($form['node'], t('You must select a node.'));
+    return;
+  }
+
+  if (empty($form_values['node'])) {
+    return;
+  }
+
+  $nid = $form_values['node'];
+  if (is_numeric($nid)) {
+    $node = db_fetch_object(db_query(db_rewrite_sql("SELECT n.nid FROM {node} n WHERE n.nid = %d"), $nid));
+  }
+  else {
+    $node = db_fetch_object(db_query(db_rewrite_sql("SELECT n.nid FROM {node} n WHERE LOWER(n.title) = LOWER('%s')"), $nid));
+    if ($node) {
+      form_set_value($form['nid'], $node->nid, $form_state);
+    }
+  }
+
+  if (!$node) {
+    form_error($form['node'], t('Invalid node selected.'));
+  }
+}
+
diff --git a/plugins/contexts/term.inc b/plugins/contexts/term.inc
new file mode 100644
index 0000000000000000000000000000000000000000..d8c601586a3e5919ed86478d565735690a345c0f
--- /dev/null
+++ b/plugins/contexts/term.inc
@@ -0,0 +1,69 @@
+<?php
+// $Id$
+
+
+/**
+ * @file
+ *
+ * Plugin to provide a term context
+ */
+
+/**
+ * Implementation of specially named hook_ctools_contexts().
+ */
+function ctools_term_ctools_contexts() {
+  $args['term'] = array(
+    'title' => t("Taxonomy term"),
+    'description' => t('A single taxonomy term object.'),
+    'context' => 'ctools_context_create_term',
+    'settings form' => 'ctools_context_term_settings_form',
+    'settings form validate' => 'ctools_context_term_settings_form_validate',
+    'keyword' => 'term',
+    'no ui' => TRUE,
+    'context name' => 'term',
+  );
+  return $args;
+}
+
+/**
+ * It's important to remember that $conf is optional here, because contexts
+ * are not always created from the UI.
+ */
+function ctools_context_create_term($empty, $data = NULL, $conf = FALSE) {
+  $context = new ctools_context('term');
+  $context->plugin = 'term';
+
+  if ($empty) {
+    return $context;
+  }
+
+  if ($conf) {
+    $data = taxonomy_get_term($data['tid']);
+  }
+
+  if (!empty($data)) {
+    $context->data     = $data;
+    $context->title    = $data->name;
+    $context->argument = $data->tid;
+    return $context;
+  }
+}
+
+function ctools_context_term_settings_form($conf, $external = FALSE) {
+  $form = array();
+  if ($external) {
+    $form['external'] = array(
+      '#type' => 'checkbox',
+      '#default_value' => $conf['external'],
+      '#title' => t('Require this context from an external source (such as a containing panel page).'),
+      '#description' => t('If selected, term selection (below) will be ignored'),
+    );
+  }
+  return $form;
+}
+
+/**
+ * Validate a term.
+ */
+function ctools_context_term_settings_form_validate($form, &$form_values, &$form_state) {
+}
diff --git a/plugins/contexts/terms.inc b/plugins/contexts/terms.inc
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/plugins/contexts/user.inc b/plugins/contexts/user.inc
new file mode 100644
index 0000000000000000000000000000000000000000..6d2e16f801b1425a8108a6ea13b7902730d1b7bc
--- /dev/null
+++ b/plugins/contexts/user.inc
@@ -0,0 +1,55 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ *
+ * Plugin to provide a user context
+ */
+
+/**
+ * Implementation of specially named hook_ctools_contexts().
+ */
+function ctools_user_ctools_contexts() {
+  $args['user'] = array(
+    'title' => t("User"),
+    'description' => t('A single user object.'),
+    'context' => 'ctools_context_create_user',
+    'settings form' => 'ctools_context_user_settings_form',
+    'settings form validate' => 'ctools_context_user_settings_form_validate',
+    'keyword' => 'user',
+    'no ui' => TRUE,
+    'context name' => 'user',
+  );
+  return $args;
+}
+
+/**
+ * It's important to remember that $conf is optional here, because contexts
+ * are not always created from the UI.
+ */
+function ctools_context_create_user($empty, $data = NULL, $conf = FALSE) {
+  $context = new ctools_context('user');
+  $context->plugin = 'user';
+
+  if ($empty) {
+    return $context;
+  }
+
+  if ($conf) {
+    $data = user_load(array('uid' => $data['uid']));
+  }
+
+  if (!empty($data)) {
+    $context->data     = $data;
+    $context->title    = $data->name;
+    $context->argument = $data->uid;
+    return $context;
+  }
+}
+
+function ctools_context_user_settings_form($conf, $external = FALSE) {
+  $form = array();
+  return $form;
+}
+
diff --git a/plugins/contexts/vocabulary.inc b/plugins/contexts/vocabulary.inc
new file mode 100644
index 0000000000000000000000000000000000000000..23cd96ecfe180ddf6246e407cf276bfc6c4853a6
--- /dev/null
+++ b/plugins/contexts/vocabulary.inc
@@ -0,0 +1,75 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ *
+ * Plugin to provide a vocabulary context
+ */
+
+/**
+ * Implementation of specially named hook_ctools_contexts().
+ */
+function ctools_vocabulary_ctools_contexts() {
+  $args['vocabulary'] = array(
+    'title' => t("Taxonomy vocabulary"),
+    'description' => t('A single taxonomy vocabulary object.'),
+    'context' => 'ctools_context_create_vocabulary',
+    'settings form' => 'ctools_context_vocabulary_settings_form',
+    'settings form validate' => 'ctools_context_vocabulary_settings_form_validate',
+    'keyword' => 'vocabulary',
+    'context name' => 'vocabulary',
+  );
+  return $args;
+}
+
+/**
+ * It's important to remember that $conf is optional here, because contexts
+ * are not always created from the UI.
+ */
+function ctools_context_create_vocabulary($empty, $data = NULL, $conf = FALSE) {
+  $context = new ctools_context('vocabulary');
+  $context->plugin = 'vocabulary';
+
+  if ($empty) {
+    return $context;
+  }
+
+  if ($conf) {
+    $data = taxonomy_vocabulary_load($data['vid']);
+  }
+
+  if (!empty($data)) {
+    $context->data     = $data;
+    $context->title    = $data->name;
+    $context->argument = $data->vid;
+    return $context;
+  }
+}
+
+function ctools_context_vocabulary_settings_form($conf, $external = FALSE) {
+  $options = array();
+  if ($external) {
+    $options[0] = t('External source');
+  }
+
+  foreach (taxonomy_get_vocabularies() as $vid => $vocabulary) {
+    $options[$vid] = $vocabulary->name;
+  }
+
+  $form['vid'] = array(
+    '#title' => t('Vocabulary'),
+    '#type' => 'select',
+    '#options' => $options,
+    '#default_value' => $conf['vids'],
+    '#prefix' => '<div class="clear-block">',
+    '#suffix' => '</div>',
+    '#description' => t('Select the vocabulary for this form.'),
+  );
+  if ($external) {
+    $form['vid']['#description'] .= ' ' . t('Select external to require this from an external source (such as a containing panel page).');
+  }
+
+  return $form;
+}
+
diff --git a/plugins/relationships/book_parent.inc b/plugins/relationships/book_parent.inc
new file mode 100644
index 0000000000000000000000000000000000000000..720e6d1af30e840676cb636dd43d2054d1d3ec43
--- /dev/null
+++ b/plugins/relationships/book_parent.inc
@@ -0,0 +1,71 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ *
+ * Plugin to provide an relationship handler for book parent
+ */
+
+/**
+ * Implementation of specially named hook_ctools_relationships().
+ */
+function ctools_book_parent_ctools_relationships() {
+  $args['book_parent'] = array(
+    'title' => t("Book parent"),
+    'keyword' => 'book_parent',
+    'description' => t('Adds a book parent from a node context.'),
+    'required context' => new ctools_context_required(t('Node'), 'node'),
+    'context' => 'ctools_book_parent_context',
+    'settings form' => 'ctools_book_parent_settings_form',
+    'settings form validate' => 'ctools_book_parent_settings_form_validate',
+  );
+  return $args;
+}
+
+/**
+ * Return a new context based on an existing context
+ */
+function ctools_book_parent_context($context = NULL, $conf) {
+  // If unset it wants a generic, unfilled context, which is just NULL
+  if (empty($context->data)) {
+    return ctools_context_create_empty('node');
+  }
+
+  if (isset($context->data->parent)) {
+    if ($conf['type'] == 'top') {
+      // Search through the book tree to load the top level.
+      $result = array('nid' => $context->data->nid, 'vid' => $context->data->vid, 'parent' => $context->data->parent);
+      while (!empty($result['parent'])) {
+        $result = db_fetch_array(db_query("SELECT * FROM {book} b INNER JOIN {node} n ON b.vid = n.nid WHERE b.nid = %d", $result['parent']));
+      }
+      $nid = $result['nid'];
+    }
+    else {
+      // Just load the parent book.
+      $nid = $context->data->parent;
+    }
+
+    if (!empty($nid)) {
+      // load the node
+      $node = node_load($nid);
+      // Generate the context.
+      return ctools_context_create('node', $node);
+    }
+  }
+}
+
+/**
+ * Settings form for the relationship
+ */
+function ctools_book_parent_settings_form($conf) {
+  $form['type'] = array(
+    '#type' => 'select',
+    '#title' => t('Relationship type'),
+    '#options' => array('parent' => t('Immediate parent'), 'top' => t('Top level book')),
+    '#default_value' => $conf['type'],
+  );
+
+  return $form;
+}
+
diff --git a/plugins/relationships/term_from_node.inc b/plugins/relationships/term_from_node.inc
new file mode 100644
index 0000000000000000000000000000000000000000..bf65227363045c18ee6265d039ac49c2824824ba
--- /dev/null
+++ b/plugins/relationships/term_from_node.inc
@@ -0,0 +1,63 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ *
+ * Plugin to provide an relationship handler for term from node
+ */
+
+/**
+ * Implementation of specially named hook_ctools_relationships().
+ */
+function ctools_term_from_node_ctools_relationships() {
+  $args['term_from_node'] = array(
+    'title' => t("Term from node"),
+    'keyword' => 'term',
+    'description' => t('Adds a taxonomy term from a node context; if multiple terms are selected, this will get the "first" term only.'),
+    'required context' => new ctools_context_required(t('Node'), 'node'),
+    'context' => 'ctools_term_from_node_context',
+    'settings form' => 'ctools_term_from_node_settings_form',
+    'settings form validate' => 'ctools_term_from_node_settings_form_validate',
+  );
+  return $args;
+}
+
+/**
+ * Return a new context based on an existing context
+ */
+function ctools_term_from_node_context($context = NULL, $conf) {
+  // If unset it wants a generic, unfilled context, which is just NULL
+  if (empty($context->data)) {
+    return ctools_context_create_empty('term', NULL);
+  }
+
+  if (isset($context->data->taxonomy)) {
+    foreach ($context->data->taxonomy as $term) {
+      if ($term->vid == $conf['vid']) {
+        return ctools_context_create('term', $term);
+      }
+    }
+  }
+}
+
+/**
+ * Settings form for the relationship
+ */
+function ctools_term_from_node_settings_form($conf) {
+  $options = array();
+  foreach (taxonomy_get_vocabularies() as $vid => $vocabulary) {
+    $options[$vid] = $vocabulary->name;
+  }
+  $form['vid'] = array(
+    '#title' => t('Vocabulary'),
+    '#type' => 'select',
+    '#options' => $options,
+    '#default_value' => $conf['vid'],
+    '#prefix' => '<div class="clear-block">',
+    '#suffix' => '</div>',
+  );
+
+  return $form;
+}
+
diff --git a/plugins/relationships/term_parent.inc b/plugins/relationships/term_parent.inc
new file mode 100644
index 0000000000000000000000000000000000000000..2988d453ab08f72373835a43cdbb359a45687773
--- /dev/null
+++ b/plugins/relationships/term_parent.inc
@@ -0,0 +1,71 @@
+<?php
+// $Id$
+
+/**
+ * @file relationships/term_parent.inc
+ *
+ * Plugin to provide an relationship handler for term parent
+ */
+
+/**
+ * Implementation of specially named hook_ctools_relationships().
+ */
+function ctools_term_parent_ctools_relationships() {
+  $args['term_parent'] = array(
+    'title' => t("Term parent"),
+    'keyword' => 'parent_term',
+    'description' => t('Adds a taxonomy term parent from a term context.'),
+    'required context' => new ctools_context_required(t('Term'), 'term'),
+    'context' => 'ctools_term_parent_context',
+    'settings form' => 'ctools_term_parent_settings_form',
+    'settings form validate' => 'ctools_term_parent_settings_form_validate',
+  );
+  return $args;
+}
+
+/**
+ * Return a new context based on an existing context
+ */
+function ctools_term_parent_context($context = NULL, $conf) {
+  // If unset it wants a generic, unfilled context, which is just NULL
+  if (empty($context->data)) {
+    return ctools_context_create_empty('term');
+  }
+
+  if (isset($context->data)) {
+    $result = db_fetch_array(db_query("SELECT t1.* FROM {term_hierarchy} t1 INNER JOIN {term_hierarchy} t2 ON t1.tid = t2.parent WHERE t2.tid = %d", $context->data->tid));
+
+    // If top level term, keep looking up until we see a top level.
+    if ($conf['type'] == 'top') {
+      // If looking for top level, and there are no parents at all, make sure
+      // the current term is the 'top level'.
+      if (empty($result)) {
+        $result['tid'] = $context->data->tid;
+      }
+      while (!empty($result['parent'])) {
+        $result = db_fetch_array(db_query("SELECT * FROM {term_hierarchy} WHERE tid = %d", $result['parent']));
+      }
+    }
+
+    // load the term
+    if ($result) {
+      $term = taxonomy_get_term($result['tid']);
+      return ctools_context_create('term', $term);
+    }
+  }
+}
+
+/**
+ * Settings form for the relationship
+ */
+function ctools_term_parent_settings_form($conf) {
+  $form['type'] = array(
+    '#type' => 'select',
+    '#title' => t('Relationship type'),
+    '#options' => array('parent' => t('Immediate parent'), 'top' => t('Top level term')),
+    '#default_value' => $conf['type'],
+  );
+
+  return $form;
+}
+
diff --git a/plugins/relationships/user_from_node.inc b/plugins/relationships/user_from_node.inc
new file mode 100644
index 0000000000000000000000000000000000000000..bccfed4304d75405e556c85911c9925ba06b864c
--- /dev/null
+++ b/plugins/relationships/user_from_node.inc
@@ -0,0 +1,44 @@
+<?php
+// $Id$
+
+/**
+ * @file
+ *
+ * Plugin to provide an relationship handler for node from user
+ */
+
+/**
+ * Implementation of specially named hook_ctools_relationships().
+ */
+function ctools_user_from_node_ctools_relationships() {
+  $args['user_from_node'] = array(
+    'title' => t("User from node"),
+    'keyword' => 'user',
+    'description' => t('Creates the author of a node as a user context.'),
+    'required context' => new ctools_context_required(t('Node'), 'node'),
+    'context' => 'ctools_user_from_node_context',
+  );
+  return $args;
+}
+
+/**
+ * Return a new context based on an existing context
+ */
+function ctools_user_from_node_context($context = NULL, $conf) {
+  // If unset it wants a generic, unfilled context, which is just NULL
+  if (empty($context->data)) {
+    return ctools_context_create_empty('user', NULL);
+  }
+  if (isset($context->data->uid)) {
+    // Load the user that is the author of the node
+    $uid = $context->data->uid;
+    $account = user_load(array('uid' => $uid));
+
+    // Send it to ctools
+    return ctools_context_create('user', $account);
+  }
+  else {
+    return ctools_context_create_empty('user', NULL);
+  }
+}
+