From 655c3e5c77f811141a7b90d1f53474155e23a57a Mon Sep 17 00:00:00 2001
From: Earl Miles <merlin@logrus.com>
Date: Sat, 20 Dec 2008 05:37:32 +0000
Subject: [PATCH] Checkpoint checkin. Lots more interesting functionality --
 pages can now be set to "single" or "multiple" task handlers, and having
 "single" removes the task handler arrange UI. Please some serious code
 re-organization so that Panels can have just one context-based task handler
 for multiple tasks.

---
 delegator/css/task-handlers.css             |  13 +
 delegator/delegator.admin.inc               | 573 ++++++++++----------
 delegator/delegator.install                 |   6 +
 delegator/delegator.module                  |  64 ++-
 delegator/js/task-handlers.js               |  21 +-
 delegator/plugins/task_types/page.admin.inc |  17 +-
 delegator/plugins/tasks/page.admin.inc      |  74 ++-
 delegator/plugins/tasks/page.inc            |  38 +-
 includes/context-task-handler.inc           | 149 +++++
 includes/context.inc                        |   4 +
 includes/dependent.inc                      |   4 +-
 includes/wizard.inc                         |  26 +-
 plugins/arguments/nid.inc                   |  22 +
 plugins/arguments/uid.inc                   |  22 +
 14 files changed, 711 insertions(+), 322 deletions(-)
 create mode 100644 includes/context-task-handler.inc

diff --git a/delegator/css/task-handlers.css b/delegator/css/task-handlers.css
index b937812c..8c2af600 100644
--- a/delegator/css/task-handlers.css
+++ b/delegator/css/task-handlers.css
@@ -63,3 +63,16 @@
   display: block;
 }
 */
+
+html.js .delegator-operations {
+  width: 170px;
+}
+
+div.delegator-task-handler-operations {
+  display: none;
+}
+
+html.js div.delegator-task-handler-operations {
+  display: block;
+}
+
diff --git a/delegator/delegator.admin.inc b/delegator/delegator.admin.inc
index 0a0c82da..18c6e2ff 100644
--- a/delegator/delegator.admin.inc
+++ b/delegator/delegator.admin.inc
@@ -24,34 +24,33 @@ define('DGA_CHANGED_DELETED', 0x04);
 /**
  * Bit flag on the 'changed' value to tell us if an item has had its disabled status changed.
  */
-define('DGA_CHANGED_STATUS', 0x01);
+define('DGA_CHANGED_STATUS', 0x08);
 
 /**
- * Page callback to administer a particular task.
+ * Reset the active menu trail to the trail specified by the task type.
+ *
+ * The task type can specify an 'admin path'. If it does,
+ * 'admin/build/delegator' will be removed from the active trail and replaced
+ * with whatever is in the admin path; that way task types can provide
+ * their own administration.
  */
-function delegator_administer_task($task_name) {
-  list($task_id, $subtask_id) = delegator_get_task_id($task_name);
-
-  $task = delegator_get_task($task_id);
-  if (!$task) {
-    return drupal_not_found();
+function delegator_set_trail($task) {
+  $task_type = delegator_get_task_type($task['task type']);
+  if (empty($task_type['admin path'])) {
+    return;
   }
 
-  $subtask = delegator_get_task_subtask($task, $subtask_id);
-
-  $task_handlers = delegator_load_task_handlers($task, $subtask_id);
+  ctools_include('menu');
+  $trail = menu_get_active_trail();
+  $remove = ctools_get_menu_trail('admin/build/delegator');
+  foreach ($remove as $id => $info) {
+    if (isset($trail[$id])) {
+      unset($trail[$id]);
+    }
+  }
 
-  $form_state = array(
-    'task_name' => $task_name,
-    'task_id' => $task_id,
-    'task' => $task,
-    'subtask' => $subtask,
-    'subtask_id' => $subtask_id,
-    'task_handlers' => $task_handlers,
-    'cache' => delegator_admin_get_task_cache($task, $subtask_id, $task_handlers),
-  );
-  ctools_include('form');
-  return ctools_build_form('delegator_admin_list_form', $form_state);
+  $trail = array_merge(ctools_get_menu_trail($task_type['admin path']), $trail);
+  menu_set_active_trail($trail);
 }
 
 /**
@@ -108,14 +107,22 @@ function delegator_admin_set_task_cache($task, $subtask_id, $cache) {
   // We only bother if something has been marked changed. This keeps us from
   // locking when we should not.
   $changed = FALSE;
-  foreach ($cache->handlers as $handler) {
-    if (!empty($handler['changed'])) {
-      $changed = TRUE;
-      break;
+  if (!empty($cache->working)) {
+    $changed = TRUE;
+  }
+  else {
+    foreach ($cache->handlers as $handler) {
+      if (!empty($handler['changed'])) {
+        $changed = TRUE;
+        break;
+      }
     }
   }
 
   if (!$changed) {
+    // We may have cancelled a working copy. We'll actually clear cache in this
+    // instance.
+    delegator_admin_clear_task_cache($task, $subtask_id);
     return;
   }
 
@@ -148,9 +155,15 @@ function delegator_admin_get_task_handler_cache($name) {
 /**
  * Store changes to a task handler in the object cache.
  */
-function delegator_admin_set_task_handler_cache($handler) {
+function delegator_admin_set_task_handler_cache($handler, $working = FALSE) {
+  $name = $handler->name;
+
+  if ($working) {
+    $name = delegator_make_task_name($handler->task, $handler->subtask) . '-working';
+  }
+
   ctools_include('object-cache');
-  $cache = ctools_object_cache_set('delegator_task_handler', $handler->name, $handler);
+  $cache = ctools_object_cache_set('delegator_task_handler', $name, $handler);
 }
 
 /**
@@ -161,6 +174,37 @@ function delegator_admin_clear_task_handler_cache($name) {
   ctools_object_cache_clear('delegator_task_handler', $name);
 }
 
+/**
+ * Write a new handler to the database.
+ */
+function delegator_admin_new_task_handler($handler, $task_name, $task, $subtask_id, $cache, $plugin) {
+  // Store the new handler.
+  if (!$cache->locked) {
+    delegator_admin_set_task_handler_cache($handler, TRUE);
+  }
+
+  $cache->handlers[$handler->name] = array(
+    'name' => $handler->name,
+    'weight' => $handler->weight,
+    'changed' => DGA_CHANGED_CACHED,
+    'disabled' => FALSE,
+  );
+  $cache->working = $handler->name;
+
+  // Store the changed task handler list.
+  delegator_admin_set_task_cache($task, $subtask_id, $cache);
+
+  // If the task handler plugin specifies an add form, set a redirect.
+  if (isset($plugin['add forms'])) {
+    // Get the beginning of the array.
+    reset($plugin['add forms']);
+    list($id, $title) = each($plugin['add forms']);
+    return "admin/build/delegator/$task_name/add/$handler->name/$id";
+  }
+  else {
+    return "admin/build/delegator/$task_name";
+  }
+}
 /**
  * Used as a callback to uasort to sort the task cache by weight.
  *
@@ -192,9 +236,12 @@ function _delegator_admin_task_cache_sort($a, $b) {
  * reverted and unsaved, which can cause issues all their own. This
  * function can be used to find the right handler to use in these cases.
  */
-function delegator_admin_find_handler($id, $cache, $task_handlers = array()) {
+function delegator_admin_find_handler($id, $cache, $task_name, $task_handlers = array()) {
   // Use the one from the database or an updated one in cache?
-  if ($cache->handlers[$id]['changed'] & DGA_CHANGED_CACHED) {
+  if (isset($cache->working) && $cache->working == $id) {
+    $handler = delegator_admin_get_task_handler_cache($task_name . '-working');
+  }
+  else if ($cache->handlers[$id]['changed'] & DGA_CHANGED_CACHED) {
     $handler = delegator_admin_get_task_handler_cache($id);
   }
   else {
@@ -214,6 +261,42 @@ function delegator_admin_find_handler($id, $cache, $task_handlers = array()) {
   return $handler;
 }
 
+/**
+ * Page callback to administer a particular task.
+ */
+function delegator_administer_task($task_name) {
+  list($task_id, $subtask_id) = delegator_get_task_id($task_name);
+
+  $task = delegator_get_task($task_id);
+  if (!$task) {
+    return drupal_not_found();
+  }
+
+  $subtask = delegator_get_task_subtask($task, $subtask_id);
+
+  // Subtasks marked as single tasks won't see this page, so redirect
+  // to wherever the subtask wants to be administered from.
+  if (!empty($subtask['single task'])) {
+    $task_type = delegator_get_task_type($task['task type']);
+    return drupal_goto($task_type['admin path']);
+  }
+
+  $task_handlers = delegator_load_task_handlers($task, $subtask_id);
+  delegator_set_trail($task);
+
+  $form_state = array(
+    'task_name' => $task_name,
+    'task_id' => $task_id,
+    'task' => $task,
+    'subtask' => $subtask,
+    'subtask_id' => $subtask_id,
+    'task_handlers' => $task_handlers,
+    'cache' => delegator_admin_get_task_cache($task, $subtask_id, $task_handlers),
+  );
+  ctools_include('form');
+  return ctools_build_form('delegator_admin_list_form', $form_state);
+}
+
 /**
  * Form to administer task handlers assigned to a task.
  */
@@ -253,7 +336,7 @@ function delegator_admin_list_form(&$form_state) {
   // Create data for a table for all of the task handlers.
   foreach ($cache->handlers as $id => $info) {
     // Skip deleted items.
-    $handler = delegator_admin_find_handler($id, $form_state['cache'], $task_handlers);
+    $handler = delegator_admin_find_handler($id, $form_state['cache'], $form_state['task_name'], $task_handlers);
 
     if (!$handler || ($info['changed'] & DGA_CHANGED_DELETED && !($handler->export_type & EXPORT_IN_CODE))) {
       $form['#changed'] = TRUE;
@@ -328,6 +411,10 @@ function delegator_admin_list_form(&$form_state) {
       }
     }
 
+    $form['handlers'][$id]['dropdown'] = array(
+      '#value' => theme('ctools_dropdown', t('Operations'), delegator_admin_make_dropdown_links($actions), 'delegator-task-handler-operations')
+    );
+
     $form['handlers'][$id]['action'] = array(
       '#type' => 'select',
       '#options' => $actions,
@@ -424,6 +511,37 @@ function delegator_admin_list_form(&$form_state) {
   return $form;
 }
 
+/**
+ * Make a set of links out of the actions array.
+ *
+ * because this can have embedded arrays, this is a function so it can
+ * use recursion.
+ */
+function delegator_admin_make_dropdown_links($actions) {
+  $links = array();
+  // Take the actions and make a dropdown for those of us with javascript.
+  foreach ($actions as $id => $text) {
+    if (!$id) {
+      continue;
+    }
+
+    if (is_array($text)) {
+      $links[] =  array(
+        'title' => '<span class="text">' . $id . '</span>' . theme('links', delegator_admin_make_dropdown_links($text)),
+        'html' => TRUE,
+      );
+    }
+    else {
+      $links[] = array(
+        'title' => $text,
+        'href' => $id,
+      );
+    }
+  }
+
+  return $links;
+}
+
 /**
  * Theme the form so it has a table.
  */
@@ -486,6 +604,7 @@ function theme_delegator_admin_list_form($form) {
       $row[] = drupal_render($element['weight']);
 
       $operations = '';
+      $operations .= drupal_render($element['dropdown']);
       $operations .= drupal_render($element['action']);
       $operations .= drupal_render($element['config']);
       $row[] = array(
@@ -588,69 +707,9 @@ function delegator_admin_list_form_add($form, &$form_state) {
   // Update the weights from the form.
   $weight = delegator_admin_update_weights($form_state);
 
-  // Generate a unique name. Unlike most named objects, we don't let people choose
-  // names for task handlers because they mostly don't make sense.
-  $base = $form_state['task']['name'];
-  if ($form_state['subtask_id']) {
-    $base .= '_' . $form_state['subtask_id'];
-  }
-  $base .= '_' . $plugin['name'];
-
-  // Once we have a base, check to see if it is used. If it is, start counting up.
-  $name = $base;
-  $count = 1;
-  // If taken
-  while (isset($form_state['cache']->handlers[$name])) {
-    $name = $base . '_' . ++$count;
-  }
-
-  // Create a new, empty handler object.
-  $handler = new stdClass;
-  $handler->task = $form_state['task']['name'];
-  $handler->subtask = $form_state['subtask_id'];
-  $handler->name = $name;
-  $handler->handler = $plugin['name'];
-  $handler->weight = $weight;
-  $handler->conf = array();
-
-  // These are provided by the core export API provided by ctools and we
-  // set defaults here so that we don't cause notices. Perhaps ctools should
-  // provide a way to do this for us so we don't have to muck with it.
-  $handler->export_type = EXPORT_IN_DATABASE;
-  $handler->type = t('Local');
-
-  if (isset($plugin['default conf'])) {
-    if (is_array($plugin['default conf'])) {
-      $handler->conf = $plugin['default conf'];
-    }
-    else if (function_exists($plugin['default conf'])) {
-      $handler->conf = $plugin['default conf']($handler, $form_state['task'], $form_state['subtask_id']);
-    }
-  }
+  $handler = delegator_new_task_handler($form_state['task'], $form_state['subtask_id'], $plugin, $weight, $form_state['cache']);
 
-  // Store the new handler.
-  if (!$form_state['cache']->locked) {
-    delegator_admin_set_task_handler_cache($handler);
-  }
-
-  $form_state['cache']->handlers[$handler->name] = array(
-    'name' => $handler->name,
-    'weight' => $handler->weight,
-    'changed' => DGA_CHANGED_CACHED,
-    'disabled' => FALSE,
-  );
-  $form_state['cache']->last_touched = $handler->name;
-
-  // Store the changed task handler list.
-  delegator_admin_set_task_cache($form_state['task'], $form_state['subtask_id'], $form_state['cache']);
-
-  // If the task handler plugin specifies an add form, set a redirect.
-  if (isset($plugin['add forms'])) {
-    // Get the beginning of the array.
-    reset($plugin['add forms']);
-    list($id, $title) = each($plugin['add forms']);
-    $form_state['redirect'] = "admin/build/delegator/" . $form_state['task_name'] . "/add/$handler->name/$id";
-  }
+  $form_state['redirect'] = delegator_admin_new_task_handler($handler, $form_state['task_name'], $form_state['task'], $form_state['subtask_id'], $form_state['cache'], $plugin);
 }
 
 /**
@@ -660,6 +719,12 @@ function delegator_admin_list_form_submit($form, &$form_state) {
   // Update the weights from the form.
   $form_state['redirect'] = $_GET['q'];
 
+  // If there's an admin path, go there instead.
+  $task_type = delegator_get_task_type($form_state['task']['task type']);
+  if (isset($task_type['admin path'])) {
+    $form_state['redirect'] = $task_type['admin path'];
+  }
+
   delegator_admin_update_weights($form_state);
 
   $cache = &$form_state['cache'];
@@ -704,10 +769,11 @@ function delegator_admin_list_form_submit($form, &$form_state) {
     }
   }
 
-  drupal_set_message(t('All changes have been updated.'));
+  drupal_set_message(t('All changes have been saved.'));
 
-  // Clear the cache and set a redirect.
+  // Clear the cache
   delegator_admin_clear_task_cache($form_state['task'], $form_state['subtask_id']);
+  delegator_admin_clear_task_handler_cache($form_state['task_name'] . '-working');
 }
 
 /**
@@ -723,6 +789,13 @@ function delegator_admin_list_form_cancel($form, &$form_state) {
   }
 
   delegator_admin_clear_task_cache($form_state['task'], $form_state['subtask_id']);
+  delegator_admin_clear_task_handler_cache($form_state['task_name'] . '-working');
+
+  // If there's an admin path, go there instead.
+  $task_type = delegator_get_task_type($form_state['task']['task type']);
+  if (isset($task_type['admin path'])) {
+    $form_state['redirect'] = $task_type['admin path'];
+  }
 }
 
 /**
@@ -782,11 +855,9 @@ function delegator_admin_list_form_action_delete($form, &$form_state, $id, $acti
  */
 function delegator_admin_list_form_action_edit($form, &$form_state, $id, $action, $argument) {
   // Use the one from the database or an updated one in cache?
-  $handler = delegator_admin_find_handler($id, $form_state['cache'], $form_state['task_handlers']);
+  $handler = delegator_admin_find_handler($id, $form_state['cache'], $form_state['task_name'], $form_state['task_handlers']);
 
   $name = $form_state['task_name'];
-  // @todo: Allow an owner UI to control this URL.
-  // @todo: subtask ID
   $form_state['redirect'] = "admin/build/delegator/$name/$handler->handler/$id/$argument";
 }
 
@@ -795,7 +866,7 @@ function delegator_admin_list_form_action_edit($form, &$form_state, $id, $action
  */
 function delegator_admin_list_form_action_clone($form, &$form_state, $id, $action, $argument) {
   // Use the one from the database or an updated one in cache?
-  $handler = delegator_admin_find_handler($id, $form_state['cache'], $form_state['task_handlers']);
+  $handler = delegator_admin_find_handler($id, $form_state['cache'], $form_state['task_name'], $form_state['task_handlers']);
 
   // Get the next weight from the form
   $handler->weight = delegator_admin_update_weights($form_state);
@@ -879,13 +950,29 @@ function delegator_administer_task_handler_export($task_name, $name) {
   drupal_set_title(t('Export task handler "@title"', array('@title' => $title)));
 
   ctools_include('export');
+  delegator_set_trail($task);
   return drupal_get_form('ctools_export_form', delegator_export_task_handler($handler), $title);
 }
 
+function delegator_admin_task_handler_form_info($task_name) {
+  return array(
+    'id' => 'delegator_task_handler',
+    'show back' => TRUE,
+    'show cancel' => TRUE,
+
+    'return path' => "admin/build/delegator/$task_name",
+
+    'next callback'   => 'delegator_admin_edit_task_handler_next',
+    'finish callback' => 'delegator_admin_edit_task_handler_finish',
+    'return callback' => 'delegator_admin_edit_task_handler_finish',
+    'cancel callback' => 'delegator_admin_edit_task_handler_cancel',
+  );
+}
+
 /**
  * Entry point to edit a task handler.
  */
-function delegator_administer_task_handler_edit($task_name, $handler_id, $name, $form_id) {
+function delegator_administer_task_handler_edit($task_name, $handler_id, $name, $step) {
   list($task_id, $subtask_id) = delegator_get_task_id($task_name);
 
   $task = delegator_get_task($task_id);
@@ -894,7 +981,7 @@ function delegator_administer_task_handler_edit($task_name, $handler_id, $name,
 
   $cache = delegator_admin_get_task_cache($task, $subtask_id);
 
-  $handler = delegator_admin_find_handler($name, $cache);
+  $handler = delegator_admin_find_handler($name, $cache, $task_name);
 
   if (!$handler) {
     return drupal_not_found();
@@ -905,28 +992,29 @@ function delegator_administer_task_handler_edit($task_name, $handler_id, $name,
   if ($handler_id != $handler->handler ||
       !$task ||
       !$plugin ||
-      !isset($plugin['forms'][$form_id]) ||
-      !isset($plugin['edit forms'][$form_id])) {
+      !isset($plugin['forms'][$step]) ||
+      !isset($plugin['edit forms'][$step])) {
     return drupal_not_found();
   }
 
-  $title = delegator_get_handler_title($plugin, $handler, $task, $subtask_id);
-  drupal_set_title(t('Edit task handler "@title"', array('@title' => $title)));
-
-  // Figure out what the next form is for the 'save and continue' button.
-  // Loop through the forms because we need to know what our next form is
-  reset($plugin['edit forms']);
-  while (list($id, $title) = each($plugin['edit forms'])) {
-    if ($form_id == $id) {
-      break;
-    }
+  $form_info = delegator_admin_task_handler_form_info($task_name);
+  if (!empty($subtask['single task'])) {
+    $task_type = delegator_get_task_type($task['task type']);
+    $form_info['return path'] = $task_type['admin path'];
+  }
+  $form_info['order'] = $plugin['edit forms'];
+  $form_info['forms'] = $plugin['forms'];
+  $form_info['path'] = "admin/build/delegator/$task_name/$handler_id/$name/%step";
+  if (empty($plugin['forms'][$step]['no return'])) {
+    $form_info['show return'] = TRUE;
   }
 
-  // The 'break' made our next form current. Running each
-  // again will advance by one.
-  $next_info = each($plugin['edit forms']);
+  $title = delegator_get_handler_title($plugin, $handler, $task, $subtask_id);
+  drupal_set_title(t('Edit task handler "@title"', array('@title' => $title)));
 
+  delegator_set_trail($task);
   $form_state = array(
+    'step' => $step,
     'task_name' => $task_name,
     'task_id' => $task_id,
     'task' => $task,
@@ -934,22 +1022,20 @@ function delegator_administer_task_handler_edit($task_name, $handler_id, $name,
     'subtask' => $subtask,
     'plugin' => $plugin,
     'handler' => $handler,
-    'plugin_form_id' => $form_id, // so it doesn't get confused with the form's form ID
-    'forms' => $plugin['edit forms'],
     'type' => 'edit',
     'cache' => $cache,
   );
 
-  if (!empty($plugin['forms'][$form_id]['alternate next'])) {
-    $form_state['next'] = "admin/build/delegator/$task_name/$handler_id/$name/" . $plugin['forms'][$form_id]['alternate next'];
-  }
-  elseif ($next_info) {
-    $form_state['next'] = "admin/build/delegator/$task_name/$handler_id/$name/$next_info[key]";
+  ctools_include('wizard');
+  $output = ctools_wizard_multistep_form($form_info, $step, $form_state);
+
+  drupal_add_css(drupal_get_path('module', 'delegator') . '/css/task-handlers.css');
+  if (!$output) {
+    // redirect.
+    drupal_redirect_form(array(), $form_state['redirect']);
   }
 
-  ctools_include('form');
-  $output = ctools_build_form('delegator_admin_edit_task_handler', $form_state);
-  if ($output && !empty($plugin['forms'][$form_id]['no blocks'])) {
+  if (!empty($plugin['forms'][$step]['no blocks'])) {
     print theme('page', $output, FALSE);
   }
   else {
@@ -960,10 +1046,10 @@ function delegator_administer_task_handler_edit($task_name, $handler_id, $name,
 /**
  * Entry point to add a task handler.
  */
-function delegator_administer_task_handler_add($task_name, $name, $form_id) {
+function delegator_administer_task_handler_add($task_name, $name, $step) {
   list($task_id, $subtask_id) = delegator_get_task_id($task_name);
 
-  $handler = delegator_admin_get_task_handler_cache($name);
+  $handler = delegator_admin_get_task_handler_cache($task_name . '-working');
 
   if (!$handler) {
     return drupal_not_found();
@@ -977,17 +1063,30 @@ function delegator_administer_task_handler_add($task_name, $name, $form_id) {
   // particular handler, or of somehow having invalid tasks or task handlers.
   if (!$task ||
       !$plugin ||
-      !isset($plugin['forms'][$form_id]) ||
-      !isset($plugin['add forms'][$form_id])) {
+      !isset($plugin['forms'][$step]) ||
+      !isset($plugin['add forms'][$step])) {
     return drupal_not_found();
   }
 
   $cache = delegator_admin_get_task_cache($task, $subtask_id);
+  $form_info = delegator_admin_task_handler_form_info($task_name);
+  // Single tasks will return to a different location, this will help that.
+  if (!empty($subtask['single task'])) {
+    $task_type = delegator_get_task_type($task['task type']);
+    $form_info['return path'] = $task_type['admin path'];
+  }
+
+  $form_info['order'] = $plugin['add forms'];
+  $form_info['forms'] = $plugin['forms'];
+  $form_info['path'] = "admin/build/delegator/$task_name/add/$handler->name/%step";
+  $form_info['show trail'] = TRUE;
 
   $title = delegator_get_handler_title($plugin, $handler, $task, $subtask_id);
   drupal_set_title(t('Add task handler "@title"', array('@title' => $title)));
 
+  delegator_set_trail($task);
   $form_state = array(
+    'step' => $step,
     'task_name' => $task_name,
     'task_id' => $task_id,
     'task' => $task,
@@ -995,177 +1094,78 @@ function delegator_administer_task_handler_add($task_name, $name, $form_id) {
     'subtask' => $subtask,
     'plugin' => $plugin,
     'handler' => $handler,
-    'plugin_form_id' => $form_id, // so it doesn't get confused with the form's form ID
-    'forms' => $plugin['add forms'],
     'type' => 'add',
     'cache' => $cache,
   );
 
-  $output = '';
+  ctools_include('wizard');
+  $output = ctools_wizard_multistep_form($form_info, $step, $form_state);
 
-  $crumbs = array();
-  $previous = TRUE;
-  foreach ($plugin['add forms'] as $id => $title) {
-    if ($id == $form_id) {
-      $previous = FALSE;
-      $class = 'delegator-current';
-    }
-    elseif ($previous) {
-      $not_first = TRUE;
-      $class = 'delegator-previous';
-    }
-    else {
-      $class = 'delegator-next';
-      if (!isset($form_state['next'])) {
-        $form_state['next'] = "admin/build/delegator/$task_name/add/$name/$id";
-      }
-    }
-    $crumbs[] = '<span class="' . $class . '">' . $title . '</span>';
+  if (!$output) {
+    // redirect.
+    drupal_redirect_form(array(), $form_state['redirect']);
   }
 
   drupal_add_css(drupal_get_path('module', 'delegator') . '/css/task-handlers.css');
 
+  if (!empty($plugin['forms'][$step]['no blocks'])) {
+    print theme('page', $output, FALSE);
+  }
+  else {
+    return $output;
+  }
+
+  /*
   if (empty($not_first)) {
     $output .= '<div class="delegator-explanation">' . t('Before proceeding, you must configure your new "@type"', array('@type' => $plugin['title'])) . '</div>';
   }
-
-  $output .= theme('delegator_breadcrumb', $crumbs);
-  ctools_include('form');
-  $output .= ctools_build_form('delegator_admin_edit_task_handler', $form_state);
-  return $output;
+  */
 }
 
 /**
- * Themable display of the 'breadcrumb' trail to show the process of
- * adding a new item.
+ * Form wizard 'next' handler -- update the cache.
  */
-function theme_delegator_breadcrumb($breadcrumb) {
-  if (!empty($breadcrumb)) {
-    return '<div class="delegator-breadcrumb">' . implode(' » ', $breadcrumb) . '</div>';
-  }
-}
-
-/**
- * Build an actual form to edit a task handler.
- *
- * Most of this form is handled by the task handler plugin, this is primarily
- * just providing a framework for the system to work within.
- */
-function delegator_admin_edit_task_handler(&$form_state) {
-  $task_name = $form_state['task_name'];
-  $plugin = $form_state['plugin'];
-  $form_id = $form_state['plugin_form_id'];
-  $task = $form_state['task'];
-  $handler = $form_state['handler'];
-  $forms = $form_state['forms']; // either edit or add forms depending upon entry
-
-  $info = $plugin['forms'][$form_id];
-
-  if (!empty($info['include'])) {
-    if (is_array($info['include'])) {
-      foreach ($info['include'] as $file) {
-        require_once './' . $file;
-      }
-    }
-    else {
-      require_once './' . $info['include'];
-    }
-  }
-
-  $form['conf']['#tree'] = TRUE;
-
-  // Let it have validate and submit handlers in a way that's nicer to the
-  // D6 system.
-  $validate = array();
-  if (!empty($info['validate']) && function_exists($info['validate'])) {
-    $validate = array($info['validate']);
-  }
-
-  $submit = array();
-  if (!empty($info['submit']) && function_exists($info['submit'])) {
-    $submit = array($info['submit']);
-  }
-  $submit[] = 'delegator_admin_edit_task_handler_submit';
-
-  // Ensure buttons stay on the bottom.
-  $form['buttons'] = array(
-    '#prefix' => '<div class="clear-block">',
-    '#suffix' => '</div>',
-    '#weight' => 1000,
-  );
-
-  if (isset($form_state['next'])) {
-    $form['buttons']['next'] = array(
-      '#type' => 'submit',
-      '#value' => t('Continue'),
-      '#next' => $form_state['next'],
-      '#validate' => $validate,
-      '#submit' => $submit,
-      '#weight' => -1000,
-    );
+function delegator_admin_edit_task_handler_next(&$form_state) {
+  if ($form_state['cache']->locked) {
+    drupal_set_message(t('Unable to update task due to lock.'), 'error');
+    return;
   }
 
-  if ($form_state['type'] == 'edit' && empty($info['no return'])) {
-    $form['buttons']['return'] = array(
-      '#type' => 'submit',
-      '#value' => t('Update and return'),
-      '#next' => "admin/build/delegator/$task_name",
-      '#validate' => $validate,
-      '#submit' => $submit,
-    );
-  }
-  else if (empty($form_state['next'])) {
-    $form['buttons']['next'] = array(
-      '#type' => 'submit',
-      '#value' => t('Add handler'),
-      '#next' => "admin/build/delegator/$task_name",
-      '#validate' => $validate,
-      '#submit' => $submit,
-    );
-  }
+  $handler = &$form_state['handler'];
 
-  $form['buttons']['cancel'] = array(
-    '#type' => 'submit',
-    '#value' => t('Cancel'),
-    '#next' => "admin/build/delegator/$task_name",
-    '#submit' => array('delegator_admin_edit_task_handler_cancel'),
-  );
+  // This updates the working cache, since we're not finished.
+  delegator_admin_set_task_handler_cache($handler, TRUE);
 
-  // Allow the plugin to add its form items now.
-  if (!empty($info['form']) && function_exists($info['form'])) {
-    $info['form']($form, $form_state);
+  // Make sure the cache is aware that we're editing this item:
+  if (!isset($form_state['cache']->working) || $form_state['cache']->working != $handler->name) {
+    $form_state['cache']->working = $handler->name;
+    delegator_admin_set_task_cache($form_state['task'], $form_state['subtask_id'], $form_state['cache']);
   }
-
-  return $form;
 }
 
+
 /**
- * Submit handler for task handler edit form.
+ * Form wizard finish handler; called to update everything the wizard touched.
  *
- * Cache data and proceed to the next form as specified by the clicked button.
+ * This transfers the working cache to the task handler cache and
+ * updates the cache to mark this item as having changed.
  */
-function delegator_admin_edit_task_handler_submit($form, &$form_state) {
+function delegator_admin_edit_task_handler_finish(&$form_state) {
   $handler = &$form_state['handler'];
-
-  // Update the task handler cache to let the system know this one has now
-  // officially changed.
   $cache = &$form_state['cache'];
 
-  $form_state['redirect'] = $form_state['clicked_button']['#next'];
   if ($cache->locked) {
     drupal_set_message(t('Unable to update task due to lock.'), 'error');
     return;
   }
 
-  // Only bother updating the cache if we're going to change something, so if
-  // our handler is not marked changed or is not the last touched handler, do so.
-  if (!($cache->handlers[$handler->name]['changed'] & DGA_CHANGED_CACHED) || !isset($cache->last_touched) || $cache->last_touched != $handler->name) {
 
-    // Set status of our handler
-    $cache->handlers[$handler->name]['changed'] |= DGA_CHANGED_CACHED;
-    $cache->last_touched = $handler->name;
-    delegator_admin_set_task_cache($form_state['task'], $form_state['subtask_id'], $cache);
-  }
+  // Set status of our handler
+  $cache->handlers[$handler->name]['changed'] |= DGA_CHANGED_CACHED;
+  $cache->last_touched = $handler->name;
+  unset($cache->working);
+
+  delegator_admin_set_task_cache($form_state['task'], $form_state['subtask_id'], $cache);
 
   if (isset($form_state['values']['conf']) && is_array($form_state['values']['conf'])) {
     // Merge whatever is in the form values with the existing configuration.
@@ -1174,36 +1174,28 @@ function delegator_admin_edit_task_handler_submit($form, &$form_state) {
 
   // Write to cache.
   delegator_admin_set_task_handler_cache($handler);
-}
 
-/**
- * Submit handler for task handler edit form.
- *
- * This is really just a nice button to return to the top level without
- * caching changes from the form. It skips validation and submit.
- */
-function delegator_admin_edit_task_handler_cancel($form, &$form_state) {
-  if ($form_state['type'] == 'add') {
-    // flush the newly added handler from the cache so that it won't show up.
-    delegator_admin_clear_task_handler_cache($form_state['handler']->name);
-
-    // Send an array() through as the list of $task_handlers -- because
-    // if we're at this point there MUST be something in the cache.
-    $cache = &$form_state['cache'];
-    if (isset($cache->handlers[$form_state['handler']->name])) {
-      unset($cache->handlers[$form_state['handler']->name]);
-    }
+  // Remove working copy.
+  delegator_admin_clear_task_handler_cache($form_state['task_name'] . '-working');
 
-    delegator_admin_set_task_cache($form_state['task'], $form_state['subtask_id'], $cache);
+  // If the task or subtask is defined as containing a single handler, then just save
+  // the whole cache here since they won't get a chance to later.
+  if (!empty($form_state['subtask']['single task']) || !empty($form_state['task']['single task'])) {
+    delegator_admin_list_form_submit(array(), $form_state);
   }
-  $form_state['redirect'] = $form_state['clicked_button']['#next'];
 }
 
 /**
- * Form to break a lock on a delegator task.
+ * Wizard cancel handler for the task handler edit. Clear the working
+ * copy from cache.
  */
+function delegator_admin_edit_task_handler_cancel(&$form_state) {
+  // Remove working copy.
+  delegator_admin_clear_task_handler_cache($form_state['task_name'] . '-working');
+}
+
 /**
- * Page to delete a view.
+ * Form to break a lock on a delegator task.
  */
 function delegator_administer_break_lock(&$form_state, $task_name) {
   list($task_id, $subtask_id) = delegator_get_task_id($task_name);
@@ -1221,6 +1213,9 @@ function delegator_administer_break_lock(&$form_state, $task_name) {
     return array('message' => array('#value' => t('There is no lock on this task to break.')));
   }
 
+  $task = delegator_get_task($task_id);
+  delegator_set_trail($task);
+
   $cancel = 'admin/build/delegator/' . $task_name;
   if (!empty($_REQUEST['cancel'])) {
     $cancel = $_REQUEST['cancel'];
@@ -1299,7 +1294,7 @@ function delegator_admin_import_task_handler_validate($form, &$form_state) {
   if (isset($cache->handlers[$handler->name])) {
     drupal_set_message(t('Warning: The handler you are importing already exists and this operation will overwrite an existing handler. If this is not what you intend, you may Cancel this. You should then modify the <code>$handler-&gt;name</code> field of your import to have a unique name.'), 'warning');
 
-    $old_handler = delegator_admin_find_handler($handler->name, $cache);
+    $old_handler = delegator_admin_find_handler($handler->name, $cache, $form_state['task_name']);
     $handler->export_type = $old_handler->export_type | EXPORT_IN_DATABASE;
   }
 
diff --git a/delegator/delegator.install b/delegator/delegator.install
index 0b66211d..04132dce 100644
--- a/delegator/delegator.install
+++ b/delegator/delegator.install
@@ -120,6 +120,12 @@ function delegator_schema_1() {
         'default' => '',
         'serialize' => TRUE,
       ),
+      'multiple' => array(
+        'type' => 'int',
+        'size' => 'tiny',
+        'description' => t('True if the UI is set up to allow multiple handlers per page.'),
+        'default' => 0,
+      ),
       'menu' => array(
         'type' => 'text',
         'size' => 'big',
diff --git a/delegator/delegator.module b/delegator/delegator.module
index 213ac7f7..0b9dd3b6 100644
--- a/delegator/delegator.module
+++ b/delegator/delegator.module
@@ -34,10 +34,6 @@ function delegator_theme() {
       'arguments' => array('form' => NULL),
       'file' => 'delegator.admin.inc',
     ),
-    'delegator_breadcrumb' => array(
-      'arguments' => array('breadcrumb' => array()),
-      'file' => 'delegator.admin.inc',
-    ),
   );
 
   // Allow task plugins to have theme registrations by passing through:
@@ -360,6 +356,66 @@ function delegator_export_task_handler($handler, $indent = '') {
   return $output;
 }
 
+/**
+ * Create a new task handler object.
+ *
+ * @param $task
+ *   The task this task handler is for.
+ * @param $subtask_id
+ *   The subtask this task handler is for.
+ * @param $plugin
+ *   The plugin this task handler is created from.
+ * @param $weight
+ *   The weight to give this new task handler.
+ * @param $cache
+ *   The task cache if the task is currently being edited. This must be used
+ *   if task handlers already exist as it is used to determine a unique name
+ *   and without this naming collisions could occur.
+ */
+function delegator_new_task_handler($task, $subtask_id, $plugin, $weight = 0, $cache = NULL) {
+  // Generate a unique name. Unlike most named objects, we don't let people choose
+  // names for task handlers because they mostly don't make sense.
+  $base = $task['name'];
+  if ($subtask_id) {
+    $base .= '_' . $subtask_id;
+  }
+  $base .= '_' . $plugin['name'];
+
+  // Once we have a base, check to see if it is used. If it is, start counting up.
+  $name = $base;
+  $count = 1;
+  // If taken
+  while (isset($cache->handlers[$name])) {
+    $name = $base . '_' . ++$count;
+  }
+
+  // Create a new, empty handler object.
+  $handler = new stdClass;
+  $handler->task = $task['name'];
+  $handler->subtask = $subtask_id;
+  $handler->name = $name;
+  $handler->handler = $plugin['name'];
+  $handler->weight = $weight;
+  $handler->conf = array();
+
+  // These are provided by the core export API provided by ctools and we
+  // set defaults here so that we don't cause notices. Perhaps ctools should
+  // provide a way to do this for us so we don't have to muck with it.
+  $handler->export_type = EXPORT_IN_DATABASE;
+  $handler->type = t('Local');
+
+  if (isset($plugin['default conf'])) {
+    if (is_array($plugin['default conf'])) {
+      $handler->conf = $plugin['default conf'];
+    }
+    else if (function_exists($plugin['default conf'])) {
+      $handler->conf = $plugin['default conf']($handler, $task, $subtask_id);
+    }
+  }
+
+  return $handler;
+}
+
 /**
  * Set an overidden weight for a task handler.
  *
diff --git a/delegator/js/task-handlers.js b/delegator/js/task-handlers.js
index 99e3b2fd..7a4da574 100644
--- a/delegator/js/task-handlers.js
+++ b/delegator/js/task-handlers.js
@@ -20,22 +20,29 @@ Drupal.behaviors.zzGoLastDelegatorTaskList = function(context) {
   }
 
   $('.delegator-operations select:not(.delegator-processed)', context).each(function() {
+    var $select = $(this);
     var $next = $(this).parent().next('input');
     $next.hide();
-    $(this).change(function() {
-      var val = $(this).val();
+    $(this).hide();
+
+    $dropdown = $(this).parent().siblings('.ctools-dropdown');
+   
+    $('.ctools-dropdown-container a', $dropdown).click(function() {
+      var val = $(this).attr('href').replace('/', '');
+
       // ignore empty
       if (!val) {
-        return;
+        return false;
       }
 
       // force confirm on delete
-      if ($(this).val() == 'delete' && !confirm(Drupal.t('Remove this task?'))) {
-        $(this).val('');
-        return;
+      if (val == 'delete' && !confirm(Drupal.t('Remove this task?'))) {
+        return false;
       }
-
+      
+      $select.val(val);
       $next.trigger('click');
+      return false;
     });
   });
 }
diff --git a/delegator/plugins/task_types/page.admin.inc b/delegator/plugins/task_types/page.admin.inc
index 79ca9f38..7c093067 100644
--- a/delegator/plugins/task_types/page.admin.inc
+++ b/delegator/plugins/task_types/page.admin.inc
@@ -3,7 +3,11 @@
 
 /**
  * @file
- * Administrative functions for the page system
+ * Administrative functions for the page task type system
+ *
+ * This system provides a unified administrative interface to handle page
+ * tasks of all types, including a generic 'page' task that lets a user
+ * create pages as well as specific page tasks for handling built-in pages.
  */
 function delegator_page_type_list() {
   $tasks = delegator_get_tasks_by_type('page');
@@ -27,14 +31,11 @@ function delegator_page_type_list() {
   );
 
   foreach ($tables as $bucket => $info) {
-    if (!empty($info['rows'])) {
-      $output .= '<h3>' . check_plain($info['title']) . '</h3>';
-      $output .= '<div class="description">' . check_plain($info['description']) . '</div>';
-      if (isset($info['operations'])) {
-        $info['rows'][] = array('data' => array(array('data' => theme('links', $info['operations']), 'colspan' => 3)), 'class' => 'delegator-page-operations');
-      }
+    $output .= '<h3>' . check_plain($info['title']) . '</h3>';
+    $output .= '<div class="description">' . check_plain($info['description']) . '</div>';
+    if (isset($info['operations'])) {
+      $info['rows'][] = array('data' => array(array('data' => theme('links', $info['operations']), 'colspan' => 3)), 'class' => 'delegator-page-operations');
     }
-
     $output .= theme('table', $header, $info['rows']);
   }
 
diff --git a/delegator/plugins/tasks/page.admin.inc b/delegator/plugins/tasks/page.admin.inc
index 07881067..c3cd1bcd 100644
--- a/delegator/plugins/tasks/page.admin.inc
+++ b/delegator/plugins/tasks/page.admin.inc
@@ -72,6 +72,9 @@ function delegator_page_menu(&$items, $task) {
 
   // Add menu entries for each subtask
   foreach (delegator_page_load_all() as $subtask_id => $subtask) {
+    if (!isset($subtask->access['type'])) {
+      $subtask->access['type'] = 'none';
+    }
     if (!isset($subtask->access['settings'])) {
       $subtask->access['settings'] = NULL;
     }
@@ -207,7 +210,7 @@ function delegator_page_clear_page_cache($name) {
  *   The value will be the position of the argument so that it can easily
  *   be found. Items with a position of -1 have multiple positions.
  */
-function delegator_page_get_arguments($path) {
+function delegator_page_get_named_arguments($path) {
   $arguments = array();
   $bits = explode('/', $path);
   foreach ($bits as $position => $bit) {
@@ -327,7 +330,18 @@ function delegator_page_add_subtask_finish(&$form_state) {
   // Force a menu rebuild to recognize our new subtask
   menu_rebuild();
 
-  if ($form_state['type'] == 'add') {
+  if (isset($form_state['create task handler'])) {
+    require_once './' . drupal_get_path('module', 'delegator') . '/delegator.admin.inc';
+    $task = $form_state['task'];
+    $task_name = delegator_make_task_name($task['name'], $page->name);
+    $cache = delegator_admin_get_task_cache($task, $page->name, $form_state['task_handlers']);
+    $plugin = delegator_get_task_handler($form_state['create task handler']);
+
+    // Create a new handler.
+    $handler = delegator_new_task_handler($task, $page->name, $plugin, 0, $cache);
+    $form_state['redirect'] = delegator_admin_new_task_handler($handler, $task_name, $task, $page->name, $cache, $plugin);
+  }
+  else if ($form_state['type'] == 'add') {
     // Redirect to the new page's task handler editor.
     $form_state['redirect'] = 'admin/build/delegator/page-' . $page->name;
   }
@@ -1093,3 +1107,59 @@ function delegator_page_argument_form_settings_submit(&$form, &$form_state) {
     $page->temporary_arguments[$keyword]['settings'] = array();
   }
 }
+
+/**
+ * Form to configure a page to have a single task handler or multiple
+ * task handlers.
+ */
+function delegator_page_argument_form_multiple(&$form, &$form_state) {
+  $task = delegator_get_task('page');
+  $task_handlers = delegator_load_task_handlers($task, $form_state['page']->name);
+
+  $form['multiple'] = array(
+    '#type' => 'radios',
+    '#options' => array(
+      1 => t('Allow multiple handlers for this page.'),
+      0 => t('Create a single handler for this page.'),
+    ),
+    '#default_value' => $form_state['page']->multiple,
+    '#description' => t('By allowing multiple handlers, the task handler UI will open up, allowing you to add and remove task handlers and adjust their priority.'),
+  );
+
+  if (count($task_handlers) > 1) {
+    $form['multiple']['#disabled'] = TRUE;
+    $form['multiple']['#description'] .= t('You may not modify this value while multiple task handlers exist. If you wish to change this to single only, you must reduce the number of task handlers attached to the page to zero or one.');
+  }
+
+  $form_state['task'] = $task;
+  $form_state['task_handlers'] = $task_handlers;
+
+  if ($form_state['type'] == 'add' || empty($task_handlers)) {
+    // Get a list of possible task handlers for this task.
+    $task_handler_plugins = delegator_get_task_handler_plugins($task);
+    foreach ($task_handler_plugins as $id => $plugin) {
+      $options[$id] = $plugin['title'];
+    }
+
+    ctools_include('dependent');
+    $form['handler'] = array(
+      '#title' => t('Select the handler for this page'),
+      '#type' => 'select',
+      '#options' => $options,
+      '#process' => array('ctools_dependent_process'),
+      '#dependency' => array('radio:multiple' => array(0)),
+    );
+  }
+}
+
+/**
+ * Submit handler for the multiple form.
+ */
+function delegator_page_argument_form_multiple_submit(&$form, &$form_state) {
+  $form_state['page']->multiple = $form_state['values']['multiple'];
+
+  // The task handler will be created in the _finish hook.
+  if (!$form_state['page']->multiple && empty($task_handlers)) {
+    $form_state['create task handler'] = $form_state['values']['handler'];
+  }
+}
diff --git a/delegator/plugins/tasks/page.inc b/delegator/plugins/tasks/page.inc
index f86658b2..3e805c10 100644
--- a/delegator/plugins/tasks/page.inc
+++ b/delegator/plugins/tasks/page.inc
@@ -91,12 +91,37 @@ function delegator_page_build_subtask($task, $page) {
   }
 
   $operations = array();
+  $task_name = delegator_make_task_name($task['name'], $name);
 
   if (empty($page->disabled)) {
-    $operations[] = array(
-      'title' => t('Task handlers'),
-      'href' => "admin/build/delegator/" . delegator_make_task_name($task['name'], $name),
-    );
+    if ($page->multiple) {
+      $operations[] = array(
+        'title' => t('Task handlers'),
+        'href' => "admin/build/delegator/$task_name",
+      );
+    }
+    else {
+      $task_handlers = delegator_load_task_handlers($task, $page->name);
+      if ($task_handlers) {
+        $handler = array_shift($task_handlers);
+        $plugin = delegator_get_task_handler($handler->handler);
+        if (!empty($plugin['edit forms'])) {
+          $actions = array();
+          foreach ($plugin['edit forms'] as $edit_id => $title) {
+            if ($title) {
+              $actions[] = array(
+                'title' => $title,
+                'href' => "admin/build/delegator/$task_name/$handler->handler/$handler->name/$edit_id",
+              );
+            }
+          }
+          $operations[] =  array(
+            'title' => '<span class="text">' . t('Edit handler') . '</span>' . theme('links', $actions),
+            'html' => TRUE,
+          );
+        }
+      }
+    }
     $operations[] =  array(
       'title' => '<span class="text">' . t('Edit page') . '</span>' . theme('links', $edit_links),
       'html' => TRUE,
@@ -141,6 +166,7 @@ function delegator_page_build_subtask($task, $page) {
     'admin path' => $page->path,
     'subtask' => $page,
     'operations' => $operations,
+    'single task' => empty($page->multiple),
   );
 }
 
@@ -174,6 +200,7 @@ function delegator_page_edit_form_info() {
       'access' => t('Access type'),
       'access-settings' => t('Access settings'),
       'menu' => t('Menu settings'),
+      'multiple' => t('Task handlers'),
     ),
     'forms' => array(
       'basic' => array(
@@ -191,6 +218,9 @@ function delegator_page_edit_form_info() {
       'argument' => array(
         'form id' => 'delegator_page_form_argument'
       ),
+      'multiple' => array(
+        'form id' => 'delegator_page_argument_form_multiple'
+      ),
     ),
   );
 }
diff --git a/includes/context-task-handler.inc b/includes/context-task-handler.inc
new file mode 100644
index 00000000..aee09e28
--- /dev/null
+++ b/includes/context-task-handler.inc
@@ -0,0 +1,149 @@
+<?php
+// $Id$
+/**
+ * @file
+ * Support for creating 'context' type task handlers.
+ *
+ * Context task handlers expect the task to provide 0 or more contexts. The
+ * task handler should use those contexts as selection criteria, as well as
+ * rendering with them.
+ *
+ * The functions and forms in this file should be common to every context type
+ * task handler made.
+ *
+ * Forms:
+ * - ...
+ */
+
+/**
+ * Compare arguments to contexts for selection purposes.
+ *
+ * @param $task
+ *   The task plugin definition.
+ * @param $subtask_id
+ *   The id of the subtask being used.
+ * @param $contexts
+ *   The context objects provided by the task.
+ * @return
+ *   TRUE if these contexts match the selection criteria. NULL or FALSE
+ *   otherwise.
+ */
+function ctools_context_handler_select($task, $subtask_id, $contexts) {
+  if ($function = ctools_plugin_get_function($task, 'get arguments')) {
+    $arguments = $function($task, $subtask_id);
+    foreach ($arguments as $argument) {
+      $id = ctools_context_id($argument, 'argument');
+      if (empty($handler->conf[$id]) || empty($contexts[$id])) {
+        return FALSE;
+      }
+
+      if ($function = ctools_plugin_load_function('ctools', 'arguments', $argument['name'], 'criteria select')) {
+        if (!$function($handler->conf[$id], $contexts[$id])) {
+          return FALSE;
+        }
+      }
+    }
+  }
+  // Either there are no arguments or all arguments passed.
+  return TRUE;
+}
+
+/**
+ * Get the array of summary strings for the arguments.
+ *
+ * These summary strings are used to communicate to the user what
+ * arguments the task handlers are selecting.
+ *
+ * @param $task
+ *   The loaded task plugin.
+ * @param $subtask_id
+ *   The subtask id.
+ * @param $conf
+ *   The configuration array that will contain the various argument settings,
+ *   keyed by argument ID.
+ */
+function ctools_context_handler_summary($task, $subtask_id, $conf) {
+  $strings = array();
+  if ($function = ctools_plugin_get_function($task, 'get arguments')) {
+    $arguments = $function($task, $subtask_id);
+    ctools_include('context');
+    foreach ($arguments as $argument) {
+      if ($function = ctools_plugin_load_function('ctools', 'arguments', $argument['name'], 'criteria summary')) {
+        $id = ctools_context_id($argument, 'argument');
+        if (!isset($conf[$id])) {
+          $conf[$id] = array();
+        }
+        $strings[] = $function($conf[$id], $argument);
+      }
+    }
+  }
+
+  return $strings;
+}
+
+/**
+ * Get empty contexts for use with the delegator panels pages.
+ */
+function ctools_context_handler_placeholders($task, $subtask_id) {
+  if ($function = ctools_plugin_get_function($task, 'get context placeholders')) {
+    return $function($task, $subtask_id);
+  }
+
+  return array();
+}
+
+/**
+ * Form to choose context based selection criteria for a task handler.
+ *
+ * The configuration will be assumed to go simply in $handler->conf and
+ * will be keyed by the argument ID.
+ */
+function ctools_context_handler_edit_criteria(&$form, &$form_state) {
+  // All 'context' type tasks are required to implement this function.
+  $form_state['arguments'] = array();
+  if ($function = ctools_plugin_get_function($form_state['task'], 'get arguments')) {
+    $arguments = $function($form_state['task'], $form_state['subtask_id']);
+
+    ctools_include('context');
+    foreach ($arguments as $argument) {
+      if ($function = ctools_plugin_load_function('ctools', 'arguments', $argument['name'], 'criteria form')) {
+        $id = ctools_context_id($argument, 'argument');
+        if (!isset($form_state['handler']->conf[$id])) {
+          $form_state['handler']->conf[$id] = array();
+        }
+
+        $form[$id] = array('#tree' => TRUE);
+        $function($form, $form_state, $form_state['handler']->conf[$id], $argument, $id);
+      }
+    }
+    $form_state['arguments'] = $arguments;
+  }
+}
+
+/**
+ * Validate handler for criteria selection
+ */
+function ctools_context_handler_edit_criteria_validate(&$form, &$form_state) {
+  foreach ($form_state['arguments'] as $argument) {
+    if ($function = ctools_plugin_load_function('ctools', 'arguments', $argument['name'], 'criteria form validate')) {
+      $id = ctools_context_id($argument, 'argument');
+      $function($form, $form_state, $form_state['handler']->conf[$id], $argument, $id);
+    }
+  }
+}
+
+/**
+ * Submit handler for criteria selection
+ */
+function ctools_context_handler_edit_criteria_submit(&$form, &$form_state) {
+  foreach ($form_state['arguments'] as $argument) {
+    $id = ctools_context_id($argument, 'argument');
+    if ($function = ctools_plugin_load_function('ctools', 'arguments', $argument['name'], 'criteria form submit')) {
+      $function($form, $form_state, $form_state['handler']->conf[$id], $argument, $id);
+    }
+
+    if (isset($form_state['values'][$id])) {
+      $form_state['handler']->conf[$id] = $form_state['values'][$id];
+    }
+  }
+}
diff --git a/includes/context.inc b/includes/context.inc
index 0a3bfcda..7fc9ccd3 100644
--- a/includes/context.inc
+++ b/includes/context.inc
@@ -407,6 +407,10 @@ function ctools_context_id($context, $type = 'context') {
 function ctools_context_next_id($objects, $name) {
   // Figure out which instance of this argument we're creating
   $id = 0;
+  if (!$objects) {
+    return $id;
+  }
+
   foreach ($objects as $object) {
     if (isset($object['name']) && $object['name'] == $name) {
       if ($object['id'] > $id) {
diff --git a/includes/dependent.inc b/includes/dependent.inc
index 39c3f10c..9031d334 100644
--- a/includes/dependent.inc
+++ b/includes/dependent.inc
@@ -38,8 +38,8 @@
  * @code { ctools_include('dependent'); }
  *
  * On any form item, add
- * - @code '#process' => 'ctools_dependent_process' @endcode
- * - @code '#dependency' => array('id-of-form-without-the-#' => array(list, of, values, that, make, this, gadget, visible)); @endcode
+ * - @code '#process' => array('ctools_dependent_process'), @endcode
+ * - @code '#dependency' => array('id-of-form-without-the-#' => array(list, of, values, that, make, this, gadget, visible)), @endcode
  */
 
 /**
diff --git a/includes/wizard.inc b/includes/wizard.inc
index 1a506534..80862c0f 100644
--- a/includes/wizard.inc
+++ b/includes/wizard.inc
@@ -245,11 +245,21 @@ function ctools_wizard_wrapper(&$form, &$form_state) {
     $form['buttons']['cancel'] = array(
       '#type' => 'submit',
       '#value' => isset($form_info['cancel text']) ? $form_info['cancel text'] : t('Cancel'),
-      '#submit' => array('ctools_wizard_cancel'),
       '#wizard type' => 'cancel',
+      // hardcode the submit so that it doesn't try to save data.
+      '#submit' => array('ctools_wizard_submit'),
     );
   }
 
+  // Set up optional validate handlers.
+  $form['#validate'] = array();
+  if (function_exists($info['form id'] . '_validate')) {
+    $form['#validate'][] = $info['form id'] . '_validate';
+  }
+  if (isset($info['validate']) && function_exists($info['validate'])) {
+    $form['#validate'][] = $info['validate'];
+  }
+
   // Set up our submit handler after theirs. Since putting something here will
   // skip Drupal's autodetect, we autodetect for it.
 
@@ -259,6 +269,9 @@ function ctools_wizard_wrapper(&$form, &$form_state) {
   if (function_exists($info['form id'] . '_submit')) {
     $form['#submit'][] = $info['form id'] . '_submit';
   }
+  if (isset($info['submit']) && function_exists($info['submit'])) {
+    $form['#submit'][] = $info['submit'];
+  }
   $form['#submit'][] = 'ctools_wizard_submit';
 
   if (!empty($form_state['modal'])) {
@@ -280,14 +293,15 @@ function ctools_wizard_submit(&$form, &$form_state) {
       }
     }
     else {
-      if ($type == 'return' || $type == 'finish') {
-        if (isset($form_state['form_info']['return path'])) {
-          $form_state['redirect'] = $form_state['form_info']['return path'];
-        }
+      if ($type == 'cancel' && isset($form_state['form_info']['cancel path'])) {
+        $form_state['redirect'] = $form_state['form_info']['return path'];
       }
-      else {
+      else if ($type == 'next') {
         $form_state['redirect'] = ctools_wizard_get_path($form_state['form_info'], $form_state['clicked_button']['#next']);
       }
+      else if (isset($form_state['form_info']['return path'])) {
+        $form_state['redirect'] = $form_state['form_info']['return path'];
+      }
     }
   }
 }
diff --git a/plugins/arguments/nid.inc b/plugins/arguments/nid.inc
index 7fc11503..2dc7622c 100644
--- a/plugins/arguments/nid.inc
+++ b/plugins/arguments/nid.inc
@@ -18,6 +18,7 @@ function ctools_nid_ctools_arguments() {
     'context' => 'ctools_argument_nid_context',
     'criteria form' => 'ctools_argument_nid_criteria_form',
     'criteria select' => 'ctools_argument_nid_criteria_select',
+    'criteria summary' => 'ctools_argument_nid_criteria_summary',
   );
   return $args;
 }
@@ -87,3 +88,24 @@ function ctools_argument_nid_criteria_select($conf, $context) {
 
   return TRUE;
 }
+
+/**
+ * Provide a summary of the criteria for selecting this node.
+ */
+function ctools_argument_nid_criteria_summary($conf, $argument) {
+  if (!isset($conf['type'])) {
+    $conf['type'] = array();
+  }
+  $types = node_get_types();
+
+  $names = array();
+  foreach (array_filter($conf['type']) as $type) {
+    $names[] = check_plain($types[$type]->name);
+  }
+
+  if (empty($names)) {
+    return t('@identifier can be any node type', array('@identifier' => $argument['identifier']));
+  }
+
+  return format_plural(count($names), '@identifier can be type "@types"', '@identifier can be types "@types"', array('@types' => implode(', ', $names), '@identifier' => $argument['identifier']));
+}
diff --git a/plugins/arguments/uid.inc b/plugins/arguments/uid.inc
index c310b04a..03710520 100644
--- a/plugins/arguments/uid.inc
+++ b/plugins/arguments/uid.inc
@@ -19,6 +19,7 @@ function ctools_uid_ctools_arguments() {
     'context' => 'ctools_argument_uid_context',
     'criteria form' => 'ctools_argument_uid_criteria_form',
     'criteria select' => 'ctools_argument_uid_criteria_select',
+    'criteria summary' => 'ctools_argument_uid_criteria_summary',
   );
   return $args;
 }
@@ -87,3 +88,24 @@ function ctools_argument_uid_criteria_select($conf, $context) {
 
   return array_intersect($rids, $roles);
 }
+
+/**
+ * Provide a summary of the criteria for selecting this node.
+ */
+function ctools_argument_uid_criteria_summary($conf, $argument) {
+  if (!isset($conf['rids'])) {
+    $conf['rids'] = array();
+  }
+  $roles = ctools_get_roles();
+
+  $names = array();
+  foreach (array_filter($conf['rids']) as $rid) {
+    $names[] = check_plain($roles[$rid]);
+  }
+
+  if (empty($names)) {
+    return t('@identifier can have any role', array('@identifier' => $argument['identifier']));
+  }
+
+  return format_plural(count($names), '@identifier must have role "@roles"', '@identifier can be one of "@roles"', array('@roles' => implode(', ', $names), '@identifier' => $argument['identifier']));
+}
-- 
GitLab