Index: title.info
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/title/title.info,v
retrieving revision 1.2
diff -u -r1.2 title.info
--- title.info	11 Oct 2010 19:59:57 -0000	1.2
+++ title.info	18 Jan 2011 00:48:25 -0000
@@ -1,8 +1,7 @@
 ; $Id: title.info,v 1.2 2010/10/11 19:59:57 sun Exp $
 name = Title
-description = Allows entity titles/labels to be translated.
-package = Multilingual
+description = Replaces entity legacy fields with regular fields.
+package = Fields
 core = 7.x
-dependencies[] = locale
 files[] = title.module
 files[] = tests/title.test
Index: title.install
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/title/title.install,v
retrieving revision 1.1
diff -u -r1.1 title.install
--- title.install	11 Oct 2010 19:56:17 -0000	1.1
+++ title.install	18 Jan 2011 00:48:25 -0000
@@ -3,6 +3,16 @@
 
 /**
  * @file
- * Installation functions for Title module.
+ * Installation functions for the Title module.
  */
 
+/**
+ * Implements hook_install().
+ */
+function title_install() {
+  // Make sure fields are properly handled before any other module access them.
+  db_update('system')
+    ->fields(array('weight' => -100))
+    ->condition('name', 'title')
+    ->execute();
+}
Index: title.module
===================================================================
RCS file: /cvs/drupal-contrib/contributions/modules/title/title.module,v
retrieving revision 1.19
diff -u -r1.19 title.module
--- title.module	11 Oct 2010 19:56:17 -0000	1.19
+++ title.module	18 Jan 2011 00:48:26 -0000
@@ -3,6 +3,398 @@
 
 /**
  * @file
- * Translatable entity title/label functionality.
+ * Replaces entity legacy fields with regular fields.
+ *
+ * Provides an API and a basic UI to replace legacy pseudo-fields with regular
+ * fields. The API only offers synchronization between the two data storage
+ * systems and data replacement on entity load. Field definitions have to be
+ * provided by the modules exploiting the API.
+ *
+ * Title implements its own entity description API to describe core legacy
+ * pseudo-fields:
+ * - Node: title
+ * - Taxonomy Term: name, description
+ * - Comment: subject
+ *
+ * @todo Field Replacement could exploit an OOP-based Entity Description Handler
+ * (@see TranslationHandlerInterface) to offer a cleaner way to describe which
+ * entity properties actually are legacy pseudo fields that can be replaced. The
+ * EDH would also allow to have an easier way to generalize entities behaviors
+ * and to access entity properties: for instance to implement synchronization,
+ * Field Replacement API has to deal with entity language which is defined only
+ * for nodes and comments.
  */
 
+/**
+ * Implements hook_field_replacement_info().
+ */
+function title_field_replacement_info() {
+  $info = array();
+
+  $field = array(
+    'type' => 'text',
+    'cardinality' => 1,
+    'translatable' => TRUE,
+  );
+
+  $instance = array(
+    'required' => TRUE,
+    'settings' => array(
+      'text_processing' => 0,
+    ),
+    'widget' => array(
+      'weight' => -5,
+    ),
+  );
+
+  $info['node'] = array(
+    'title' => array(
+      'field' => $field,
+      'instance' => array(
+        'label' => t('Title'),
+        'description' => t('A field replacing node title.'),
+      ) + $instance,
+    ),
+  );
+
+  $info['taxonomy_term'] = array(
+    'name' => array(
+      'field' => $field,
+      'instance' => array(
+        'label' => t('Name'),
+        'description' => t('A field replacing taxonomy term name.'),
+      ) + $instance,
+    ),
+    'description' => array(
+      'field' => array(
+        'type' => 'text_with_summary',
+      ) + $field,
+      'instance' => array(
+        'required' => FALSE,
+        'label' => t('Description'),
+        'description' => t('A field replacing taxonomy term description.'),
+      ) + $instance,
+    ),
+  );
+
+  $info['comment'] = array(
+    'subject' => array(
+      'field' => $field,
+      'instance' => array(
+        'label' => t('Subject'),
+        'description' => t('A field replacing comment subject.'),
+      ) + $instance,
+    ),
+  );
+
+  return $info;
+}
+
+/**
+ * Implements hook_entity_info_alter().
+ */
+function title_entity_info_alter(&$info) {
+  $field_replacement_info = module_invoke_all('field_replacement_info');
+  drupal_alter('field_replacement_info', $field_replacement_info);
+
+  foreach ($info as $entity_type => $entity_info) {
+    if ($entity_info['fieldable'] && !empty($field_replacement_info[$entity_type])) {
+      $info[$entity_type]['field replacement'] = $field_replacement_info[$entity_type];
+      foreach ($info[$entity_type]['field replacement'] as $field_name => $fr_info) {
+        if (empty($fr_info['field']['field_name'])) {
+          $info[$entity_type]['field replacement'][$field_name]['field']['field_name'] = $field_name . '_field';
+        }
+        $info[$entity_type]['field replacement'][$field_name]['instance']['field_name'] = $info[$entity_type]['field replacement'][$field_name]['field']['field_name'];
+      }
+    }
+  }
+}
+
+/**
+ * Implements hook_field_attach_presave().
+ */
+function title_field_attach_presave($entity_type, $entity) {
+  title_entity_sync($entity_type, $entity, FALSE);
+}
+
+/**
+ * Implements hook_field_attach_load().
+ *
+ * @see title_entity_load()
+ */
+function title_field_attach_load($entity_type, $entities, $age, $options) {
+  // @todo: Do we need to handle revisions here?
+  title_entity_load($entities, $entity_type);
+}
+
+/**
+ * Implements hook_entity_load().
+ *
+ * Since the result of field_attach_load() is cached, title synchronization must
+ * be performed also here to ensure that there is always the correct value in
+ * the replaced fields.
+ */
+function title_entity_load($entities, $type) {
+  foreach ($entities as &$entity) {
+    title_entity_sync($type, $entity);
+  }
+}
+
+/**
+ * Check whether field replacement is enabled for the given field.
+ *
+ * @param $entity_type
+ *   The type of $entity.
+ * @param $bundle
+ *   The bundle the legacy field belongs to.
+ * @param $legacy_field
+ *   The name of the legacy field to be replaced.
+ *
+ * @return
+ *   TRUE if field replacement is enabled for the given field, FALSE otherwise.
+ */
+function title_field_replacement_enabled($entity_type, $bundle, $legacy_field) {
+  $entity_info = entity_get_info($entity_type);
+  $info = $entity_info['field replacement'][$legacy_field];
+  $instance = field_info_instance($entity_type, $info['field']['field_name'], $bundle);
+  return !empty($instance);
+}
+
+/**
+ * Synchronize replaced fields with the regular field values.
+ *
+ * @param $entity_type
+ *   The name of the entity type.
+ * @param $entity
+ *   The entity to work with.
+ * @param $display
+ *   Specifies if synchronization is being performed on display or on save.
+ */
+function title_entity_sync($entity_type, &$entity, $display = TRUE) {
+  $entity_info = entity_get_info($entity_type);
+  if (isset($entity_info['field replacement'])) {
+    list(,, $bundle) = entity_extract_ids($entity_type, $entity);
+    foreach ($entity_info['field replacement'] as $legacy_field => $info) {
+      if (title_field_replacement_enabled($entity_type, $bundle, $legacy_field)) {
+        title_field_sync($entity_type, &$entity, $legacy_field, $info['field']['field_name'], $display);
+      }
+    }
+  }
+}
+
+/**
+ * Synchronize a single legacy field with its regular field value.
+ *
+ * @param $entity_type
+ *   The name of the entity type.
+ * @param $entity
+ *   The entity to work with.
+ * @param $legacy_field
+ *   The name of the legacy field to be replaced.
+ * @param $field_name
+ *   The regular field to use as source value.
+ * @param $display
+ *   Specifies if synchronization is being performed on display or on save.
+ */
+function title_field_sync($entity_type, &$entity, $legacy_field, $field_name, $display = TRUE) {
+  if (isset($entity->{$legacy_field})) {
+    $langcode = NULL;
+
+    if (!$display) {
+      // If a language property is defined for the current entity we synchronize
+      // the field value using the "original" language, otherwise we fall back
+      // to the defaut language.
+      $langcode = !empty($entity->language) ? $entity->language : language_default()->language;
+    }
+
+    // Find out the actual language to use (field might be untranslatable).
+    $langcode = field_language($entity_type, $entity, $field_name, $langcode);
+
+    // @todo: For now we are assuming we are replacing a single text field. We
+    // will need to generalize this behavior to support any field type and
+    // multiplicity.
+    if (isset($entity->{$field_name}[$langcode][0]['value'])) {
+      $entity->{$legacy_field} = $entity->{$field_name}[$langcode][0]['value'];
+    }
+  }
+}
+
+/**
+ * Implements hook_field_attach_form().
+ *
+ * Hide legacy field widgets on the assumption that this is always called on
+ * fieldable entity forms.
+ */
+function title_field_attach_form($entity_type, $entity, &$form, &$form_state, $langcode) {
+  list(,, $bundle) = entity_extract_ids($entity_type, $entity);
+  $entity_info = entity_get_info($entity_type);
+
+  if (isset($entity_info['field replacement'])) {
+    foreach ($entity_info['field replacement'] as $legacy_field => $info)  {
+      if (isset($form[$legacy_field]) && title_field_replacement_enabled($entity_type, $bundle, $legacy_field)) {
+        // @todo: This should be enough for most situations. We might need a
+        // #parents array in the field replacement info indicating where the
+        // widget to hide actually is. Moreover $form might be a form element
+        // and not a complete form structure.
+        $form[$legacy_field]['#access'] = FALSE;
+      }
+    }
+  }
+}
+
+/**
+ * Implements of hook_menu().
+ */
+function title_menu() {
+  $items = array();
+
+  foreach (entity_get_info() as $entity_type => $entity_info) {
+    if (!empty($entity_info['field replacement'])) {
+      foreach ($entity_info['bundles'] as $bundle_name => $bundle_info) {
+        // Blindly taken from field_ui_menu().
+        if (isset($bundle_info['admin'])) {
+          $path = $bundle_info['admin']['path'];
+
+          if (isset($bundle_info['admin']['bundle argument'])) {
+            $bundle_arg = $bundle_info['admin']['bundle argument'];
+          }
+          else {
+            $bundle_arg = $bundle_name;
+          }
+
+          $access = array_intersect_key($bundle_info['admin'], drupal_map_assoc(array('access callback', 'access arguments')));
+          $access += array(
+            'access callback' => 'user_access',
+            'access arguments' => array('administer site configuration'),
+          );
+
+          $items["$path/fields/replace"] = array(
+            'load arguments' => array(),
+            'title' => 'Replace fields',
+            'page callback' => 'drupal_get_form',
+            'page arguments' => array('title_field_replacement_form', $entity_type, $bundle_arg),
+          ) + $access;
+        }
+      }
+    }
+  }
+
+  return $items;
+}
+
+/**
+ * Implements hook_field_extra_fields_alter().
+ */
+function title_field_extra_fields_alter(&$info) {
+  $entity_info = entity_get_info();
+  foreach ($info as $entity_type => $bundles) {
+    foreach ($bundles as $bundle_name => $bundle) {
+      if (!empty($entity_info[$entity_type]['field replacement'])) {
+        foreach ($entity_info[$entity_type]['field replacement'] as $field_name => $field_replacement_info) {
+          if (title_field_replacement_enabled($entity_type, $bundle_name, $field_name)) {
+            unset($info[$entity_type][$bundle_name]['form'][$field_name]);
+          }
+        }
+      }
+    }
+  }
+}
+
+/**
+ * Implements hook_form_FORM_ID_alter().
+ *
+ * Provide settings to enable title field.
+ */
+function title_form_field_ui_field_overview_form_alter(&$form, &$form_state) {
+  $entity_info = entity_get_info($form['#entity_type']);
+
+  if (!empty($entity_info['field replacement'])) {
+    $field_replacement_info = $entity_info['field replacement'];
+    $admin_path = _field_ui_bundle_admin_path($form['#entity_type'], $form['#bundle']);
+    $form['fields']['#header'][6]['colspan'] += 1;
+
+    foreach (element_children($form['fields']) as $field_name) {
+      if (isset($field_replacement_info[$field_name])) {
+        $form['fields'][$field_name]['field_replacement'] = array(
+          '#type' => 'link',
+          '#title' => t('replace'),
+          '#href' => $admin_path . '/fields/replace/' . $field_name,
+          '#options' => array('attributes' => array('title' => t('Replace %field with a field instance.', array('%field' => $field_name)))),
+        );
+      }
+      else {
+        $form['fields'][$field_name]['field_replacement'] = array();
+      }
+    }
+  }
+}
+
+/**
+ * Generate a field replacement form.
+ */
+function title_field_replacement_form($form, $form_state, $entity_type, $bundle, $field_name) {
+  $bundle_name = field_extract_bundle($entity_type, $bundle);
+  $entity_info = entity_get_info($entity_type);
+  $info = $entity_info['field replacement'][$field_name];
+  $instance = field_info_instance($entity_type, $info['field']['field_name'], $bundle_name);
+
+  $form['#entity_type'] = $entity_type;
+  $form['#bundle'] = $bundle_name;
+  $form['#field_name'] = $field_name;
+
+  $form['enabled'] = array(
+    '#type' => 'checkbox',
+    '#title' => t('Replace %field with a field instance.', array('%field' => $field_name)),
+    '#description' => t('If this is enabled the %field legacy field will be replaced with a regular field and will disappear from the <em>Manage fields</em> page. It will get back if the replacing field instance is deleted.', array('%field' => $field_name)),
+    '#default_value' => !empty($instance),
+  );
+
+  $form['actions'] = array('#type' => 'actions');
+  $form['actions']['submit'] = array('#type' => 'submit', '#value' => t('Save settings'));
+  return $form;
+}
+
+/**
+ * Process field replacement form subissions.
+ */
+function title_field_replacement_form_submit($form, &$form_state) {
+  if (title_toggle_field_replacement($form['#entity_type'], $form['#bundle'], $form['#field_name'])) {
+    drupal_set_message(t('%field replaced with a field instance.', array('%field' => $form['#field_name'])));
+  }
+  else {
+    drupal_set_message(t('Field replacement removed.'));
+  }
+  $form_state['redirect'] = _field_ui_bundle_admin_path($form['#entity_type'], $form['#bundle']) . '/fields';
+}
+
+/**
+ * Toggle field replacement for the given field.
+ *
+ * @param $entity_type
+ *   The name of the entity type.
+ * @param $bundle
+ *   The bundle the legacy field belongs to.
+ * @param $legacy_field
+ *   The name of the legacy field to be replaced.
+ */
+function title_toggle_field_replacement($entity_type, $bundle, $legacy_field) {
+  $entity_info = entity_get_info($entity_type);
+  $info = $entity_info['field replacement'][$legacy_field];
+  $field_name = $info['field']['field_name'];
+  $instance = field_info_instance($entity_type, $field_name, $bundle);
+
+  if (empty($instance)) {
+    $field = field_info_field($field_name);
+    if (empty($field)) {
+      field_create_field($info['field']);
+    }
+    $info['instance']['entity_type'] = $entity_type;
+    $info['instance']['bundle'] = $bundle;
+    field_create_instance($info['instance']);
+    return TRUE;
+  }
+  else {
+    field_delete_instance($instance);
+    return FALSE;
+  }
+}
