This is a backport to CCK-2 of the CCK-3 patch from: http://drupal.org/node/300084

diff -Nurp ../cck.orig/includes/content.admin.inc ./includes/content.admin.inc
--- ../cck.orig/includes/content.admin.inc	2009-11-02 15:21:24.000000000 -0600
+++ ./includes/content.admin.inc	2010-05-05 10:03:51.000000000 -0500
@@ -147,6 +147,7 @@ function content_field_overview_form(&$f
   if (module_exists('fieldgroup')) {
     $groups = fieldgroup_groups($type['type']);
     $group_types = fieldgroup_types();
+    $plain_tree = _fieldgroup_plain_tree($groups);
     $group_options = _fieldgroup_groups_label($type['type']);
     // Add the ability to group under the newly created row.
     $group_options['_add_new_group'] = '_add_new_group';
@@ -194,6 +195,8 @@ function content_field_overview_form(&$f
 
   // Groups.
   foreach ($groups as $name => $group) {
+    $current_group_options = $plain_tree;
+    unset($current_group_options[$name]);
     $weight = $group['weight'];
     $form[$name] = array(
       'label' => array('#value' => check_plain($group['label'])),
@@ -202,17 +205,24 @@ function content_field_overview_form(&$f
       'configure' => array('#value' => l(t('Configure'), 'admin/content/node-type/'. $type['url_str'] .'/groups/'. $group['group_name'])),
       'remove' => array('#value' => l(t('Remove'), 'admin/content/node-type/'. $type['url_str'] .'/groups/'. $group['group_name'] .'/remove')),
       'weight' => array('#type' => 'textfield', '#default_value' => $weight, '#size' => 3),
-      'parent' => array('#type' => 'hidden', '#default_value' => ''),
+      'parent' => array('#type' => 'select', '#options' => $current_group_options, '#default_value' => ''),
+      'prev_parent' => array('#type' => 'hidden', '#value' => ''),
       'hidden_name' => array('#type' => 'hidden', '#default_value' => $group['group_name']),
-      '#root' => TRUE,
       '#row_type' => 'group',
       'group' => array('#type' => 'value', '#value' => $group),
     );
     // Adjust child fields rows.
-    foreach ($group['fields'] as $field_name => $field) {
-      $form[$field_name]['parent']['#default_value'] = $name;
-      $form[$field_name]['prev_parent']['#value'] = $name;
+    if (isset($group['fields'])) {
+      foreach ($group['fields'] as $field_name => $field) {
+        $form[$field_name]['parent']['#default_value'] = $name;
+        $form[$field_name]['prev_parent']['#value'] = $name;
+      }
     }
+
+    // Adjust child group rows
+    $form[$name]['parent']['#default_value'] = $group['parent_group_name'];
+    $form[$name]['prev_parent']['#value'] = $group['parent_group_name'];
+
     $form['#group_rows'][] = $name;
     $weights[] = $weight;
   }
@@ -317,6 +327,9 @@ function content_field_overview_form(&$f
 
   // Additional row : add new group.
   if (!empty($group_types)) {
+    $current_group_options = $group_options;
+    $options = fieldgroup_types();
+    unset($current_group_options[_add_new_group]);
     $weight++;
     $name = '_add_new_group';
     $form[$name] = array(
@@ -346,9 +359,9 @@ function content_field_overview_form(&$f
         '#value' => 'standard',
       ),
       'weight' => array('#type' => 'textfield', '#default_value' => $weight, '#size' => 3),
-      'parent' => array('#type' => 'hidden', '#default_value' => ''),
+      'parent' => array('#type' => 'select', '#options' => $current_group_options, '#default_value' => ''),
+      'prev_parent' => array('#type' => 'hidden', '#value' => ''),
       'hidden_name' => array('#type' => 'hidden', '#default_value' => $name),
-      '#root' => TRUE,
       '#add_new' => TRUE,
       '#row_type' => 'add_new_group',
     );
@@ -662,6 +675,7 @@ function content_display_overview_form(&
     $form[$name] = array(
       'human_name' => array('#value' => check_plain($group['label'])),
       'weight' => array('#type' => 'value', '#value' => $weight),
+      'parent' => array('#type' => 'value', '#value' => ''),
     );
     if ($contexts_selector == 'basic') {
       $form[$name]['label'] = array(
@@ -686,6 +700,8 @@ function content_display_overview_form(&
     foreach ($group['fields'] as $field_name => $field) {
       $form[$field_name]['parent']['#value'] = $name;
     }
+    $form[$name]['parent']['#value'] = $group['parent_group_name'];
+    $form[$name]['group']['#value']['depth'] = $group['depth'];
   }
 
   $form['submit'] = array('#type' => 'submit', '#value' => t('Save'));
@@ -1667,6 +1683,25 @@ function _content_overview_order(&$form,
       }
     }
   }
+
+  // Nested fieldgroup
+  if (module_exists('fieldgroup')) {
+    // readjust the depth and parenting of fieldgroup
+    foreach ($group_rows as $name) {
+      if ($parent = $form[$name]['parent']['#value']) {
+        $form[$name]['#depth'] = $form[$parent]['#depth'] + 1;
+        $dummy[$parent][$name] = $dummy[$name];
+        unset($dummy[$name]);
+      }
+    }
+    // readjust the depth
+    foreach ($field_rows as $name) {
+      if ($parent = $form[$name]['parent']['#value']) {
+        $form[$name]['#depth'] = $form[$parent]['#depth'] + 1;
+      }
+    }
+  }
+
   return $dummy ? explode(' ', trim(drupal_render($dummy))) : array();
 }
 
diff -Nurp ../cck.orig/includes/content.node_form.inc ./includes/content.node_form.inc
--- ../cck.orig/includes/content.node_form.inc	2009-07-17 19:40:20.000000000 -0500
+++ ./includes/content.node_form.inc	2010-05-05 10:03:51.000000000 -0500
@@ -325,12 +325,7 @@ function content_add_more_js($type_name_
   drupal_alter('form', $form_element, array(), 'content_add_more_js');
 
   // Add the new element at the right place in the (original, unbuilt) form.
-  if (module_exists('fieldgroup') && ($group_name = _fieldgroup_field_get_group($type['type'], $field_name))) {
-    $form[$group_name][$field_name] = $form_element[$field_name];
-  }
-  else {
-    $form[$field_name] = $form_element[$field_name];
-  }
+  content_set_form_element($field_name, $type['type'], $form, $form_element[$field_name]);
 
   // Save the new definition of the form.
   $form_state['values'] = array();
@@ -348,7 +343,7 @@ function content_add_more_js($type_name_
   $form = form_builder($_POST['form_id'], $form, $form_state);
 
   // Render the new output.
-  $field_form = (!empty($group_name)) ? $form[$group_name][$field_name] : $form[$field_name];
+  $field_form = content_get_form_element($field_name, $type['type'], $form);
   // We add a div around the new content to receive the ahah effect.
   $field_form[$delta]['#prefix'] = '<div class="ahah-new-content">'. (isset($field_form[$delta]['#prefix']) ? $field_form[$delta]['#prefix'] : '');
   $field_form[$delta]['#suffix'] = (isset($field_form[$delta]['#suffix']) ? $field_form[$delta]['#suffix'] : '') .'</div>';
@@ -372,3 +367,74 @@ function content_add_more_js($type_name_
   print drupal_to_js(array('status' => TRUE, 'data' => $output));
   exit;
 }
+
+/**
+ * Store an element into a form.
+ *
+ * @param $name
+ *   The field name.
+ * @param $type
+ *   The content type where the field instance belongs to.
+ * @param $form
+ *   The form to store this field element into.
+ * @param $element
+ *   The form element to store.
+ */
+function content_set_form_element($name, $type, &$form, $element, $is_group = FALSE) {
+  if (module_exists('fieldgroup') && ($parents = _fieldgroup_field_get_parents($type, $name, $is_group))) {
+    foreach (module_implements('fieldgroup_parents_alter') as $module) {
+      $parents = call_user_func($module .'_fieldgroup_parents_alter', $form, $parents, $type, $name);
+    }
+    $reference = &$form;
+    if ($is_group) {
+      array_shift($parents);
+      $parents = array_reverse($parents);
+    }
+    else {
+      $parents = array_reverse($parents);
+    }
+
+    foreach (array_values($parents) as $group_name) {
+      $reference = &$reference[$group_name];
+    }
+
+    $reference[$name] = $element;
+
+  }
+  else {
+    $form[$name] = $element;
+  }
+}
+
+/**
+ * Retrieve an element from a form.
+ *
+ * @param $name
+ *   The field name.
+ * @param $type
+ *   The content type where the field instance belongs to.
+ * @param $form
+ *   The form to retrieve this field element from.
+ */
+function content_get_form_element($name, $type, $form, $is_group = FALSE) {
+  if (module_exists('fieldgroup') && ($parents = _fieldgroup_field_get_parents($type, $name, $is_group))) {
+    foreach (module_implements('fieldgroup_parents_alter') as $module) {
+      $parents = call_user_func($module .'_fieldgroup_parents_alter', $form, $parents, $type, $name);
+    }
+    $reference = &$form;
+    if ($is_group) {
+      array_shift($parents);
+      $parents = array_reverse($parents);
+    }
+    else {
+      $parents = array_reverse($parents);
+    }
+
+    foreach (array_values($parents) as $group_name) {
+      $reference = &$reference[$group_name];
+    }
+
+    return $reference[$name];
+  }
+  return $form[$name];
+}
diff -Nurp ../cck.orig/js/content.node_form.js ./js/content.node_form.js
--- ../cck.orig/js/content.node_form.js	1969-12-31 18:00:00.000000000 -0600
+++ ./js/content.node_form.js	2010-05-05 10:03:51.000000000 -0500
@@ -0,0 +1,255 @@
+// $Id: content.node_form.js,v 1.1.2.1 2009/06/06 23:44:56 markuspetrux Exp $
+
+/**
+ * Private namespace for local methods.
+ */
+Drupal.contentRemoveButtons = Drupal.contentRemoveButtons || {};
+
+/**
+ * Manipulation of content remove buttons.
+ *
+ * TableDrag objects for multiple value fields (and multigroups) are scanned
+ * to find 'remove' checkboxes. These checkboxes are hidden when javascript is
+ * enabled (using the Global CSS Killswitch, html.js, defined in drupal.js).
+ * A new 'remove' button is created here in place of these checkboxes aimed to
+ * provide a more user-friendly method to remove items.
+ */
+Drupal.behaviors.contentRemoveButtons = function(context) {
+  var self = Drupal.contentRemoveButtons;
+
+  $('table.content-multiple-table', context).not('.content-remove-buttons-processed').addClass('content-remove-buttons-processed').each(function() {
+    var table = this, tableDrag = Drupal.tableDrag[$(table).attr('id')];
+
+    // Replace remove checkboxes with buttons.
+    $('input.content-multiple-remove-checkbox', table).each(function() {
+      var $checkbox = $(this), $row = $checkbox.parents('tr:first');
+      var isRemoved = $checkbox.attr('checked');
+      var $button = $(Drupal.theme('contentRemoveButton', tableDrag.getRemoveButtonTitle(isRemoved)));
+
+      // Bind the onClick event to the remove button.
+      $button.bind('click', function(event) {
+        self.onClick($button, $checkbox, $row, tableDrag);
+        return false;
+      });
+
+      // Attach the new button to the DOM tree.
+      $checkbox.parent().append($button);
+
+      // If the row is removed, then hide the contents of the cells and show
+      // the removed warning on the cell next to the drag'n'drop cell.
+      if (isRemoved) {
+        self.getCellWrappers($row).hide();
+        self.showRemovedWarning($row, tableDrag);
+
+        // FAPI not rendering the form on errors - case #1:
+        // If the form has been submitted and any error was found, FAPI will
+        // send back the same exact form that was submitted to show the error
+        // messages, but it will not invoke the rendering engine which is where
+        // we actually assign the removed class to the row, so we need to check
+        // this situation here and add the class if it is not present.
+        if (!$row.hasClass('content-multiple-removed-row')) {
+          $row.addClass('content-multiple-removed-row');
+        }
+      }
+      else {
+        // FAPI not rendering the form on errors - case #2:
+        // Similar issue than #1, but this time caused when user removes an
+        // item, previews, FAPI renders the new form with the removed class,
+        // then user changes anything in the form that causes an error, and
+        // also restores the previously removed item. This time, FAPI will
+        // send the form validation error with the item not flagged for removal
+        // but having the removed class that was present when the form was
+        // rendered in the previous step. So we need to remove this class here,
+        // if present, because the item is not really flagged for removal.
+        if ($row.hasClass('content-multiple-removed-row')) {
+          $row.removeClass('content-multiple-removed-row');
+        }
+      }
+    });
+  });
+};
+
+/**
+ * onClick handler for remove buttons.
+ *
+ * @param $button
+ *   The jQuery object of the remove button.
+ * @param $checkbox
+ *   The jQuery object of the remove checkbox.
+ * @param $row
+ *   The jQuery object of the table row.
+ * @param tableDrag
+ *   The tableDrag object where the row is.
+ */
+Drupal.contentRemoveButtons.onClick = function($button, $checkbox, $row, tableDrag) {
+  var self = Drupal.contentRemoveButtons;
+
+  // Prevent the user from firing this event while another one is still being
+  // processed. This flag is (should be) restored at end of animations.
+  // Note that this technique is required because the browser may experience
+  // delays while performing the animation, for whatever reason, and if this
+  // process it fired more than once at the same time for the same row, then
+  // it may cause unexpected behavior because the state of the elements being
+  // manipulated would be unknown.
+  if ($row.animating) {
+    return;
+  }
+  $row.animating = true;
+
+  // Toggle the state of the checkbox.
+  var isRemoved = !$checkbox.attr('checked');
+  $checkbox.attr('checked', isRemoved);
+
+  // Toggle the row class.
+  if (isRemoved) {
+    $row.addClass('content-multiple-removed-row');
+  }
+  else {
+    $row.removeClass('content-multiple-removed-row');
+  }
+
+  // Toggle the button title.
+  $button.attr('title', tableDrag.getRemoveButtonTitle(isRemoved));
+
+  // Get the list of cell wrappers in this row.
+  var $cellWrappers = self.getCellWrappers($row);
+
+  // If for whatever reason this row doesn't have cells with elements,
+  // then we are done, but we still need to reset the global busy flag
+  // and display the tableDrag changed warning.
+  if (!$cellWrappers.size()) {
+    tableDrag.displayChangedWarning();
+    $row.animating = false;
+    return;
+  }
+
+  // Toggle the visible state of the row cells.
+  $cellWrappers.each(function() {
+    var $cellWrapper = $(this);
+
+    // Drop the removed warning during restore operation.
+    if (!isRemoved) {
+      self.hideRemovedWarning($row);
+    }
+
+    // Toggle the visibility state of the contents of cells.
+    $cellWrapper.animate({opacity: (isRemoved ? 'hide' : 'show')}, 'fast', function() {
+      var $cell = $cellWrapper.parent();
+
+      // Show the removed warning during remove operation.
+      if (isRemoved && $cell.prev(':first').hasClass('content-multiple-drag')) {
+        self.showRemovedWarning($row, tableDrag);
+      }
+
+      // Disable the busy flag when animation of last cell has finished.
+      if ($cell.next(':first').hasClass('delta-order')) {
+        tableDrag.displayChangedWarning();
+        $row.animating = false;
+      }
+    });
+  });
+};
+
+/**
+ * Show the removed warning on the given row.
+ *
+ * @param $row
+ *   The jQuery object of the table row.
+ * @param tableDrag
+ *   The tableDrag object where the row is.
+ */
+Drupal.contentRemoveButtons.showRemovedWarning = function($row, tableDrag) {
+  $('.content-multiple-drag', $row).next(':first').append(Drupal.theme('contentRemovedWarning', tableDrag.getRemovedWarning()));
+};
+
+/**
+ * Hide the removed warning from the given row.
+ *
+ * @param $row
+ *   The jQuery object of the table row.
+ */
+Drupal.contentRemoveButtons.hideRemovedWarning = function($row) {
+  if ($('.content-multiple-removed-warning', $row).size()) {
+    $('.content-multiple-removed-warning', $row).remove();
+  }
+};
+
+/**
+ * Get cell wrappers for the given row.
+ *
+ * @param $row
+ *   The jQuery object of the table row.
+ */
+Drupal.contentRemoveButtons.getCellWrappers = function($row) {
+  // Create cell wrappers if this row has not already been processed.
+  if (!$('.content-multiple-cell-content-wrapper', $row).size()) {
+    // Wrap the contents of all cells (except the drag'n'drop, weight and
+    // remove button cells) with a dummy block element. This operation makes
+    // animations faster because we just need to show/hide a single element
+    // per cell, and it also prevents from creating more than one warning
+    // element per row.
+    $row.children('td:not(.content-multiple-drag):not(.delta-order):not(.content-multiple-remove-cell)').each(function() {
+      var $cell = $(this);
+      $cell.wrapInner('<div class="content-multiple-cell-content-wrapper"/>');
+    });
+  }
+  return $('.content-multiple-cell-content-wrapper', $row);
+};
+
+/**
+ * Display table change warning when appropriate.
+ */
+Drupal.tableDrag.prototype.displayChangedWarning = function() {
+  if (this.changed == false) {
+    $(Drupal.theme('tableDragChangedWarning')).insertAfter(this.table).hide().fadeIn('slow');
+    this.changed = true;
+  }
+};
+
+/**
+ * Get the title of the remove button.
+ *
+ * This method is an extension of the tableDrag class. This means a separate
+ * module can override this method for a particular tableDrag instance. For
+ * example, the multigroup module can change the text to read 'Remove this
+ * group of items', another module could change it to 'Remove this image',
+ * and so on...
+ * To override this function:
+ *
+ * @code
+ *  var tableId = $(table).attr('id');
+ *  Drupal.tableDrag[tableId].getRemoveButtonTitle = function(isRemoved) {
+ *    return (isRemoved ? Drupal.t('Restore this foo') : Drupal.t('Remove this foo'));
+ *  };
+ * @endcode
+ *
+ * @param isRemoved
+ *   A flag that indicates the state of the button.
+ */
+Drupal.tableDrag.prototype.getRemoveButtonTitle = function(isRemoved) {
+  return (isRemoved ? Drupal.t('Restore this item') : Drupal.t('Remove this item'));
+};
+
+/**
+ * Get the item removed warning.
+ *
+ * This method is an extension of the tableDrag class. It can be overridden by
+ * a separate module. See getRemoveButtonTitle() for further information.
+ */
+Drupal.tableDrag.prototype.getRemovedWarning = function() {
+  return Drupal.t('Removed');
+};
+
+/**
+ * Theme the remove button.
+ */
+Drupal.theme.prototype.contentRemoveButton = function(title) {
+  return '<a href="javascript:void(0)" class="content-multiple-remove-button" title="'+ title +'"></a>';
+};
+
+/**
+ * Theme the item removed warning.
+ */
+Drupal.theme.prototype.contentRemovedWarning = function(warning) {
+  return '<div class="content-multiple-removed-warning">'+ warning +'</div>';
+};
diff -Nurp ../cck.orig/modules/fieldgroup/fieldgroup.install ./modules/fieldgroup/fieldgroup.install
--- ../cck.orig/modules/fieldgroup/fieldgroup.install	2008-12-26 05:51:46.000000000 -0600
+++ ./modules/fieldgroup/fieldgroup.install	2010-05-05 10:03:51.000000000 -0500
@@ -52,6 +52,7 @@ function fieldgroup_schema() {
       'group_type' => array('type' => 'varchar', 'length' => 32, 'not null' => TRUE, 'default' => 'standard'),
       'type_name'  => array('type' => 'varchar', 'length' => 32, 'not null' => TRUE, 'default' => ''),
       'group_name' => array('type' => 'varchar', 'length' => 32, 'not null' => TRUE, 'default' => ''),
+      'parent_group_name' => array('type' => 'varchar', 'length' => 32, 'not null' => FALSE, 'default' => ''),
       'label'      => array('type' => 'varchar', 'length' => 255, 'not null' => TRUE, 'default' => ''),
       'settings'   => array('type' => 'text', 'size' => 'medium', 'not null' => TRUE),
       'weight'     => array('type' => 'int', 'not null' => TRUE, 'default' => 0),
@@ -313,4 +314,16 @@ function fieldgroup_update_6007() {
   $ret = array();
   $ret[] = update_sql("DELETE FROM {content_group_fields} WHERE (field_name, type_name) NOT IN (SELECT field_name, type_name FROM {content_node_field_instance})");
   return $ret;
-}
\ No newline at end of file
+}
+
+/**
+ * allow for nesting of fieldgroups
+ */
+function fieldgroup_update_6008() {
+  if ($abort = content_check_update('fieldgroup')) {
+    return $abort;
+  }
+  $ret = array();
+  db_add_field($ret, 'content_group', 'parent_group_name', array('type' => 'varchar', 'length' => 32, 'not null' => FALSE, 'default' => ''));
+  return $ret;
+}
diff -Nurp ../cck.orig/modules/fieldgroup/fieldgroup.module ./modules/fieldgroup/fieldgroup.module
--- ../cck.orig/modules/fieldgroup/fieldgroup.module	2009-08-14 07:17:49.000000000 -0500
+++ ./modules/fieldgroup/fieldgroup.module	2010-05-05 10:03:51.000000000 -0500
@@ -199,6 +199,7 @@ function fieldgroup_remove_group(&$form_
   $form['#content_type'] = $content_type;
   $form['#group_name'] = $group_name;
 
+
   return confirm_form($form,
                   t('Are you sure you want to remove the group %label?',
                   array('%label' => t($group['label']))),
@@ -210,6 +211,9 @@ function fieldgroup_remove_group_submit(
   $form_values = $form_state['values'];
   $content_type = $form['#content_type'];
   $group_name = $form['#group_name'];
+  $parent_group_name = db_fetch_array(db_query("SELECT parent_group_name FROM {". fieldgroup_tablename() ."} WHERE group_name = '%s' and type_name = '%s'", $group_name, $content_type['type']));
+  $result = db_query("UPDATE {". fieldgroup_tablename() ."} SET parent_group_name = '%s' WHERE parent_group_name = '%s'", $parent_group_name['parent_group_name'], $group_name);
+  $result = db_query("UPDATE {". fieldgroup_fields_tablename() ."} SET group_name = '%s' WHERE group_name = '%s'", $parent_group_name['parent_group_name'], $group_name);
   fieldgroup_delete($content_type['type'], $group_name);
   drupal_set_message(t('The group %group_name has been removed.', array('%group_name' => $group_name)));
   $form_state['redirect'] = 'admin/content/node-type/'. $content_type['url_str'] .'/fields';
@@ -227,46 +231,16 @@ function fieldgroup_groups($content_type
       $groups = $data['groups'];
       $groups_sorted = $data['groups_sorted'];
     }
-    else {
-      $result = db_query("SELECT * FROM {". fieldgroup_tablename() ."} ORDER BY weight, group_name");
+    else {;
+      $result = db_query("SELECT * FROM {". fieldgroup_tablename() ."} ORDER BY type_name, weight");
       $groups = array();
       $groups_sorted = array();
       while ($group = db_fetch_array($result)) {
-        $group['settings'] = unserialize($group['settings']);
-        $group['fields'] = array();
-
-        // Allow external modules to translate field group strings.
-        $group_strings = array(
-          'label' => $group['label'],
-          'form_description' => $group['settings']['form']['description'],
-          'display_description' => $group['settings']['display']['description'],
-        );
-        drupal_alter('content_fieldgroup_strings', $group_strings, $group['type_name'], $group['group_name']);
-        $group['label'] = $group_strings['label'];
-        $group['settings']['form']['description'] = $group_strings['form_description'];
-        $group['settings']['display']['description'] = $group_strings['display_description'];
-
-        $groups[$group['type_name']][$group['group_name']] = $group;
+        $groups[$group['type_name']] = _fieldgroup_get_tree($group['type_name']);
         $groups_sorted[$group['type_name']][] = &$groups[$group['type_name']][$group['group_name']];
       }
       //load fields
-      $result = db_query("SELECT nfi.*, ng.group_name FROM {". fieldgroup_tablename() ."} ng ".
- "INNER JOIN {". fieldgroup_fields_tablename() ."} ngf ON ngf.type_name = ng.type_name AND ngf.group_name = ng.group_name ".
- "INNER JOIN {". content_instance_tablename() ."} nfi ON nfi.field_name = ngf.field_name AND nfi.type_name = ngf.type_name ".
- "WHERE nfi.widget_active = 1 ORDER BY nfi.weight");
-      while ($field = db_fetch_array($result)) {
-        // Allow external modules to translate field strings.
-        $field_strings = array(
-          'widget_label' => $field['label'],
-          'widget_description' => $field['description'],
-        );
-        drupal_alter('content_field_strings', $field_strings, $field['type_name'], $field['field_name']);
-        $field['label'] = $field_strings['widget_label'];
-        $field['description'] = $field_strings['widget_description'];
-
-        $groups[$field['type_name']][$field['group_name']]['fields'][$field['field_name']] = $field;
-      }
-      cache_set('fieldgroup_data:'. $language->language, array('groups' => $groups, 'groups_sorted' => $groups_sorted), content_cache_tablename());
+      cache_set('fieldgroup_data', array('groups' => $groups, 'groups_sorted' => $groups_sorted), content_cache_tablename());
     }
   }
   if (empty($content_type)) {
@@ -278,6 +252,81 @@ function fieldgroup_groups($content_type
   return $sorted ? $groups_sorted[$content_type] : $groups[$content_type];
 }
 
+/**
+ * create a tree of fieldgroups for nesting them
+ */
+function _fieldgroup_get_tree($type_name, $parent_group_name = '', $depth = -1, $max_depth = null) {
+  static $children, $parents, $groups;
+
+  $depth++;
+  // We cache trees, so it's not CPU-intensive to call get_tree() on a term
+  // and its children, too.
+  if (!isset($children[$type_name])) {
+    $children[$type_name] = array();
+
+    $s = "SELECT * FROM {". fieldgroup_tablename() ."} WHERE type_name='%s' ORDER BY weight";
+    $r = db_query($s, $type_name);
+    while ($group = db_fetch_array($r)) {
+    $group['settings'] = unserialize($group['settings']);
+    $group['fields'] = array();
+
+      // Allow external modules to translate field group strings.
+    $group_strings = array(
+      'label' => $group['label'],
+    'form_description' => $group['settings']['form']['description'],
+    'display_description' => $group['settings']['display']['description'],
+    );
+    drupal_alter('content_fieldgroup_strings', $group_strings, $type_name, $group['group_name']);
+    $group['label'] = $group_strings['label'];
+    $group['settings']['form']['description'] = $group_strings['form_description'];
+    $group['settings']['display']['description'] = $group_strings['display_description'];
+
+      $children[$type_name][$group['parent_group_name']][] = $group['group_name'];
+      $parents[$type_name][$group['group_name']][] = $group['parent_group_name'];
+      $groups[$type_name][$group['group_name']] = $group;
+    }
+    //load fields
+    $result = db_query("SELECT nfi.*, ng.group_name FROM {". fieldgroup_tablename() ."} ng ".
+        "INNER JOIN {". fieldgroup_fields_tablename() ."} ngf ON ngf.type_name = ng.type_name AND ngf.group_name = ng.group_name ".
+        "INNER JOIN {". content_instance_tablename() ."} nfi ON nfi.field_name = ngf.field_name AND nfi.type_name = ngf.type_name ".
+        "WHERE nfi.widget_active = 1 ORDER BY nfi.weight");
+    while ($field = db_fetch_array($result)) {
+      $groups[$field['type_name']][$field['group_name']]['fields'][$field['field_name']] = $field;
+    }
+  }
+
+  $max_depth = (is_null($max_depth)) ? count($children[$type_name]) : $max_depth;
+  if (isset($children[$type_name][$parent_group_name])) {
+    foreach ($children[$type_name][$parent_group_name] as $child_group_name) {
+      if ($max_depth > $depth) {
+        $group = $groups[$type_name][$child_group_name];
+        $group['depth'] = $depth;
+        $group['parents'] = $parents[$type_name][$child_group_name];
+        $tree[$group['group_name']] = $group;
+        if ($children[$type_name][$child_group_name]) {
+          $tree = array_merge($tree, _fieldgroup_get_tree($type_name, $child_group_name, $depth, $max_depth));
+        }
+      }
+    }
+  }
+  return $tree ? $tree : array();
+}
+
+/**
+ * go through a set of fieldgroups and construct a simple representation of their hierarchy
+ */
+function _fieldgroup_plain_tree($items) {
+  $rows = array();
+  $rows[''] = '<'. t('none') .'>';
+  foreach ($items as $item) {
+    $group_name = $item['group_name'];
+    $label = t($item['label']);
+    if ($group_name) {
+      $rows[$group_name] = str_repeat('--', $item['depth']) . ' ' . $label;
+    }
+  }
+  return $rows;
+}
 
 function _fieldgroup_groups_label($content_type) {
   $groups = fieldgroup_groups($content_type);
@@ -293,51 +342,159 @@ function _fieldgroup_field_get_group($co
   return db_result(db_query("SELECT group_name FROM {". fieldgroup_fields_tablename() ."} WHERE type_name = '%s' AND field_name = '%s'", $content_type, $field_name));
 }
 
+function _fieldgroup_field_get_parents($content_type, $name, $is_group = FALSE) {
+  $counter = 0;
+  if ($is_group) {
+    $parents[$counter] = $name;
+  }
+  else {
+    if ($result = db_result(db_query("SELECT group_name FROM {". fieldgroup_fields_tablename() ."} WHERE type_name = '%s' AND field_name = '%s'", $content_type, $name))) {
+      $parents[$counter] = $result;
+    }
+  }
+  while ($result = db_result(db_query("SELECT parent_group_name FROM {". fieldgroup_tablename() ."} WHERE type_name = '%s' AND group_name = '%s'", $content_type, $parents[$counter]))) {
+    $counter++;
+    $parents[$counter] = $result;
+  }
+  return $parents;
+}
+
+function _fieldgroup_add_group_to_form(&$form, &$form_state, $form_id, $group_name, $group, $groups) {
+  $form[$group_name] = array(
+    '#type' => 'fieldset',
+    '#title' => check_plain(t($group['label'])),
+    '#collapsed' => $group['settings']['form']['style'] == 'fieldset_collapsed',
+    '#collapsible' => in_array($group['settings']['form']['style'], array('fieldset_collapsed', 'fieldset_collapsible')),
+    '#weight' => $group['weight'],
+    '#depth' => $group['depth'],
+    '#group_parent' => $group['parent_group_name'],
+    '#description' => content_filter_xss(t($group['settings']['form']['description'])),
+    '#attributes' => array('class' => strtr($group['group_name'], '_', '-')),
+  );
+  $has_accessible_field = FALSE;
+  foreach ($group['fields'] as $field_name => $field) {
+    if (isset($form[$field_name])) {
+      $form[$field_name]['#weight'] = $field['weight'];
+      $form[$group_name][$field_name] = $form[$field_name];
+      //Track whether this group has any accessible fields within it.
+      if (!isset($form[$field_name]['#access']) || $form[$field_name]['#access'] !== FALSE) {
+        $has_accessible_field = TRUE;
+      }
+      unset($form[$field_name]);
+    }
+  }
+  if (!empty($group['fields']) && !element_children($form[$group_name])) {
+    //hide the fieldgroup, because the fields are hidden too
+    unset($form[$group_name]);
+  }
+
+  if (!$has_accessible_field) {
+    // Hide the fieldgroup, because the fields are inaccessible.
+    $form[$group_name]['#access'] = FALSE;
+  }
+  else {
+    //cascade visibility up
+    $form[$group_name]['#access'] = TRUE;
+  }
+
+  // Allow other modules to alter the form.
+  // Can't use module_invoke_all because we want
+  // to be able to use a reference to $form and $form_state.
+  foreach (module_implements('fieldgroup_form') as $module) {
+    $function = $module .'_fieldgroup_form';
+    $function($form, $form_state, $form_id, $group);
+  }
+}
+
+
+/**
+ * This is function fieldgroup_order_fields_and_groups
+ *
+ * @param array $group_rows An empty array that we will fill.
+ * @param array $groups All of the info we need about all of the groups for the content type we're working on.
+ * @param array $field_check_off This contains the fields. We will unset them as we process them.
+ *
+ */
+function fieldgroup_order_fields_groups(&$group_rows, &$groups, &$field_check_off) {
+  $max_depth = 0;
+  foreach ($group_rows as $name) {
+    $depth = $groups[$name]['depth'];
+    if ($depth > $max_depth) {
+      $max_depth = $depth;
+    }
+    $parent = $groups[$name]['parent_group_name'];
+
+    //run through the fields and come up with new weights for display purposes
+    if (isset($groups[$name]['fields'])) {
+      foreach ($groups[$name]['fields'] as $name2 => $elements) {
+        $depth2 = $groups[$name]['depth'] + 1;
+        $groups[$name]['fields'][$name2]['depth'] = $depth2;
+        if (in_array($name2, $field_check_off)) {
+          $index = array_search($name2, $field_check_off);
+          unset($field_check_off[$index]);
+        }
+      }
+    }
+  }
+  return $max_depth;
+}
+
+
 /**
  * Implementation of hook_form_alter()
  */
 function fieldgroup_form_alter(&$form, $form_state, $form_id) {
   if (isset($form['type']) && isset($form['#node']) && $form['type']['#value'] .'_node_form' == $form_id) {
-    foreach (fieldgroup_groups($form['type']['#value']) as $group_name => $group) {
-      $form[$group_name] = array(
-        '#type' => 'fieldset',
-        '#title' => check_plain(t($group['label'])),
-        '#collapsed' => $group['settings']['form']['style'] == 'fieldset_collapsed',
-        '#collapsible' => in_array($group['settings']['form']['style'], array('fieldset_collapsed', 'fieldset_collapsible')),
-        '#weight' => $group['weight'],
-        '#description' => content_filter_xss(t($group['settings']['form']['description'])),
-        '#attributes' => array('class' => strtr($group['group_name'], '_', '-')),
-      );
-
-      $has_accessible_field = FALSE;
-      foreach ($group['fields'] as $field_name => $field) {
-        if (isset($form[$field_name])) {
-          $form[$group_name][$field_name] = $form[$field_name];
-          // Track whether this group has any accessible fields within it.
-          if (!isset($form[$field_name]['#access']) || $form[$field_name]['#access'] !== FALSE) {
-            $has_accessible_field = TRUE;
-          }
-          unset($form[$field_name]);
-        }
+    $group_rows = array();
+    $field_rows = array();
+
+    //prepare data that will make this easier
+    $groups = fieldgroup_groups($form['type']['#value']);
+    if (!empty($groups)) {
+      foreach ($groups as $name => $more) {
+        $group_rows[] = $name;
       }
-      if (!empty($group['fields']) && !element_children($form[$group_name])) {
-        //hide the fieldgroup, because the fields are hidden too
-        unset($form[$group_name]);
+    }
+
+    $fields = $form['#field_info'];
+    if (!empty($fields)) {
+      foreach ($fields as $name => $more) {
+        $field_rows[] = $name;
       }
+    }
+
+    $max_depth = fieldgroup_order_fields_groups($group_rows, $groups, $field_rows);
 
-      if (!$has_accessible_field) {
-        // Hide the fieldgroup, because the fields are inaccessible.
-        $form[$group_name]['#access'] = FALSE;
+    //cover the top level fields that aren't in fieldgroups
+    if (isset($field_rows)) {
+      foreach ($field_rows as $name) {
+        $form[$name]['#depth'] = 0;
       }
+    }
 
-      // Allow other modules to alter the form.
-      // Can't use module_invoke_all because we want
-      // to be able to use a reference to $form and $form_state.
-      foreach (module_implements('fieldgroup_form') as $module) {
-        $function = $module .'_fieldgroup_form';
-        $function($form, $form_state, $form_id, $group);
+    //now that we have the order of things as we want them, let's create the fieldsets for the fieldgroups
+    foreach ($groups as $group_name => $group) {
+      _fieldgroup_add_group_to_form($form, $form_state, $form_id, $group_name, $group, $groups);
+    }
+
+    //reorder the groups from the inside-out in order to avoid a recursive function
+    while ($max_depth >= 0) {
+      foreach ($group_rows as $name) {
+        if ($form[$name]['#depth'] == $max_depth) {
+          $parent = $form[$name]['#group_parent'];
+          if (isset($parent) && $parent != '') {
+            $form[$parent][$name] = $form[$name];
+            if ($form[$name]['#access']) {
+              $form[$parent]['#access'] = TRUE;
+            }
+            unset($form[$name]);
+            $index = array_search($name, $group_rows);
+            unset($group_rows[$index]);
+          }
+        }
       }
 
+      $max_depth--;
     }
 
   }
@@ -463,6 +620,8 @@ function fieldgroup_field_overview_form_
   // Parse incoming rows.
   $add_field_rows = array('_add_new_field', '_add_existing_field');
   $field_rows = array_merge($form['#fields'], $add_field_rows);
+  $add_group_rows = array($new_group_name);
+  $group_rows = array_merge($form['#groups'], $add_group_rows);
   foreach ($form_values as $key => $values) {
     // If 'field' row: update field parenting.
     if (in_array($key, $field_rows)) {
@@ -487,11 +646,15 @@ function fieldgroup_field_overview_form_
       fieldgroup_update_fields(array('field_name' => $key, 'group' => $parent, 'type_name' => $type_name));
     }
 
-    // If 'group' row:  update groups weights
+  }
+
+  foreach ($form_state['values'] as $key => $values) {
+    // If 'group' row:  update groups weights and parent
     // (possible newly created group has already been taken care of).
-    elseif (in_array($key, $form['#groups'])) {
-      db_query("UPDATE {". fieldgroup_tablename() ."} SET weight = %d WHERE type_name = '%s' AND group_name = '%s'",
-        $values['weight'], $type_name, $key);
+    if (in_array($key, $group_rows)) {
+      $parent = ($values['parent'] == '_add_new_group' && isset($new_group_name)) ? $new_group_name : $values['parent'];
+      $weight = $values['weight'];
+      db_query("UPDATE {". fieldgroup_tablename() ."} SET weight = %d, parent_group_name = '%s' WHERE type_name = '%s' AND group_name = '%s'", $weight, $parent, $type_name, $key);
     }
   }
 
@@ -543,10 +706,50 @@ function fieldgroup_nodeapi(&$node, $op,
     case 'view':
       // Prevent against invalid 'nodes' built by broken 3rd party code.
       if (isset($node->type)) {
+        //prepare data that will make this easier
+        $group_rows = array();
+        $field_rows = array();
+        $groups = fieldgroup_groups($node->type);
+        if (!empty($groups)) {
+          foreach ($groups as $name => $more) {
+            $group_rows[] = $name;
+          }
+        }
+
+        $fields = $node->content;
+        if (!empty($fields)) {
+          foreach ($fields as $name => $more) {
+            if (strstr($name, 'field_')) {
+              $field_rows[] = $name;
+            }
+          }
+        }
+
+        $max_depth = fieldgroup_order_fields_groups($group_rows, $groups, $field_rows);
+
+        //cover the top level fields that aren't in fieldgroups
+        if (isset($field_rows)) {
+          foreach ($field_rows as $name) {
+            $node->content[$name]['#depth'] = 0;
+          }
+        }
         // Build the node content element needed to render each fieldgroup.
-        foreach (fieldgroup_groups($node->type) as $group) {
+        foreach ($groups as $group) {
           fieldgroup_build_content($group, $node, $teaser, $page);
         }
+        //reorder the groups from the inside-out in order to avoid writing a recursive function
+        while ($max_depth >= 0) {
+          foreach ($group_rows as $name) {
+            if ($node->content[$name]['#depth'] == $max_depth) {
+              $parent = $node->content[$name]['#group_parent'];
+              if (isset($parent) && $parent != '') {
+                $node->content[$parent]['group'][$name] = $node->content[$name];
+                unset($node->content[$name]);
+              }
+            }
+          }
+          $max_depth--;
+        }
       }
       break;
   }
@@ -611,6 +814,8 @@ function fieldgroup_build_content($group
   foreach ($group['fields'] as $field_name => $field) {
     if (isset($node->content[$field_name])) {
       $element[$field_name] = $node->content[$field_name];
+      $element[$field_name]['#weight'] = $field['weight'];
+      $element[$field_name]['#depth'] = $field['depth'];
     }
   }
 
@@ -636,10 +841,12 @@ function fieldgroup_build_content($group
   $wrapper = array(
     'group' => $element,
     '#weight' => $group['weight'],
+    '#depth' => $group['depth'],
     '#post_render' => array('fieldgroup_wrapper_post_render'),
     '#group_name' => $group_name,
     '#type_name' => $node->type,
     '#context' => $context,
+    '#group_parent' => $group['parent_group_name'],
   );
 
   $node->content[$group_name] = $wrapper;
@@ -806,15 +1013,16 @@ function fieldgroup_save_group($type_nam
 
   if (!isset($groups[$group['group_name']])) {
     // Accept group name from programmed submissions if valid.
-    db_query("INSERT INTO {". fieldgroup_tablename() ."} (group_type, type_name, group_name, label, settings, weight)".
-      " VALUES ('%s', '%s', '%s', '%s', '%s', %d)", $group['group_type'], $type_name, $group['group_name'], $group['label'], serialize($group['settings']), $group['weight']);
+    db_query("INSERT INTO {". fieldgroup_tablename() ."} (parent_group_name, group_type, type_name, group_name, label, settings, weight)".
+      " VALUES ('%s','%s', '%s', '%s', '%s', '%s', %d)",
+      isset($group['parent']) ? $group['parent'] : $group['parent_group_name'], $group['group_type'], $type_name, $group['group_name'], $group['label'], serialize($group['settings']), $group['weight']);
     cache_clear_all('fieldgroup_data:', content_cache_tablename(), TRUE);
     return SAVED_NEW;
   }
   else {
-    db_query("UPDATE {". fieldgroup_tablename() ."} SET group_type = '%s', label = '%s', settings = '%s', weight = %d ".
-             "WHERE type_name = '%s' AND group_name = '%s'",
-             $group['group_type'], $group['label'], serialize($group['settings']), $group['weight'], $type_name, $group['group_name']);
+    db_query("UPDATE {". fieldgroup_tablename() ."} SET parent_group_name =  '%s', group_type = '%s', label = '%s', settings = '%s', weight = %d ".
+      "WHERE type_name = '%s' AND group_name = '%s'",
+      isset($group['parent']) ? $group['parent'] : $group['parent_group_name'], $group['group_type'], $group['label'], serialize($group['settings']), $group['weight'], $type_name, $group['group_name']);
     cache_clear_all('fieldgroup_data:', content_cache_tablename(), TRUE);
     return SAVED_UPDATED;
   }
@@ -884,6 +1092,7 @@ function theme_fieldgroup_fieldset($elem
 function fieldgroup_preprocess_fieldgroup_simple(&$vars) {
   $element = $vars['element'];
 
+  $vars['parent_group_name'] = $element['#parent_group_name'];
   $vars['group_name'] = $element['#group_name'];
   $vars['group_name_css'] = strtr($element['#group_name'], '_', '-');
   $vars['label'] = isset($element['#title']) ? $element['#title'] : '';;
@@ -910,4 +1119,4 @@ function fieldgroup_preprocess_node(&$va
     // '#chilren' might not be set if the group is empty.
     $vars[$group_name .'_rendered'] = isset($node->content[$group_name]['#children']) ? $node->content[$group_name]['#children'] : '';
   }
-}
\ No newline at end of file
+}
diff -Nurp ../cck.orig/modules/fieldgroup/fieldgroup.tabledrag.js ./modules/fieldgroup/fieldgroup.tabledrag.js
--- ../cck.orig/modules/fieldgroup/fieldgroup.tabledrag.js	1969-12-31 18:00:00.000000000 -0600
+++ ./modules/fieldgroup/fieldgroup.tabledrag.js	2010-05-05 10:03:51.000000000 -0500
@@ -0,0 +1,247 @@
+// $Id$
+
+/**
+ * @file
+ * Override Drupal tabledrag methods to add support for nested fieldgroups.
+ */
+
+/**
+ * Adjust the classes associated with a row's weight when it is dropped.
+ * Use the parent's name to base the change on.
+ */
+Drupal.tableDrag.prototype.onDrop = function() {
+  tableDragObject = this;
+  var siblingRowUp = $(tableDragObject.rowObject.element).prev('tr');
+  var siblingRowsDown = $(tableDragObject.rowObject.element).nextAll('tr');
+  var weightField = $('input.field-weight', tableDragObject.rowObject.element);
+  var siblingWeightFieldDown = $('input.field-weight', siblingRowsDown);
+  var oldWeightField = $('input.field-weight', tableDragObject.rowObject.element);
+  var previousRow = $(tableDragObject.rowObject.element).prev('tr');
+
+
+  while (previousRow.length && $('.indentation', previousRow).length >= this.rowObject.indents) {
+    previousRow = previousRow.prev('tr');
+  }
+  // If we found a row.
+  if (previousRow.length) {
+    sourceRow = previousRow[0];
+    var oldGroupField = $('input.field-name', sourceRow);
+    var newGroupName = oldGroupField[0].value.replace(/_/g, '-');
+  }
+  // Otherwise we went all the way to the left of the table without finding
+  // a parent, meaning this item has been placed at the root level.
+  else {
+    var newGroupName = "top";
+  }
+
+  var oldGroupName = oldWeightField[0].className.replace(/([^ ]+[ ]+)*field-weight-([^ ]+)([ ]+[^ ]+)*/, '$2');
+  var oldClass = 'field-weight-' + oldGroupName;
+  var newClass = 'field-weight-' + newGroupName;
+  weightField.removeClass(oldClass).addClass(newClass);
+
+  //Now that we've updated the class names, let's make sure the weight is properly set.
+  //First make sure that if the rows above are nested deeper, that we find their parent, which is a sibling of
+  //our current row.
+  while (siblingRowUp.length && $('.indentation', siblingRowUp).length > this.rowObject.indents) {
+    siblingRowUp = siblingRowUp.prev('tr');
+  }
+  //before we set the new weight, make sure we know whether the previous row is a sibling or a parent
+  var siblingIndent = $('.indentation', siblingRowUp).length;
+  var indents = this.rowObject.indents;
+
+  //if we found a sibling
+  if (siblingIndent == indents) {
+    //the previous row is a sibling, so let's set the new weight
+    var siblingWeightFieldUp = $('input.field-weight', siblingRowUp);
+    var siblingWeightUp = siblingWeightFieldUp[0].value;
+    var newWeight = parseInt(siblingWeightUp) + 1;
+    $('input.field-weight', tableDragObject.rowObject.element).val(newWeight);
+
+    //now deal with the case where we moved left/right
+    if (oldClass != newClass) {
+      $(tableDragObject.rowObject.element).nextAll('tr').each(function() {
+        if ($('input.field-weight', this).hasClass(newClass)) {
+          //let's make sure we grab an actual sibling
+          var prevRow = $(this).prev('tr');
+          while (prevRow.length && $('.indentation', prevRow).length > $('.indentation', this).length) {
+            prevRow = prevRow.prev('tr');
+          }
+          var prevWeight = $('input.field-weight', prevRow).val();
+          var incWeight = parseInt(prevWeight) + 1;
+          $('input.field-weight', this).val(incWeight);
+        }
+      });
+    }
+  }
+  else {
+    //the previous row is a parent, which means we're at the top of this group, so set the index to zero
+    $('input.field-weight', tableDragObject.rowObject.element).val(0);
+
+    //find the row's group to pass in to the each function for comparison since we only want to affect the
+    //weights in this same group
+    $(tableDragObject.rowObject.element).nextAll('tr').each(function() {
+      if ($('input.field-weight', this).hasClass(newClass)) {
+        //let's make sure we grab an actual sibling
+        var prevRow = $(this).prev('tr');
+        while (prevRow.length && $('.indentation', prevRow).length > $('.indentation', this).length) {
+          prevRow = prevRow.prev('tr');
+        }
+        var prevWeight = $('input.field-weight', prevRow).val();
+        var incWeight = parseInt(prevWeight) + 1;
+        $('input.field-weight', this).val(incWeight);
+      }
+    });
+  }
+}
+
+/**
+* Determine the valid indentations interval for the row at a given position
+* in the table.
+*
+* @param prevRow
+*   DOM object for the row before the tested position
+*   (or null for first position in the table).
+* @param nextRow
+*   DOM object for the row after the tested position
+*   (or null for last position in the table).
+*/
+Drupal.tableDrag.prototype.row.prototype.validIndentInterval = function(prevRow, nextRow) {
+  var minIndent, maxIndent;
+  var previousRow = $(this.element).prev('tr');
+  var previousMultiRow = previousRow;
+  var thisRow = $(previousRow).next('tr');
+  var realNextRow = $(this.element).next('tr');
+  var nextMultiRow = realNextRow;
+  var thisDepth = $('.indentation', thisRow).size();
+  var which = 'standard';
+  var rowOne, rowTwo, rowThree, rowFour;
+
+  // Minimum indentation:
+  // Do not orphan the next row.
+  minIndent = nextRow ? $('.indentation', nextRow).size() : 0;
+
+  if ($(this.element).is('.tabledrag-multigroup') || $(this.element).is('.tabledrag-standardgroup')) {
+    //find the first multigroup below where we are
+    while (nextMultiRow.length && (!($(nextMultiRow).is('.tabledrag-multigroup')) || ($('.indentation', nextMultiRow).size() > $('.indentation', thisRow).size()))) {
+      nextMultiRow = nextMultiRow.next('tr');
+    }
+    if (!($(nextMultiRow).is('.tabledrag-multigroup'))) {
+      nextMultiRow = null;
+    }
+    //find the first standard group below where we are
+    while (realNextRow.length && !($(realNextRow).is('.tabledrag-standardgroup'))) {
+      realNextRow = realNextRow.next('tr');
+    }
+    if (!($(realNextRow).is('.tabledrag-standardgroup'))) {
+      realNextRow = null;
+    }
+
+    if (typeof (realNextRow) != 'undefined' && realNextRow != undefined) {
+      rowOne = realNextRow.get(0);
+      //alert("rowOne " + rowOne.rowIndex);
+    }
+    else {
+      rowOne = null;
+    }
+    if (typeof (nextMultiRow) != 'undefined' && nextMultiRow != undefined) {
+      rowTwo = nextMultiRow.get(0);
+      //alert("rowTwo " + rowTwo.rowIndex);
+    }
+    else {
+      rowTwo = null;
+    }
+
+    if (rowOne == null && rowTwo == null) {
+      realNextRow = nextRow;
+    }
+    else if (rowTwo != null && rowOne == null) {
+      realNextRow = nextMultiRow;
+      which = 'multi';
+    }
+    else if (rowTwo != null && rowOne != null && rowTwo.rowIndex < rowOne.rowIndex) {
+      realNextRow = nextMultiRow;
+      which = 'multi';
+    }
+
+    //find the first multigroup above where we are at
+    while (previousMultiRow.length && !($(previousMultiRow).is('.tabledrag-multigroup'))) {
+      previousMultiRow = previousMultiRow.prev('tr');
+    }
+    if (!($(previousMultiRow).is('.tabledrag-multigroup'))) {
+      previousMultiRow = null;
+    }
+    //find the first multigroup above where we are at
+    while (previousRow.length && !($(previousRow).is('.tabledrag-standardgroup'))) {
+      previousRow = previousRow.prev('tr');
+    }
+    if (!($(previousRow).is('.tabledrag-standardgroup'))) {
+      previousRow = null;
+    }
+
+    if (typeof (previousRow) != 'undefined' && previousRow != undefined) {
+      rowThree = previousRow.get(0);
+      //alert("rowThree " + rowThree.rowIndex);
+    }
+    else {
+      rowThree = null;
+    }
+    if (typeof (previousMultiRow) != 'undefined' && previousMultiRow != undefined) {
+      rowFour = previousMultiRow.get(0);
+      //alert("rowFour " + rowFour.rowIndex);
+    }
+    else {
+      rowFour = null;
+    }
+
+    if (rowThree == null && rowFour == null) {
+      previousRow = prevRow;
+    }
+    else if (rowFour != null && rowThree == null) {
+      previousRow = previousMultiRow;
+      which = 'multi';
+    }
+    else if (rowFour != null && rowThree != null && rowFour.rowIndex > rowThree.rowIndex) {
+      previousRow = previousMultiRow;
+      which = 'multi';
+    }
+
+    if ($(this.element).is('.tabledrag-multigroup') || $(this.element).is('.tabledrag-standardgroup')) {
+      if (this.direction != 'down') {
+        if (which == 'standard') {
+          maxIndent = $('.indentation', previousRow).size() + 1;
+        }
+        else {
+          maxIndent = $('.indentation', previousRow).size();
+        }
+        //alert("up " + maxIndent);
+      }
+      else if (this.direction == 'down') {
+        if (which == 'standard') {
+          maxIndent = $('.indentation', realNextRow).size() + 1;
+        }
+        else {
+          maxIndent = $('.indentation', realNextRow).size();
+        }
+        //alert("down " + maxIndent);
+      }
+    }
+  }
+  else {
+    // Maximum indentation:
+    if (!prevRow || $(this.element).is('.tabledrag-root')) {
+      // Do not indent the first row in the table or 'root' rows.
+      maxIndent = 0;
+    }
+    else {
+      // Do not go deeper than as a child of the previous row.
+      maxIndent = $('.indentation', prevRow).size() + ($(prevRow).is('.tabledrag-leaf') ? 0 : 1);
+      // Limit by the maximum allowed depth for the table.
+      if (this.maxDepth) {
+        maxIndent = Math.min(maxIndent, this.maxDepth - (this.groupDepth - this.indents));
+      }
+    }
+  }
+  //alert("min " + minIndent);
+  //alert("max " + maxIndent);
+  return { 'min': minIndent, 'max': maxIndent };
+}
diff -Nurp ../cck.orig/theme/theme.inc ./theme/theme.inc
--- ../cck.orig/theme/theme.inc	2009-04-28 18:06:37.000000000 -0500
+++ ./theme/theme.inc	2010-05-05 10:03:51.000000000 -0500
@@ -28,6 +28,8 @@ function template_preprocess_content_fie
       $add_rows[] = $key;
     }
   }
+  $parent_list = array();
+  $parent_list['top'] = 'top';
   while ($order) {
     $key = reset($order);
     $element = &$form[$key];
@@ -48,7 +50,16 @@ function template_preprocess_content_fie
     $row = new stdClass();
 
     // Add target classes for the tabledrag behavior.
-    $element['weight']['#attributes']['class'] = 'field-weight';
+    if ($element['#row_type'] == 'group') {
+      $parent_list[$element['group_name']['#value']] = strtr($element['group_name']['#value'], '_', '-');
+    }
+    if (empty($element['parent']['#value']) || !isset($element['parent']['#value'])) {
+      $element['weight']['#attributes']['class'] = 'field-weight field-weight-' . 'top';
+    }
+    else {
+      $element['weight']['#attributes']['class'] = 'field-weight field-weight-' . strtr($element['parent']['#value'], '_', '-');
+    }
+
     $element['parent']['#attributes']['class'] = 'group-parent';
     $element['hidden_name']['#attributes']['class'] = 'field-name';
     // Add target classes for the update selects behavior.
@@ -74,6 +85,8 @@ function template_preprocess_content_fie
     $row->class .= isset($element['#add_new']) ? ' content-add-new' : '';
     $row->class .= isset($element['#leaf']) ? ' tabledrag-leaf' : '';
     $row->class .= isset($element['#root']) ? ' tabledrag-root' : '';
+    $row->class .= (isset($element['group_type']['#value']) && $element['group_type']['#value'] == 'Standard group') ? ' tabledrag-standardgroup' : '';
+    $row->class .= (isset($element['group_type']['#value']) && $element['group_type']['#value'] == 'Multigroup') ? ' tabledrag-multigroup' : '';
 
     $rows[] = $row;
     array_shift($order);
@@ -82,10 +95,15 @@ function template_preprocess_content_fie
   $vars['submit'] = drupal_render($form);
 
   // Add tabledrag behavior.
-//  drupal_add_tabledrag('content-field-overview', 'match', 'parent', 'group-parent', 'group-parent', 'field-name', FALSE, 1);
-  drupal_add_tabledrag('content-field-overview', 'match', 'parent', 'group-parent', 'group-parent', 'field-name', TRUE, 1);
-//  drupal_add_tabledrag('content-field-overview', 'order', 'sibling', 'field-weight', NULL, NULL, FALSE);
-  drupal_add_tabledrag('content-field-overview', 'order', 'sibling', 'field-weight');
+  //drupal_add_tabledrag('content-field-overview', 'match', 'parent', 'group-parent', 'group-parent', 'field-name', FALSE, 10);
+  drupal_add_tabledrag('content-field-overview', 'match', 'parent', 'group-parent', 'group-parent', 'field-name', TRUE, 10);
+  foreach ($parent_list as $name => $parent) {
+    //drupal_add_tabledrag('content-field-overview', 'order', 'sibling', 'field-weight', 'field-weight-' . $parent, NULL, FALSE);
+    drupal_add_tabledrag('content-field-overview', 'order', 'sibling', 'field-weight', 'field-weight-' . $parent, NULL, TRUE);
+  }
+
+  // Override methods in Drupal core tabledrag.js.
+  drupal_add_js(drupal_get_path('module', 'fieldgroup') .'/fieldgroup.tabledrag.js');
 
   // Add settings for the update selects behavior.
   $js_fields = array();
@@ -142,4 +160,4 @@ function template_preprocess_content_dis
   }
   $vars['rows'] = $rows;
   $vars['submit'] = drupal_render($form);
-}
\ No newline at end of file
+}
