diff --git a/css/media.css b/css/media.css
index 2ea3699..2397a17 100644
--- a/css/media.css
+++ b/css/media.css
@@ -62,30 +62,34 @@
 /* Media item display */
 
 .media-item {
-  background: #FFFFFF;
+  background: #eee;
   border: 1px solid #CCCCCC;
+  box-shadow: inset 0 0 15px rgba(0,0,0,.1), inset 0 0 0 1px rgba(0,0,0,.05);
+  display: inline-block;
   padding: 5px;
-   position: relative;
+  position: relative;
 }
 
 .media-item img {
-  margin-bottom: 10px;
+  display: block;
 }
 
 .media-item .label-wrapper {
-  text-align: center;
-  position: absolute;
+  background: rgba(255,255,255,.8);
   bottom: 0;
-  margin-left: auto;
-  margin-right: auto;
-  width: 90%;
+  box-shadow: inset 0 0 0 1px rgba(0,0,0,.15);
+  left: 0;
+  max-height: 100%;
+  overflow: hidden;
+  position: absolute;
+  right: 0;
+  text-align: center;
+  word-wrap: break-word;
 }
 
 .media-item .label-wrapper label {
   font-size: 10px;
-  overflow: hidden;
-  text-overflow: ellipsis;
-  white-space: nowrap;
+  padding: 5px 10px;
 }
 
 /* Media item lists */
@@ -124,8 +128,8 @@
 }
 
 .media-list-thumbnails .form-type-checkbox {
-  bottom: 135px;
-  left: 8px;
+  bottom: 117px;
+  left: 6px;
   margin: 0;
   padding: 0;
   position: relative;
@@ -135,26 +139,6 @@
 
 .media-widget .preview {
   display: inline-block;
-  vertical-align: middle;
-}
-
-.media-widget .preview a {
-  text-decoration: none;
-}
-
-.media-widget .preview .media-item {
   margin-right: 10px;
-}
-
-.media-widget .preview .media-item:hover {
-  border-color: #058AC5;
-  cursor: pointer;
-}
-
-.media-widget .preview .media-item .label-wrapper label {
-  color: #058AC5;
-}
-
-.media-widget .preview .media-item .label-wrapper label:hover {
-  cursor: pointer;
+  vertical-align: middle;
 }
diff --git a/includes/media.fields.inc b/includes/media.fields.inc
index 29b63a9..65abe28 100644
--- a/includes/media.fields.inc
+++ b/includes/media.fields.inc
@@ -12,16 +12,15 @@
 function media_field_widget_info() {
   return array(
     'media_generic' => array(
-      'label' => t('Media file selector'),
+      'label' => t('Media browser'),
       'field types' => array('file', 'image'),
       'settings' => array(
-        'progress_indicator' => 'throbber',
         'allowed_types' => array('image'),
         'browser_plugins' => array(),
         'allowed_schemes' => array('public', 'private'),
       ),
       'behaviors' => array(
-        'multiple values' => FIELD_BEHAVIOR_DEFAULT,
+        'multiple values' => FIELD_BEHAVIOR_CUSTOM,
         'default value' => FIELD_BEHAVIOR_NONE,
       ),
     ),
@@ -77,45 +76,43 @@ function media_field_formatter_view($entity_type, $entity, $field, $instance, $l
 function media_field_widget_settings_form($field, $instance) {
   $widget = $instance['widget'];
   $settings = $widget['settings'];
-  $form = array();
 
-  $streams = file_get_stream_wrappers(STREAM_WRAPPERS_VISIBLE);
+  $plugins = media_get_browser_plugin_info();
+  $options = array();
+  foreach ($plugins as $key => $plugin) {
+    $options[$key] = check_plain($plugin['title']);
+  }
+
+  $form['browser_plugins'] = array(
+    '#type' => 'checkboxes',
+    '#title' => t('Enabled browser plugins'),
+    '#options' => $options,
+    '#default_value' => $settings['browser_plugins'],
+    '#description' => t('Media browser plugins which are allowed for this field. If no plugins are selected, they will all be available.'),
+  );
 
   $form['allowed_types'] = array(
     '#type' => 'checkboxes',
-    '#title' => t('Allowed remote media types'),
+    '#title' => t('Allowed file types'),
     '#options' => file_entity_type_get_names(),
     '#default_value' => $settings['allowed_types'],
-    '#description' => t('Media types which are allowed for this field when using remote streams.'),
-    '#weight' => 1,
-    '#access' => count(file_get_stream_wrappers(STREAM_WRAPPERS_VISIBLE | STREAM_WRAPPERS_LOCAL)) != count($streams),
+    '#description' => t('File types which are allowed for this field. If no file types are selected, they will all be available.'),
   );
 
+  $visible_steam_wrappers = file_get_stream_wrappers(STREAM_WRAPPERS_VISIBLE);
   $options = array();
-  foreach ($streams as $scheme => $data) {
-    $options[$scheme] = t('@scheme (@name)', array('@scheme' => $scheme . '://', '@name' => $data['name']));
+  foreach ($visible_steam_wrappers as $scheme => $information) {
+    $options[$scheme] = check_plain($information['name']);
   }
+
   $form['allowed_schemes'] = array(
     '#type' => 'checkboxes',
     '#title' => t('Allowed URI schemes'),
     '#options' => $options,
     '#default_value' => $settings['allowed_schemes'],
-    '#description' => t('URI schemes include public:// and private:// which are the Drupal files directories, and may also refer to remote sites.'),
-    '#weight' => 2,
+    '#description' => t('URI schemes which are allowed for this field. If no schemes are selected, they will all be available.'),
   );
 
-  $plugins = media_get_browser_plugin_info();
-  $form['browser_plugins'] = array(
-    '#type' => 'checkboxes',
-    '#title' => t('Enabled browser plugins'),
-    '#options' => array(),
-    '#default_value' => $settings['browser_plugins'],
-    '#description' => t('If no plugins are selected, they will all be available.'),
-  );
-  foreach ($plugins as $key => $plugin) {
-    $form['browser_plugins']['#options'][$key] = !empty($plugin['title']) ? $plugin['title'] : $key;
-  }
-
   return $form;
 }
 
@@ -123,33 +120,35 @@ function media_field_widget_settings_form($field, $instance) {
  * Implements hook_field_widget_form().
  */
 function media_field_widget_form(&$form, &$form_state, $field, $instance, $langcode, $items, $delta, $element) {
-  $field_settings = $instance['settings'];
-  $widget_settings = $instance['widget']['settings'];
 
-  // @todo The Field API supports automatic serialization / unserialization, so
-  //   this should no longer be needed. After verifying with a module that uses
-  //   the 'data' column, remove this.
-  // @see media_field_widget_value()
-  $current_value = array();
-  if (isset($items[$delta])) {
-    $current_value = $items[$delta];
-    // @todo $items[$delta] is sometimes a loaded media entity (an object)
-    //   rather than an array. This conflicts with Field API expectations (for
-    //   example, it results in fatal errors when previewing a node with a
-    //   multi-valued media field), so should be fixed. In the meantime, don't
-    //   assume that $current_value is an array.
-    if (is_array($current_value) && isset($current_value['data']) && is_string($current_value['data'])) {
-      $current_value['data'] = unserialize($current_value['data']);
-    }
+  // Add display_field setting to field because media_field_widget_form() assumes it is set.
+  if (!isset($field['settings']['display_field'])) {
+    $field['settings']['display_field'] = 0;
+  }
+
+  $defaults = array(
+    'fid' => 0,
+    'display' => !empty($field['settings']['display_default']),
+    'description' => '',
+  );
+
+  // Load the items for form rebuilds from the field state as they might not be
+  // in $form_state['values'] because of validation limitations. Also, they are
+  // only passed in as $items when editing existing entities.
+  $field_state = field_form_get_state($element['#field_parents'], $field['field_name'], $langcode, $form_state);
+  if (isset($field_state['items'])) {
+    $items = $field_state['items'];
   }
 
+  $field_settings = $instance['settings'];
+  $widget_settings = $instance['widget']['settings'];
+
+  // Essentially we use the media type, extended with some enhancements.
+  $element_info = element_info('media');
   $element += array(
-    // @todo This should be a fieldset, but throws a warning about
-    // element_children.
     '#type' => 'media',
-    '#collapsed' => TRUE,
-    '#default_value' => $current_value,
-    '#required' => $instance['required'],
+    '#value_callback' => 'media_field_widget_value',
+    '#process' => array_merge($element_info['#process'], array('media_field_widget_process')),
     '#media_options' => array(
       'global' => array(
         'types' => array_filter($widget_settings['allowed_types']),
@@ -161,57 +160,386 @@ function media_field_widget_form(&$form, &$form_state, $field, $instance, $langc
         'uri_scheme' => !empty($field['settings']['uri_scheme']) ? $field['settings']['uri_scheme'] : file_default_scheme(),
       ),
     ),
+    // Allows this field to return an array instead of a single value.
+    '#extended' => TRUE,
   );
 
-  if ($field['cardinality'] != 1) {
-    $element['#title'] = check_plain($instance['label']);
-    $element['#title_display'] = 'invisible';
+  // Add image field specific validators.
+  if ($field['type'] == 'image') {
+    if ($field_settings['min_resolution'] || $field_settings['max_resolution']) {
+      $element['#media_options']['global']['min_resolution'] = $field_settings['min_resolution'];
+      $element['#media_options']['global']['max_resolution'] = $field_settings['max_resolution'];
+    }
+  }
+
+  if ($field['cardinality'] == 1) {
+    // Set the default value.
+    $element['#default_value'] = !empty($items) ? $items[0] : $defaults;
+    // If there's only one field, return it as delta 0.
+    $elements = array($element);
+  }
+  else {
+    // If there are multiple values, add an element for each existing one.
+    foreach ($items as $item) {
+      $elements[$delta] = $element;
+      $elements[$delta]['#default_value'] = $item;
+      $elements[$delta]['#weight'] = $delta;
+      $delta++;
+    }
+    // And then add one more empty row for new uploads except when this is a
+    // programmed form as it is not necessary.
+    if (($field['cardinality'] == FIELD_CARDINALITY_UNLIMITED || $delta < $field['cardinality']) && empty($form_state['programmed'])) {
+      $elements[$delta] = $element;
+      $elements[$delta]['#default_value'] = $defaults;
+      $elements[$delta]['#weight'] = $delta;
+      $elements[$delta]['#required'] = ($element['#required'] && $delta == 0);
+    }
+    // The group of elements all-together need some extra functionality
+    // after building up the full list (like draggable table rows).
+    $elements['#file_upload_delta'] = $delta;
+    $elements['#theme'] = 'media_widget_multiple';
+    $elements['#theme_wrappers'] = array('fieldset');
+    $elements['#process'] = array('media_field_widget_process_multiple');
+    $elements['#title'] = $element['#title'];
+    $elements['#description'] = $element['#description'];
+    $elements['#field_name'] = $element['#field_name'];
+    $elements['#language'] = $element['#language'];
+    $elements['#display_field'] = $field['settings']['display_field'];
+
+    // Add some properties that will eventually be added to the file upload
+    // field. These are added here so that they may be referenced easily through
+    // a hook_form_alter().
+    $elements['#file_upload_title'] = t('Attach media');
+  }
+
+  return $elements;
+}
+
+/**
+ * The #value_callback for the media_generic field element.
+ */
+function media_field_widget_value($element, $input = FALSE, $form_state) {
+  if ($input) {
+    // Checkboxes lose their value when empty.
+    // If the display field is present make sure its unchecked value is saved.
+    $field = field_widget_field($element, $form_state);
+    if (empty($input['display'])) {
+      $input['display'] = $field['settings']['display_field'] ? 0 : 1;
+    }
   }
 
-  if ($field['type'] == 'file') {
+  // We depend on the managed file element to handle uploads.
+  $return = media_file_value($element, $input, $form_state);
+
+  // Ensure that all the required properties are returned even if empty.
+  $return += array(
+    'fid' => 0,
+    'display' => 1,
+    'description' => '',
+  );
+
+  return $return;
+}
+
+/**
+ * An element #process callback for the file_generic field type.
+ *
+ * Expands the file_generic type to include the description and display fields.
+ */
+function media_field_widget_process($element, &$form_state, $form) {
+  $item = $element['#value'];
+  $item['fid'] = $element['fid']['#value'];
+
+  $field = field_widget_field($element, $form_state);
+  $instance = field_widget_instance($element, $form_state);
+  $settings = $instance['widget']['settings'];
+
+  $element['#theme'] = 'media_widget';
+
+  // Add the display field if enabled.
+  if (!empty($field['settings']['display_field']) && $item['fid']) {
+    $element['display'] = array(
+      '#type' => empty($item['fid']) ? 'hidden' : 'checkbox',
+      '#title' => t('Include file in display'),
+      '#value' => isset($item['display']) ? $item['display'] : $field['settings']['display_default'],
+      '#attributes' => array('class' => array('file-display')),
+    );
+  }
+  else {
     $element['display'] = array(
-      '#type' => 'value',
-      '#value' => 1,
+      '#type' => 'hidden',
+      '#value' => '1',
     );
   }
 
-  // Add image field specific validators.
-  if ($field['type'] == 'image') {
-    if ($field_settings['min_resolution'] || $field_settings['max_resolution']) {
-      $element['#media_options']['global']['min_resolution'] = $field_settings['min_resolution'];
-      $element['#media_options']['global']['max_resolution'] = $field_settings['max_resolution'];
+  // Add the description field if enabled.
+  if (!empty($instance['settings']['description_field']) && $item['fid']) {
+    $element['description'] = array(
+      '#type' => variable_get('file_description_type', 'textfield'),
+      '#title' => t('Description'),
+      '#value' => isset($item['description']) ? $item['description'] : '',
+      '#maxlength' => variable_get('file_description_length', 128),
+      '#description' => t('The description may be used as the label of the link to the file.'),
+    );
+  }
+
+  // Adjust the Ajax settings so that on upload and remove of any individual
+  // file, the entire group of file fields is updated together.
+  if ($field['cardinality'] != 1) {
+    $parents = array_slice($element['#array_parents'], 0, -1);
+    $new_path = 'media/ajax/' . implode('/', $parents) . '/' . $form['form_build_id']['#value'];
+    $field_element = drupal_array_get_nested_value($form, $parents);
+    $new_wrapper = $field_element['#id'] . '-ajax-wrapper';
+    foreach (element_children($element) as $key) {
+      if (isset($element[$key]['#ajax'])) {
+        $element[$key]['#ajax']['path'] = $new_path;
+        $element[$key]['#ajax']['wrapper'] = $new_wrapper;
+      }
     }
+    unset($element['#prefix'], $element['#suffix']);
+  }
+
+  // Add another submit handler to the upload and remove buttons, to implement
+  // functionality needed by the field widget. This submit handler, along with
+  // the rebuild logic in media_field_widget_form() requires the entire field,
+  // not just the individual item, to be valid.
+  foreach (array('attach_button', 'edit_button', 'remove_button') as $key) {
+    $element[$key]['#submit'][] = 'media_field_widget_submit';
+    $element[$key]['#limit_validation_errors'] = array(array_slice($element['#parents'], 0, -1));
   }
 
   return $element;
 }
 
 /**
- * Widget value.
+ * An element #process callback for a group of file_generic fields.
  *
- * @todo Is this function ever called? If not, remove it. The Field API now
- *   supports automatic serialization / unserialization, so this should no
- *   longer be needed. After verifying with a module that uses the 'data'
- *   column, remove this.
+ * Adds the weight field to each row so it can be ordered and adds a new Ajax
+ * wrapper around the entire group so it can be replaced all at once.
+ */
+function media_field_widget_process_multiple($element, &$form_state, $form) {
+  $element_children = element_children($element, TRUE);
+  $count = count($element_children);
+
+  foreach ($element_children as $delta => $key) {
+    if ($key != $element['#file_upload_delta']) {
+      $description = _media_field_get_description_from_element($element[$key]);
+      $element[$key]['_weight'] = array(
+        '#type' => 'weight',
+        '#title' => $description ? t('Weight for @title', array('@title' => $description)) : t('Weight for new file'),
+        '#title_display' => 'invisible',
+        '#delta' => $count,
+        '#default_value' => $delta,
+      );
+    }
+    else {
+      // The title needs to be assigned to the upload field so that validation
+      // errors include the correct widget label.
+      $element[$key]['#title'] = $element['#title'];
+      $element[$key]['_weight'] = array(
+        '#type' => 'hidden',
+        '#default_value' => $delta,
+      );
+    }
+  }
+
+  // Add a new wrapper around all the elements for Ajax replacement.
+  $element['#prefix'] = '<div id="' . $element['#id'] . '-ajax-wrapper">';
+  $element['#suffix'] = '</div>';
+
+  return $element;
+}
+
+/**
+ * Retrieves the file description from a field field element.
+ *
+ * This helper function is used by media_field_widget_process_multiple().
+ *
+ * @param $element
+ *   The element being processed.
+ *
+ * @return
+ *   A description of the file suitable for use in the administrative interface.
+ */
+function _media_field_get_description_from_element($element) {
+  // Use the actual file description, if it's available.
+  if (!empty($element['#default_value']['description'])) {
+    return $element['#default_value']['description'];
+  }
+  // Otherwise, fall back to the filename.
+  if (!empty($element['#default_value']['filename'])) {
+    return $element['#default_value']['filename'];
+  }
+  // This is probably a newly uploaded file; no description is available.
+  return FALSE;
+}
+
+/**
+ * Form submission handler for upload/remove button of media_field_widget_form().
+ *
+ * This runs in addition to and after media_field_widget_submit().
  *
+ * @see media_field_widget_submit()
  * @see media_field_widget_form()
+ * @see media_field_widget_process()
  */
-function media_field_widget_value($element, $input, $form_state) {
-  $return = $input;
+function media_field_widget_submit($form, &$form_state) {
+  // During the form rebuild, media_field_widget_form() will create field item
+  // widget elements using re-indexed deltas, so clear out $form_state['input']
+  // to avoid a mismatch between old and new deltas. The rebuilt elements will
+  // have #default_value set appropriately for the current state of the field,
+  // so nothing is lost in doing this.
+  $parents = array_slice($form_state['triggering_element']['#parents'], 0, -2);
+  drupal_array_set_nested_value($form_state['input'], $parents, NULL);
+
+  $button = $form_state['triggering_element'];
 
-  if (!is_array($return)) {
-    $return = array();
+  // Go one level up in the form, to the widgets container.
+  $element = drupal_array_get_nested_value($form, array_slice($button['#array_parents'], 0, -1));
+  $field_name = $element['#field_name'];
+  $langcode = $element['#language'];
+  $parents = $element['#field_parents'];
+
+  $submitted_values = drupal_array_get_nested_value($form_state['values'], array_slice($button['#array_parents'], 0, -2));
+  foreach ($submitted_values as $delta => $submitted_value) {
+    if (!$submitted_value['fid']) {
+      unset($submitted_values[$delta]);
+    }
   }
 
-  if (isset($return['data'])) {
-    $return['data'] = serialize($return['data']);
+  // Re-index deltas after removing empty items.
+  $submitted_values = array_values($submitted_values);
+
+  // Update form_state values.
+  drupal_array_set_nested_value($form_state['values'], array_slice($button['#array_parents'], 0, -2), $submitted_values);
+
+  // Update items.
+  $field_state = field_form_get_state($parents, $field_name, $langcode, $form_state);
+  $field_state['items'] = $submitted_values;
+  field_form_set_state($parents, $field_name, $langcode, $form_state, $field_state);
+}
+
+/**
+ * Returns HTML for an individual file upload widget.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - element: A render element representing the widget.
+ *
+ * @ingroup themeable
+ */
+function theme_media_widget($variables) {
+  $element = $variables['element'];
+  $output = '';
+
+  // The "form-media" class is required for proper Ajax functionality.
+  $output .= '<div id="' . $element['#id'] . '" class="media-widget form-media clearfix">';
+  $output .= drupal_render_children($element);
+  $output .= '</div>';
+
+  return $output;
+}
+
+/**
+ * Returns HTML for a group of file upload widgets.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - element: A render element representing the widgets.
+ *
+ * @ingroup themeable
+ */
+function theme_media_widget_multiple($variables) {
+  $element = $variables['element'];
+
+  // Special ID and classes for draggable tables.
+  $weight_class = $element['#id'] . '-weight';
+  $table_id = $element['#id'] . '-table';
+
+  // Build up a table of applicable fields.
+  $headers = array();
+  $headers[] = t('File information');
+  if ($element['#display_field']) {
+    $headers[] = array(
+      'data' => t('Display'),
+      'class' => array('checkbox'),
+    );
   }
+  $headers[] = t('Weight');
+  $headers[] = t('Operations');
 
-  $return += array(
-    'fid' => 0,
-    'title' => '',
-    'data' => NULL,
-  );
+  // Get our list of widgets in order (needed when the form comes back after
+  // preview or failed validation).
+  $widgets = array();
+  foreach (element_children($element) as $key) {
+    $widgets[] = &$element[$key];
+  }
+  usort($widgets, '_field_sort_items_value_helper');
 
-  return $return;
+  $rows = array();
+  foreach ($widgets as $key => &$widget) {
+    // Save the uploading row for last.
+    if ($widget['#file'] == FALSE) {
+      $widget['#title'] = $element['#file_upload_title'];
+      continue;
+    }
+
+    // Delay rendering of the buttons, so that they can be rendered later in the
+    // "operations" column.
+    $operations_elements = array();
+    foreach (element_children($widget) as $sub_key) {
+      if (isset($widget[$sub_key]['#type']) && $widget[$sub_key]['#type'] == 'submit') {
+        hide($widget[$sub_key]);
+        $operations_elements[] = &$widget[$sub_key];
+      }
+    }
+
+    // Delay rendering of the "Display" option and the weight selector, so that
+    // each can be rendered later in its own column.
+    if ($element['#display_field']) {
+      hide($widget['display']);
+    }
+    hide($widget['_weight']);
+
+    // Render everything else together in a column, without the normal wrappers.
+    $widget['#theme_wrappers'] = array();
+    $information = drupal_render($widget);
+
+    // Render the previously hidden elements, using render() instead of
+    // drupal_render(), to undo the earlier hide().
+    $operations = '';
+    foreach ($operations_elements as $operation_element) {
+      $operations .= render($operation_element);
+    }
+    $display = '';
+    if ($element['#display_field']) {
+      unset($widget['display']['#title']);
+      $display = array(
+        'data' => render($widget['display']),
+        'class' => array('checkbox'),
+      );
+    }
+    $widget['_weight']['#attributes']['class'] = array($weight_class);
+    $weight = render($widget['_weight']);
+
+    // Arrange the row with all of the rendered columns.
+    $row = array();
+    $row[] = $information;
+    if ($element['#display_field']) {
+      $row[] = $display;
+    }
+    $row[] = $weight;
+    $row[] = $operations;
+    $rows[] = array(
+      'data' => $row,
+      'class' => isset($widget['#attributes']['class']) ? array_merge($widget['#attributes']['class'], array('draggable')) : array('draggable'),
+    );
+  }
+
+  drupal_add_tabledrag($table_id, 'order', 'sibling', $weight_class);
+
+  $output = '';
+  $output = empty($rows) ? '' : theme('table', array('header' => $headers, 'rows' => $rows, 'attributes' => array('id' => $table_id)));
+  $output .= drupal_render_children($element);
+  return $output;
 }
diff --git a/includes/media.pages.inc b/includes/media.pages.inc
deleted file mode 100644
index b22a8b1..0000000
--- a/includes/media.pages.inc
+++ /dev/null
@@ -1,33 +0,0 @@
-<?php
-
-/**
- * @file
- * Common pages for the Media module.
- */
-
-/**
- * CTools modal callback for editing a file.
- */
-function media_file_edit_modal($form, &$form_state, $file, $js) {
-  ctools_include('modal');
-  ctools_include('ajax');
-
-  $form_state['ajax'] = $js;
-  form_load_include($form_state, 'inc', 'file_entity', 'file_entity.pages');
-
-  $output = ctools_modal_form_wrapper('file_entity_edit', $form_state);
-
-  if ($js) {
-    $commands = $output;
-
-    if ($form_state['executed']) {
-      $commands = array(ctools_modal_command_dismiss(t('File saved')));
-    }
-
-    print ajax_render($commands);
-    exit();
-  }
-
-  // Otherwise, just return the output.
-  return $output;
-}
diff --git a/includes/media.theme.inc b/includes/media.theme.inc
index 9f2ff31..38d2bfa 100644
--- a/includes/media.theme.inc
+++ b/includes/media.theme.inc
@@ -8,6 +8,35 @@
  */
 
 /**
+ * Returns HTML for a managed file element.
+ *
+ * @param $variables
+ *   An associative array containing:
+ *   - element: A render element representing the file.
+ *
+ * @ingroup themeable
+ */
+function theme_media_element($variables) {
+  $element = $variables['element'];
+
+  $attributes = array();
+  if (isset($element['#id'])) {
+    $attributes['id'] = $element['#id'];
+  }
+  if (!empty($element['#attributes']['class'])) {
+    $attributes['class'] = (array) $element['#attributes']['class'];
+  }
+  $attributes['class'][] = 'form-media';
+
+  // This wrapper is required to apply JS behaviors and CSS styling.
+  $output = '';
+  $output .= '<div' . drupal_attributes($attributes) . '>';
+  $output .= drupal_render_children($element);
+  $output .= '</div>';
+  return $output;
+}
+
+/**
  * Add messages to the page.
  */
 function template_preprocess_media_dialog_page(&$variables) {
diff --git a/js/media.js b/js/media.js
index 04585bd..33e1c5a 100644
--- a/js/media.js
+++ b/js/media.js
@@ -1,97 +1,64 @@
-
 /**
- *  @file
- *  This file handles the JS for Media Module functions.
+ * @file
+ * Provides JavaScript additions to the media field widget.
+ *
+ * This file provides support for launching the media browser to select and
+ * attach existing files.
  */
 
 (function ($) {
 
-/**
- * Loads media browsers and callbacks, specifically for media as a field.
- */
-Drupal.behaviors.mediaElement = {
-  attach: function (context, settings) {
-    // Options set from media.fields.inc for the types, etc to show in the browser.
-
-    // For each widget (in case of multi-entry)
-    $('.media-widget', context).once('mediaBrowserLaunch', function () {
-      var options = settings.media.elements[this.id];
-      globalOptions = {};
-      if (options.global != undefined) {
-        var globalOptions = options.global;
+  /**
+   * Attach behaviors to media element browse fields.
+   */
+  Drupal.behaviors.media = {
+    attach: function (context, settings) {
+      if (settings.media && settings.media.elements) {
+        $.each(settings.media.elements, function(selector) {
+          var configuration = settings.media.elements[selector];
+          $(selector, context).bind('click', {configuration: configuration}, Drupal.behaviors.media.openBrowser);
+        });
+      }
+    },
+    detach: function (context, settings) {
+      if (settings.media && settings.media.elements) {
+        $.each(settings.media.elements, function(selector) {
+          $(selector, context).unbind('click', Drupal.behaviors.media.openBrowser);
+        });
       }
-      //options = Drupal.settings.media.fields[this.id];
-      var fidField = $('.fid', this);
-      var previewField = $('.preview', this);
-      var editButton = $('.edit', this);
-      var removeButton = $('.remove', this);
+    },
+    openBrowser: function (event) {
+      var clickedButton = this;
+      var configuration = event.data.configuration.global;
+
+      // Find the file ID and preview fields.
+      var fidField = $(this).siblings('.fid');
+      var previewField = $(this).siblings('.preview');
 
-      // Hide the edit and remove buttons if there is no file data yet.
-      if (fidField.val() == 0) {
-        if (editButton.length) {
-          editButton.hide();
+      // Find the edit and remove buttons.
+      var editButton = $(this).siblings('.edit');
+      var removeButton = $(this).siblings('.remove');
+
+      // Launch the media browser.
+      Drupal.media.popups.mediaBrowser(function (mediaFiles) {
+        // Ensure that there was at least one media file selected.
+        if (mediaFiles.length < 0) {
+          return;
         }
-        removeButton.hide();
-      }
 
-      // When someone clicks the link to pick media (or clicks on an existing thumbnail)
-      $('.launcher', this).bind('click', function (e) {
-        // Launch the browser, providing the following callback function
-        // @TODO: This should not be an anomyous function.
-        Drupal.media.popups.mediaBrowser(function (mediaFiles) {
-          if (mediaFiles.length < 0) {
-            return;
-          }
-          var mediaFile = mediaFiles[0];
-          // Set the value of the filefield fid (hidden) and trigger a change.
-          fidField.val(mediaFile.fid);
-          fidField.trigger('change');
-          // Set the preview field HTML.
-          previewField.html(mediaFile.preview);
-        }, globalOptions);
-        e.preventDefault();
-      });
+        // Grab the first of the selected media files.
+        var mediaFile = mediaFiles[0];
 
-      // When someone clicks the Remove button.
-      $('.remove', this).bind('click', function (e) {
-        // Set the value of the filefield fid (hidden) and trigger change.
-        fidField.val(0);
+        // Set the value of the hidden file ID field and trigger a change.
+        fidField.val(mediaFile.fid);
         fidField.trigger('change');
-        // Set the preview field HTML.
-        previewField.html('');
-        e.preventDefault();
-      });
 
-      // Show or hide the edit/remove buttons if the field has a file or not.
-      $('.fid', this).bind('change', function() {
-        if (fidField.val() == 0) {
-          if (editButton.length) {
-            editButton.hide();
-          }
-          removeButton.hide();
-        }
-        else {
-          if (editButton.length) {
-            var url = Drupal.settings.basePath + 'file/' + fidField.val() + '/edit';
-            $.ajax({
-              url: location.protocol + '//' + location.host + url,
-              type: 'HEAD',
-              success: function(data) {
-                editButton.attr('href', editButton.attr('href').replace(/media\/\d+\/edit/, 'media/' + fidField.val() + '/edit'));
-                // Re-process the edit link through CTools modal behaviors.
-                editButton.unbind('click');
-                editButton.removeClass('ctools-use-modal-processed');
-                // @todo Maybe add support for Drupal.detachBehaviors in Drupal.behaviors.ZZCToolsModal?
-                Drupal.attachBehaviors(editButton.parent(), Drupal.settings);
-                editButton.show();
-              }
-            });
-          }
-          removeButton.show();
-        }
-      });
-    });
-  }
-};
+        // Display a preview of the file using the selected media file's display.
+        previewField.html(mediaFile.preview);
+      }, configuration);
+
+      return false;
+    }
+  };
 
 })(jQuery);
diff --git a/media.module b/media.module
index 197f62c..1db30de 100644
--- a/media.module
+++ b/media.module
@@ -103,6 +103,14 @@ function media_menu() {
     'weight' => 10,
   );
 
+  $items['media/ajax'] = array(
+    'page callback' => 'media_ajax_upload',
+    'delivery callback' => 'ajax_deliver',
+    'access arguments' => array('access content'),
+    'theme callback' => 'ajax_base_page_theme',
+    'type' => MENU_CALLBACK,
+  );
+
   $items['media/browser'] = array(
     'title' => 'Media browser',
     'description' => 'Media Browser for picking media and uploading new media',
@@ -125,18 +133,6 @@ function media_menu() {
     'file' => 'includes/media.browser.inc',
   );
 
-  // We could re-use the file/%file/edit path for the modal callback, but
-  // it is just easier to use our own namespace here.
-  $items['media/%file/edit/%ctools_js'] = array(
-    'title' => 'Edit',
-    'page callback' => 'drupal_get_form',
-    'page arguments' => array('media_file_edit_modal', 1, 3),
-    'access callback' => 'file_entity_access',
-    'access arguments' => array('update', 1),
-    'file' => 'includes/media.pages.inc',
-    'type' => MENU_CALLBACK,
-  );
-
   return $items;
 }
 
@@ -215,10 +211,84 @@ function media_theme() {
       'variables' => array('file' => NULL, 'attributes' => array(), 'style_name' => 'media_thumbnail'),
       'file' => 'includes/media.theme.inc',
     ),
+
+    // media.field.inc.
+    'media_widget' => array(
+      'render element' => 'element',
+    ),
+    'media_widget_multiple' => array(
+      'render element' => 'element',
+    ),
+    'media_formatter_table' => array(
+      'variables' => array('items' => NULL),
+    ),
   );
 }
 
 /**
+ * Menu callback; Shared Ajax callback for file uploads and deletions.
+ *
+ * This rebuilds the form element for a particular field item. As long as the
+ * form processing is properly encapsulated in the widget element the form
+ * should rebuild correctly using FAPI without the need for additional callbacks
+ * or processing.
+ */
+function media_ajax_upload() {
+  $form_parents = func_get_args();
+  $form_build_id = (string) array_pop($form_parents);
+
+  if (empty($_POST['form_build_id']) || $form_build_id != $_POST['form_build_id']) {
+    // Invalid request.
+    drupal_set_message(t('An unrecoverable error occurred. The uploaded file likely exceeded the maximum file size (@size) that this server supports.', array('@size' => format_size(file_upload_max_size()))), 'error');
+    $commands = array();
+    $commands[] = ajax_command_replace(NULL, theme('status_messages'));
+    return array('#type' => 'ajax', '#commands' => $commands);
+  }
+
+  list($form, $form_state) = ajax_get_form();
+
+  if (!$form) {
+    // Invalid form_build_id.
+    drupal_set_message(t('An unrecoverable error occurred. Use of this form has expired. Try reloading the page and submitting again.'), 'error');
+    $commands = array();
+    $commands[] = ajax_command_replace(NULL, theme('status_messages'));
+    return array('#type' => 'ajax', '#commands' => $commands);
+  }
+
+  // Get the current element and count the number of files.
+  $current_element = $form;
+  foreach ($form_parents as $parent) {
+    $current_element = $current_element[$parent];
+  }
+  $current_file_count = isset($current_element['#file_upload_delta']) ? $current_element['#file_upload_delta'] : 0;
+
+  // Process user input. $form and $form_state are modified in the process.
+  drupal_process_form($form['#form_id'], $form, $form_state);
+
+  // Retrieve the element to be rendered.
+  foreach ($form_parents as $parent) {
+    $form = $form[$parent];
+  }
+
+  // Add the special Ajax class if a new file was added.
+  if (isset($form['#file_upload_delta']) && $current_file_count < $form['#file_upload_delta']) {
+    $form[$current_file_count]['#attributes']['class'][] = 'ajax-new-content';
+  }
+  // Otherwise just add the new content class on a placeholder.
+  else {
+    $form['#suffix'] .= '<span class="ajax-new-content"></span>';
+  }
+
+  $output = theme('status_messages') . drupal_render($form);
+  $js = drupal_add_js();
+  $settings = call_user_func_array('array_merge_recursive', $js['settings']['data']);
+
+  $commands = array();
+  $commands[] = ajax_command_replace(NULL, $output, $settings);
+  return array('#type' => 'ajax', '#commands' => $commands);
+}
+
+/**
  * Implements hook_image_default_styles().
  */
 function media_image_default_styles() {
@@ -259,38 +329,12 @@ function media_page_alter(&$page) {
 }
 
 /**
- * Implements hook_form_FIELD_UI_FIELD_SETTINGS_FORM_alter().
- *
- * @todo: Respect field settings in 7.x-2.x and handle them in the media widget
- * UI.
- */
-function media_form_field_ui_field_settings_form_alter(&$form, &$form_state) {
-  // On file fields that use the media widget we need remove specific fields.
-  if ($form['field']['type']['#value'] == 'file') {
-    $fields = field_info_instances($form['#entity_type'], $form['#bundle']);
-    if ($fields[$form['field']['field_name']['#value']]['widget']['type'] == 'media_generic') {
-      $form['field']['settings']['display_field']['#access'] = FALSE;
-      $form['field']['settings']['display_default']['#access'] = FALSE;
-    }
-  }
-}
-
-/**
  * Implements hook_form_FIELD_UI_FIELD_EDIT_FORM_alter().
  *
  * @todo: Respect field settings in 7.x-2.x and handle them in the media widget
  * UI.
  */
 function media_form_field_ui_field_edit_form_alter(&$form, &$form_state) {
-  // On file fields that use the media widget we need remove specific fields.
-  if ($form['#field']['type'] == 'file' && $form['instance']['widget']['type']['#value'] == 'media_generic') {
-    $form['field']['settings']['display_field']['#access'] = FALSE;
-    $form['field']['settings']['display_default']['#access'] = FALSE;
-    $form['instance']['settings']['description_field']['#access'] = FALSE;
-    $form['instance']['settings']['file_extensions']['#title'] = t('Allowed file extensions for uploaded files');
-    $form['instance']['settings']['file_extensions']['#maxlength'] = 255;
-  }
-
   // On image fields using the media widget we remove the alt/title fields.
   if ($form['#field']['type'] == 'image' && $form['instance']['widget']['type']['#value'] == 'media_generic') {
     $form['instance']['settings']['alt_field']['#access'] = FALSE;
@@ -310,23 +354,6 @@ function media_form_field_ui_field_edit_form_alter(&$form, &$form_state) {
 }
 
 /**
- * Implements hook_form_FORM_ID_alter().
- */
-function media_form_file_entity_edit_alter(&$form, &$form_state) {
-  // Make adjustments to the file edit form when used in a CTools modal.
-  if (!empty($form_state['ajax'])) {
-    // Remove the preview and the delete button.
-    $form['preview']['#access'] = FALSE;
-    $form['actions']['delete']['#access'] = FALSE;
-
-    // Convert the cancel link to a button which triggers a modal close.
-    $form['actions']['cancel']['#attributes']['class'][] = 'button';
-    $form['actions']['cancel']['#attributes']['class'][] = 'button-no';
-    $form['actions']['cancel']['#attributes']['class'][] = 'ctools-close-modal';
-  }
-}
-
-/**
  * Implements hook_form_alter().
  */
 function media_form_alter(&$form, &$form_state, $form_id) {
@@ -561,12 +588,12 @@ function media_element_info() {
   $types['media'] = array(
     '#input' => TRUE,
     '#process' => array('media_element_process'),
-    // '#value_callback' => 'media_element_value',
+    '#value_callback' => 'media_file_value',
     '#element_validate' => array('media_element_validate'),
-    '#theme_wrappers' => array('container'),
-    '#progress_indicator' => 'throbber',
+    '#pre_render' => array('media_element_pre_render'),
+    '#theme' => 'media_element',
+    '#theme_wrappers' => array('form_element'),
     '#extended' => FALSE,
-    '#required' => FALSE,
     '#media_options' => array(
       'global' => array(
         // Example: array('image', 'audio');
@@ -575,112 +602,192 @@ function media_element_info() {
         'schemes' => array(),
       ),
     ),
-    '#attributes' => array(
-      'class' => array('media-widget', 'form-item'),
-    ),
     '#attached' => array(
       'library' => array(
         array('media', 'media_browser'),
       ),
     ),
   );
+
+  $setting = array();
+  $setting['media']['global'] = $types['media']['#media_options'];
+
+  $types['media']['#attached']['js'][] = array(
+    'type' => 'setting',
+    'data' => $setting,
+  );
+
   return $types;
 }
 
 /**
  * Process callback for the media form element.
  */
-function media_element_process(&$element, &$form_state, $form) {
-  $fid = isset($element['#value']['fid']) ? $element['#value']['fid'] : 0;
-  $file = $fid ? file_load($fid) : FALSE;
-
-  // Add the CTools modal JavaScript for the edit button if necessary.
-  ctools_include('modal');
+function media_element_process($element, &$form_state, $form) {
+  // Add the CTools modal JavaScript for the edit button.
   ctools_include('ajax');
+  ctools_include('modal');
   ctools_modal_add_js();
 
+  $fid = isset($element['#value']['fid']) ? $element['#value']['fid'] : 0;
+
   // Set some default element properties.
-  $element['#file'] = $file;
-
-  $element['title'] = array(
-    '#type' => 'item',
-    '#title' => $element['#title'],
-    '#description' => $element['#description'],
-    '#required' => $element['#required'],
-    '#weight' => -100,
+  $element['#file'] = $fid ? file_load($fid) : FALSE;
+  $element['#tree'] = TRUE;
+
+  $ajax_settings = array(
+    'path' => 'media/ajax/' . implode('/', $element['#array_parents']) . '/' . $form['form_build_id']['#value'],
+    'wrapper' => $element['#id'] . '-ajax-wrapper',
+    'effect' => 'fade',
+  );
+
+  // Set up the buttons first since we need to check if they were clicked.
+  $element['attach_button'] = array(
+    '#name' => implode('_', $element['#parents']) . '_attach_button',
+    '#type' => 'submit',
+    '#value' => t('Attach'),
+    '#validate' => array(),
+    '#submit' => array('media_file_submit'),
+    '#limit_validation_errors' => array($element['#parents']),
+    '#ajax' => $ajax_settings,
+    '#attributes' => array('class' => array('button', 'attach')),
+    '#weight' => -5,
   );
-  if (isset($element['#title_display'])) {
-    $element['title']['#title_display'] = $element['#title_display'];
-  }
 
-  // @todo This should load from the JS in case of a failed form submission.
   $element['preview'] = array(
-    '#prefix' => '<div class="preview launcher">',
+    'content' => array(),
+    '#prefix' => '<div class="preview">',
     '#suffix' => '</div>',
-    '#weight' => 0,
-    'content' => $file ? media_get_thumbnail_preview($file) : array(),
+    '#ajax' => $ajax_settings,
+    '#weight' => -10,
   );
 
-  // @todo: Perhaps this language logic should be handled by JS since the state
-  // changes on client side after choosing an item.
-  $element['select'] = array(
+  // Substitute the JS preview for a true file thumbnail and disable browsing
+  // for files once the file is attached.
+  if ($element['#file']) {
+    $element['preview']['content'] = media_get_thumbnail_preview($element['#file']);
+    $element['preview']['#prefix'] = '';
+    $element['preview']['#suffix'] = '';
+  }
+
+  $element['browse_button'] = array(
     '#type' => 'link',
     '#href' => '',
-    '#title' => t('Select'),
-    '#attributes' => array('class' => array('button', 'launcher')),
+    '#title' => t('Browse'),
+    '#attributes' => array('class' => array('button', 'browse')),
     '#options' => array('fragment' => FALSE, 'external' => TRUE),
-    '#weight' => 10,
+    '#weight' => -9,
   );
-  // @todo Figure out how to update the preview image after the Edit modal is
-  // closed.
-  $element['edit'] = array(
-    '#type' => 'link',
-    '#href' => 'media/' . $fid . '/edit/nojs',
-    '#title' => t('Edit'),
-    '#attributes' => array(
-      'class' => array(
-        // Required for CTools modal to work.
-        'ctools-use-modal', 'use-ajax',
-        'ctools-modal-media-file-edit', 'button', 'edit',
-      ),
-    ),
-    '#weight' => 20,
-    '#access' => $file ? file_entity_access('update', $file) : TRUE, // only do perm check for existing files
-  );
-  $element['remove'] = array(
-    '#type' => 'link',
-    '#href' => '',
-    '#title' => t('Remove'),
+
+  // Force the progress indicator for the remove button to be either 'none' or
+  // 'throbber', even if the upload button is using something else.
+  $ajax_settings['progress']['type'] = 'throbber';
+  $ajax_settings['progress']['message'] = NULL;
+  $ajax_settings['effect'] = 'none';
+  $element['remove_button'] = array(
+    '#name' => implode('_', $element['#parents']) . '_remove_button',
+    '#type' => 'submit',
+    '#value' => t('Remove'),
+    '#validate' => array(),
+    '#submit' => array('media_file_submit'),
+    '#limit_validation_errors' => array($element['#parents']),
+    '#ajax' => $ajax_settings,
     '#attributes' => array('class' => array('button', 'remove')),
-    '#options' => array('fragment' => FALSE, 'external' => TRUE),
-    '#weight' => 30,
+    '#weight' => -3,
+    '#access' => $element['#file'] ? file_entity_access('delete', $element['#file']) : TRUE, // only do perm check for existing files
   );
 
   $element['fid'] = array(
     '#type' => 'hidden',
     '#value' => $fid,
     '#attributes' => array('class' => array('fid')),
-    '#weight' => 100,
   );
 
   // Media browser attach code.
   $element['#attached']['js'][] = drupal_get_path('module', 'media') . '/js/media.js';
 
-  $setting = array();
-  $setting['media']['elements'][$element['#id']] = $element['#media_options'];
-
-  $element['#attached']['js'][] = array(
-    'type' => 'setting',
-    'data' => $setting,
+  // Add the media options to the page as JavaScript settings.
+  $element['browse_button']['#attached']['js'] = array(
+    array(
+      'type' => 'setting',
+      'data' => array('media' => array('elements' => array('#' . $element['#id'] . '-browse-button' => $element['#media_options'])))
+    )
   );
 
-  // @todo: Might need to think about this. All settings would likely apply to
-  // all media in a multi-value, but what about passing the existing fid?
   module_load_include('inc', 'media', 'includes/media.browser');
   media_attach_browser_js($element);
 
+  // Prefix and suffix used for Ajax replacement.
+  $element['#prefix'] = '<div id="' . $element['#id'] . '-ajax-wrapper">';
+  $element['#suffix'] = '</div>';
+
   return $element;
-  // @todo: make this work for file and image fields.
+}
+
+/**
+ * The #value_callback for a media type element.
+ */
+function media_file_value(&$element, $input = FALSE, $form_state = NULL) {
+  $fid = 0;
+
+  // Find the current value of this field from the form state.
+  $form_state_fid = $form_state['values'];
+  foreach ($element['#parents'] as $parent) {
+    $form_state_fid = isset($form_state_fid[$parent]) ? $form_state_fid[$parent] : 0;
+  }
+
+  if ($element['#extended'] && isset($form_state_fid['fid'])) {
+    $fid = $form_state_fid['fid'];
+  }
+  elseif (is_numeric($form_state_fid)) {
+    $fid = $form_state_fid;
+  }
+
+  // Process any input and save new uploads.
+  if ($input !== FALSE) {
+    $return = $input;
+
+    // Uploads take priority over all other values.
+    if ($file = file_managed_file_save_upload($element)) {
+      $fid = $file->fid;
+    }
+    else {
+      // Check for #filefield_value_callback values.
+      // Because FAPI does not allow multiple #value_callback values like it
+      // does for #element_validate and #process, this fills the missing
+      // functionality to allow File fields to be extended through FAPI.
+      if (isset($element['#file_value_callbacks'])) {
+        foreach ($element['#file_value_callbacks'] as $callback) {
+          $callback($element, $input, $form_state);
+        }
+      }
+      // Load file if the FID has changed to confirm it exists.
+      if (isset($input['fid']) && $file = file_load($input['fid'])) {
+        $fid = $file->fid;
+      }
+    }
+  }
+
+  // If there is no input, set the default value.
+  else {
+    if ($element['#extended']) {
+      $default_fid = isset($element['#default_value']['fid']) ? $element['#default_value']['fid'] : 0;
+      $return = isset($element['#default_value']) ? $element['#default_value'] : array('fid' => 0);
+    }
+    else {
+      $default_fid = isset($element['#default_value']) ? $element['#default_value'] : 0;
+      $return = array('fid' => 0);
+    }
+
+    // Confirm that the file exists when used as a default value.
+    if ($default_fid && $file = file_load($default_fid)) {
+      $fid = $file->fid;
+    }
+  }
+
+  $return['fid'] = $fid;
+
+  return $return;
 }
 
 /**
@@ -690,20 +797,96 @@ function media_element_process(&$element, &$form_state, $form) {
  * necessary in order to respect the #required property.
  */
 function media_element_validate(&$element, &$form_state) {
-  if ($element['#required']) {
-    $has_value = FALSE;
-    $widget_parents = $element['#array_parents'];
-    array_pop($widget_parents);
-    $items = drupal_array_get_nested_value($form_state['values'], $widget_parents);
-    foreach ($items as $value) {
-      if (is_array($value) && !empty($value['fid'])) {
-        $has_value = TRUE;
-      }
-    }
-    if (!$has_value) {
-      form_error($element, t('%element_title field is required.', array('%element_title' => $element['#title'])));
+  $clicked_button = end($form_state['triggering_element']['#parents']);
+
+  // Check required property based on the FID.
+  if ($element['#required'] && empty($element['fid']['#value']) && !in_array($clicked_button, array('attach_button', 'edit_button', 'remove_button'))) {
+    form_error($element['browse_button'], t('!name field is required.', array('!name' => $element['#title'])));
+  }
+
+  // Consolidate the array value of this field to a single FID.
+  if (!$element['#extended']) {
+    form_set_value($element, $element['fid']['#value'], $form_state);
+  }
+}
+
+/**
+ * Form submission handler for attach / edit / remove buttons of media elements.
+ *
+ * @see media_element_process()
+ */
+function media_file_submit($form, &$form_state) {
+  // Determine whether it was the attach, edit or remove button that was
+  // clicked, and set $element to the managed_file element that contains that
+  // button.
+  $parents = $form_state['triggering_element']['#array_parents'];
+  $button_key = array_pop($parents);
+  $element = drupal_array_get_nested_value($form, $parents);
+
+  // No action is needed here for the attach button, because all file uploads on
+  // the form are processed by media_file_value() regardless of which button was
+  // clicked. Action is needed here for the remove button, because we only
+  // remove a file in response to its remove button being clicked.
+  if ($button_key == 'remove_button') {
+    // If it's a temporary file we can safely remove it immediately, otherwise
+    // it's up to the implementing module to clean up files that are in use.
+    if ($element['#file'] && $element['#file']->status == 0) {
+      file_delete($element['#file']);
     }
+    // Update both $form_state['values'] and $form_state['input'] to reflect
+    // that the file has been removed, so that the form is rebuilt correctly.
+    // $form_state['values'] must be updated in case additional submit handlers
+    // run, and for form building functions that run during the rebuild, such as
+    // when the media element is part of a field widget.
+    // $form_state['input'] must be updated so that media_file_value() has
+    // correct information during the rebuild.
+    $values_element = $element['#extended'] ? $element['fid'] : $element;
+    form_set_value($values_element, NULL, $form_state);
+    drupal_array_set_nested_value($form_state['input'], $values_element['#parents'], NULL);
   }
+
+  // Set the form to rebuild so that $form is correctly updated in response to
+  // processing the file removal. Since this function did not change $form_state
+  // if the upload button was clicked, a rebuild isn't necessary in that
+  // situation and setting $form_state['redirect'] to FALSE would suffice.
+  // However, we choose to always rebuild, to keep the form processing workflow
+  // consistent between the two buttons.
+  $form_state['rebuild'] = TRUE;
+}
+
+/**
+ * #pre_render callback to hide display of the browse/attach or edit/remove
+ * controls.
+ *
+ * Browse/attach controls are hidden when a file is already uploaded.
+ * Edit/Remove controls are hidden when there is no file attached. Controls are
+ * hidden here instead of in media_element_process(), because #access for these
+ * buttons depends on the media element's #value. See the documentation of
+ * form_builder() for more detailed information about the relationship between
+ * #process, #value, and #access.
+ *
+ * Because #access is set here, it affects display only and does not prevent
+ * JavaScript or other untrusted code from submitting the form as though access
+ * were enabled. The form processing functions for these elements should not
+ * assume that the buttons can't be "clicked" just because they are not
+ * displayed.
+ *
+ * @see media_element_process()
+ * @see form_builder()
+ */
+function media_element_pre_render($element) {
+  // If we already have a file, we don't want to show the browse and attach
+  // controls.
+  if (!empty($element['#value']['fid'])) {
+    $element['browse_button']['#access'] = FALSE;
+    $element['attach_button']['#access'] = FALSE;
+  }
+  // If we don't already have a file, there is nothing to edit or remove.
+  else {
+    $element['edit_button']['#access'] = FALSE;
+    $element['remove_button']['#access'] = FALSE;
+  }
+  return $element;
 }
 
 /**
