Repository: spyder-ide/spyder

Issue:
DataFrame viewer should display tooltips for truncated column headers
When a DataFrame with long column names is viewed, the column names are truncated. It would be useful to have the full column names displayed as tooltips when pointing at them.

I my case, I have got many columns with long names, so I want to avoid pressing the Resize button to keep the data compact.

(Seen in WinPython-64bit-3.5.2.3 with "Spyder 3.1.0.dev0".)



Discussion:
Thanks for the suggestion. We'll try to work on it for Spyder 3.2.
@malliwi88, please work on this one.
Working on this, I already made rebase/merge 3.x, according to your suggestion #3611. @ccordoba12 :+1: 
@jondo 

You could provide a code or capture to take as an example of a problem case?, Please.
I think that it would have a base case to illustrate this new feature in the future.

Thanks

The screenshot shows part of a DataFrame with over 400 columns. The highlighted column has the long name `QC_CAQ_RMSE_0_95_Percentile_Long_MW_MESSWERT`. It would be great to have this name displayed as column header tooltip.
![screenshot](https://cloud.githubusercontent.com/assets/21648/20917954/d50167c0-bb94-11e6-88e0-b9666b69004f.png)
@camendoza94, please work on this one.

The idea is that after putting the mouse over a column name, a tooltip be shown with the full column's name. For this you need to work on this file

    spyder/widgets/variableexplorer/dataframeeditor.py

Resolving patch:
diff --git a/spyder/widgets/variableexplorer/dataframeeditor.py b/spyder/widgets/variableexplorer/dataframeeditor.py
--- a/spyder/widgets/variableexplorer/dataframeeditor.py
+++ b/spyder/widgets/variableexplorer/dataframeeditor.py
@@ -8,20 +8,34 @@
 # Present in pandas.sandbox.qtpandas in v0.13.1
 # Copyright (c) 2011-2012, Lambda Foundry, Inc.
 # and PyData Development Team All rights reserved
+#
+# DataFrameHeaderModel and DataFrameLevelModel are based on the classes
+# Header4ExtModel and Level4ExtModel from the gtabview project. The
+# DataFrameModel is based on the classes ExtDataModel and ExtFrameModel from
+# the same project. Also the DataFrameEditor is based in the ExtTableView.
+# https://github.com/TabViewer/gtabview
+# Copyright(c) 2014-2016: wave++ "Yuri D'Elia" <wavexx@thregr.org>
+# Copyright(c) 2014-2015: Scott Hansen <firecat4153@gmail.com>
+# Licensed under the terms of the MIT License
+#
 
 """
 Pandas DataFrame Editor Dialog
 """
 
+# Standard library imports
+import time
+
 # Third party imports
-from qtpy import API
 from qtpy.compat import from_qvariant, to_qvariant
-from qtpy.QtCore import QAbstractTableModel, QModelIndex, Qt, Signal, Slot
+from qtpy.QtCore import (QAbstractTableModel, QModelIndex, Qt, Signal, Slot,
+                         QItemSelectionModel, QEvent)
 from qtpy.QtGui import QColor, QCursor
 from qtpy.QtWidgets import (QApplication, QCheckBox, QDialogButtonBox, QDialog,
                             QGridLayout, QHBoxLayout, QInputDialog, QLineEdit,
                             QMenu, QMessageBox, QPushButton, QTableView,
-                            QHeaderView)
+                            QScrollBar, QTableWidget, QFrame,
+                            QItemDelegate)
 
 from pandas import DataFrame, Index, Series
 import numpy as np
@@ -51,6 +65,8 @@
 LARGE_SIZE = 5e5
 LARGE_NROWS = 1e5
 LARGE_COLS = 60
+ROWS_TO_LOAD = 500
+COLS_TO_LOAD = 40
 
 # Background colours
 BACKGROUND_NUMBER_MINHUE = 0.66 # hue for largest number
@@ -66,8 +82,9 @@
 
 def bool_false_check(value):
     """
-    Used to convert bool intrance to false since any string in bool('')
-    will return True
+    Used to convert bool entrance to false.
+
+    Needed since any string in bool('') will return True.
     """
     if value.lower() in _bool_false:
         value = ''
@@ -75,18 +92,22 @@ def bool_false_check(value):
 
 
 def global_max(col_vals, index):
-    """Returns the global maximum and minimum"""
+    """Returns the global maximum and minimum."""
     col_vals_without_None = [x for x in col_vals if x is not None]
     max_col, min_col = zip(*col_vals_without_None)
     return max(max_col), min(min_col)
 
 
 class DataFrameModel(QAbstractTableModel):
-    """ DataFrame Table Model"""
-    
-    ROWS_TO_LOAD = 500
-    COLS_TO_LOAD = 40
-    
+    """ DataFrame Table Model.
+
+    Partly based in ExtDataModel and ExtFrameModel classes
+    of the gtabview project.
+
+    For more information please see:
+    https://github.com/wavexx/gtabview/blob/master/gtabview/models.py
+    """
+
     def __init__(self, dataFrame, format=DEFAULT_FORMAT, parent=None):
         QAbstractTableModel.__init__(self)
         self.dialog = parent
@@ -114,26 +135,77 @@ def __init__(self, dataFrame, format=DEFAULT_FORMAT, parent=None):
         # Use paging when the total size, number of rows or number of
         # columns is too large
         if size > LARGE_SIZE:
-            self.rows_loaded = self.ROWS_TO_LOAD
-            self.cols_loaded = self.COLS_TO_LOAD
+            self.rows_loaded = ROWS_TO_LOAD
+            self.cols_loaded = COLS_TO_LOAD
         else:
             if self.total_rows > LARGE_NROWS:
-                self.rows_loaded = self.ROWS_TO_LOAD
+                self.rows_loaded = ROWS_TO_LOAD
             else:
                 self.rows_loaded = self.total_rows
             if self.total_cols > LARGE_COLS:
-                self.cols_loaded = self.COLS_TO_LOAD
+                self.cols_loaded = COLS_TO_LOAD
             else:
                 self.cols_loaded = self.total_cols
 
+    def _axis(self, axis):
+        """
+        Return the corresponding labels taking into account the axis.
+
+        The axis could be horizontal (0) or vertical (1).
+        """
+        return self.df.columns if axis == 0 else self.df.index
+
+    def _axis_levels(self, axis):
+        """
+        Return the number of levels in the labels taking into account the axis.
+
+        Get the number of levels for the columns (0) or rows (1).
+        """
+        ax = self._axis(axis)
+        return 1 if not hasattr(ax, 'levels') else len(ax.levels)
+
+    @property
+    def shape(self):
+        """Return the shape of the dataframe."""
+        return self.df.shape
+
+    @property
+    def header_shape(self):
+        """Return the levels for the columns and rows of the dataframe."""
+        return (self._axis_levels(0), self._axis_levels(1))
+
+    @property
+    def chunk_size(self):
+        """Return the max value of the dimensions of the dataframe."""
+        return max(*self.shape())
+
+    def header(self, axis, x, level=0):
+        """
+        Return the values of the labels for the header of columns or rows.
+
+        The value corresponds to the header of column or row x in the
+        given level.
+        """
+        ax = self._axis(axis)
+        return ax.values[x] if not hasattr(ax, 'levels') \
+            else ax.values[x][level]
+
+    def name(self, axis, level):
+        """Return the labels of the levels if any."""
+        ax = self._axis(axis)
+        if hasattr(ax, 'levels'):
+            return ax.names[level]
+        if ax.name:
+            return ax.name
+
     def max_min_col_update(self):
         """
         Determines the maximum and minimum number in each column.
-        
+
         The result is a list whose k-th entry is [vmax, vmin], where vmax and
         vmin denote the maximum and minimum of the k-th column (ignoring NaN). 
         This list is stored in self.max_min_col.
-        
+
         If the k-th column has a non-numerical dtype, then the k-th entry
         is set to None. If the dtype is complex, then compute the maximum and
         minimum of the absolute values. If vmax equals vmin, then vmin is 
@@ -182,56 +254,13 @@ def colum_avg(self, state):
             self.return_max = global_max
         self.reset()
 
-    def headerData(self, section, orientation, role=Qt.DisplayRole):
-        """Set header data"""
-        if role != Qt.DisplayRole:
-            return to_qvariant()
-
-        if orientation == Qt.Horizontal:
-            if section == 0:
-                return 'Index'
-            elif section == 1 and PY2:
-                # Get rid of possible BOM utf-8 data present at the
-                # beginning of a file, which gets attached to the first
-                # column header when headers are present in the first
-                # row.
-                # Fixes Issue 2514
-                try:
-                    header = to_text_string(self.df_header[0],
-                                            encoding='utf-8-sig')
-                except:
-                    header = to_text_string(self.df_header[0])
-                return to_qvariant(header)
-            elif isinstance(self.df_header[section-1], TEXT_TYPES):
-                # Get the proper encoding of the text in the header.
-                # Fixes Issue 3896
-                if not PY2:
-                    try:
-                        header = self.df_header[section-1].encode('utf-8')
-                        coding = 'utf-8-sig'
-                    except:
-                        header = self.df_header[section-1].encode('utf-8')
-                        coding = encoding.get_coding(header)
-                else:
-                    header = self.df_header[section-1]
-                    coding = encoding.get_coding(header)
-                return to_qvariant(to_text_string(header, encoding=coding))
-            else:
-                return to_qvariant(to_text_string(self.df_header[section-1]))
-        else:
-            return to_qvariant()
-
     def get_bgcolor(self, index):
-        """Background color depending on value"""
+        """Background color depending on value."""
         column = index.column()
-        if column == 0:
-            color = QColor(BACKGROUND_NONNUMBER_COLOR)
-            color.setAlphaF(BACKGROUND_INDEX_ALPHA)
-            return color
         if not self.bgcolor_enabled:
             return
-        value = self.get_value(index.row(), column-1)
-        if self.max_min_col[column - 1] is None:
+        value = self.get_value(index.row(), column)
+        if self.max_min_col[column] is None:
             color = QColor(BACKGROUND_NONNUMBER_COLOR)
             if is_text_string(value):
                 color.setAlphaF(BACKGROUND_STRING_ALPHA)
@@ -242,18 +271,19 @@ def get_bgcolor(self, index):
                 color_func = abs
             else:
                 color_func = float
-            vmax, vmin = self.return_max(self.max_min_col, column-1)
+            vmax, vmin = self.return_max(self.max_min_col, column)
             hue = (BACKGROUND_NUMBER_MINHUE + BACKGROUND_NUMBER_HUERANGE *
                    (vmax - color_func(value)) / (vmax - vmin))
             hue = float(abs(hue))
             if hue > 1:
                 hue = 1
             color = QColor.fromHsvF(hue, BACKGROUND_NUMBER_SATURATION,
-                                    BACKGROUND_NUMBER_VALUE, BACKGROUND_NUMBER_ALPHA)
+                                    BACKGROUND_NUMBER_VALUE,
+                                    BACKGROUND_NUMBER_ALPHA)
         return color
 
     def get_value(self, row, column):
-        """Returns the value of the DataFrame"""
+        """Return the value of the DataFrame."""
         # To increase the performance iat is used but that requires error
         # handling, so fallback uses iloc
         try:
@@ -273,22 +303,19 @@ def data(self, index, role=Qt.DisplayRole):
         if role == Qt.DisplayRole or role == Qt.EditRole:
             column = index.column()
             row = index.row()
-            if column == 0:
-                return to_qvariant(to_text_string(self.df_index[row]))
+            value = self.get_value(row, column)
+            if isinstance(value, float):
+                try:
+                    return to_qvariant(self._format % value)
+                except (ValueError, TypeError):
+                    # may happen if format = '%d' and value = NaN;
+                    # see issue 4139
+                    return to_qvariant(DEFAULT_FORMAT % value)
             else:
-                value = self.get_value(row, column-1)
-                if isinstance(value, float):
-                    try:
-                        return to_qvariant(self._format % value)
-                    except (ValueError, TypeError):
-                        # may happen if format = '%d' and value = NaN;
-                        # see issue 4139
-                        return to_qvariant(DEFAULT_FORMAT % value)
-                else:
-                    try:
-                        return to_qvariant(to_text_string(value))
-                    except UnicodeDecodeError:
-                        return to_qvariant(encoding.to_unicode(value))
+                try:
+                    return to_qvariant(to_text_string(value))
+                except UnicodeDecodeError:
+                    return to_qvariant(encoding.to_unicode(value))
         elif role == Qt.BackgroundColorRole:
             return to_qvariant(self.get_bgcolor(index))
         elif role == Qt.FontRole:
@@ -298,21 +325,21 @@ def data(self, index, role=Qt.DisplayRole):
     def sort(self, column, order=Qt.AscendingOrder):
         """Overriding sort method"""
         if self.complex_intran is not None:
-            if self.complex_intran.any(axis=0).iloc[column-1]:
+            if self.complex_intran.any(axis=0).iloc[column]:
                 QMessageBox.critical(self.dialog, "Error",
                                      "TypeError error: no ordering "
                                      "relation is defined for complex numbers")
                 return False
         try:
             ascending = order == Qt.AscendingOrder
-            if column > 0:
+            if column >= 0:
                 try:
-                    self.df.sort_values(by=self.df.columns[column-1],
+                    self.df.sort_values(by=self.df.columns[column],
                                         ascending=ascending, inplace=True,
                                         kind='mergesort')
                 except AttributeError:
                     # for pandas version < 0.17
-                    self.df.sort(columns=self.df.columns[column-1],
+                    self.df.sort(columns=self.df.columns[column],
                                  ascending=ascending, inplace=True,
                                  kind='mergesort')
                 except ValueError as e:
@@ -321,6 +348,7 @@ def sort(self, column, order=Qt.AscendingOrder):
                                          "ValueError: %s" % to_text_string(e))
                 self.update_df_index()
             else:
+                # To sort by index
                 self.df.sort_index(inplace=True, ascending=ascending)
                 self.update_df_index()
         except TypeError as e:
@@ -333,8 +361,6 @@ def sort(self, column, order=Qt.AscendingOrder):
 
     def flags(self, index):
         """Set flags"""
-        if index.column() == 0:
-            return Qt.ItemIsEnabled | Qt.ItemIsSelectable
         return Qt.ItemFlags(QAbstractTableModel.flags(self, index) |
                             Qt.ItemIsEditable)
 
@@ -349,19 +375,19 @@ def setData(self, index, value, role=Qt.EditRole, change_type=None):
                 val = from_qvariant(value, str)
                 if change_type is bool:
                     val = bool_false_check(val)
-                self.df.iloc[row, column - 1] = change_type(val)
+                self.df.iloc[row, column] = change_type(val)
             except ValueError:
-                self.df.iloc[row, column - 1] = change_type('0')
+                self.df.iloc[row, column] = change_type('0')
         else:
             val = from_qvariant(value, str)
-            current_value = self.get_value(row, column-1)
+            current_value = self.get_value(row, column)
             if isinstance(current_value, bool):
                 val = bool_false_check(val)
             supported_types = (bool,) + REAL_NUMBER_TYPES + COMPLEX_NUMBER_TYPES
             if (isinstance(current_value, supported_types) or 
                     is_text_string(current_value)):
                 try:
-                    self.df.iloc[row, column-1] = current_value.__class__(val)
+                    self.df.iloc[row, column] = current_value.__class__(val)
                 except ValueError as e:
                     QMessageBox.critical(self.dialog, "Error",
                                          "Value error: %s" % str(e))
@@ -385,29 +411,18 @@ def rowCount(self, index=QModelIndex()):
         else:
             return self.rows_loaded
 
-    def can_fetch_more(self, rows=False, columns=False):
-        if rows:
-            if self.total_rows > self.rows_loaded:
-                return True
-            else:
-                return False
-        if columns:
-            if self.total_cols > self.cols_loaded:
-                return True
-            else:
-                return False
-
     def fetch_more(self, rows=False, columns=False):
-        if self.can_fetch_more(rows=rows):
+        """Get more columns and/or rows."""
+        if rows and self.total_rows > self.rows_loaded:
             reminder = self.total_rows - self.rows_loaded
-            items_to_fetch = min(reminder, self.ROWS_TO_LOAD)
+            items_to_fetch = min(reminder, ROWS_TO_LOAD)
             self.beginInsertRows(QModelIndex(), self.rows_loaded,
                                  self.rows_loaded + items_to_fetch - 1)
             self.rows_loaded += items_to_fetch
             self.endInsertRows()
-        if self.can_fetch_more(columns=columns):
+        if columns and self.total_cols > self.cols_loaded:
             reminder = self.total_cols - self.cols_loaded
-            items_to_fetch = min(reminder, self.COLS_TO_LOAD)
+            items_to_fetch = min(reminder, COLS_TO_LOAD)
             self.beginInsertColumns(QModelIndex(), self.cols_loaded,
                                     self.cols_loaded + items_to_fetch - 1)
             self.cols_loaded += items_to_fetch
@@ -419,75 +434,40 @@ def columnCount(self, index=QModelIndex()):
         if len(self.df.shape) == 1:
             return 2
         elif self.total_cols <= self.cols_loaded:
-            return self.total_cols + 1
+            return self.total_cols
         else:
-            return self.cols_loaded + 1
+            return self.cols_loaded
 
     def reset(self):
         self.beginResetModel()
         self.endResetModel()
 
 
-class FrozenTableView(QTableView):
-    """This class implements a table with its first column frozen
-    For more information please see:
-    http://doc.qt.io/qt-5/qtwidgets-itemviews-frozencolumn-example.html"""
-    def __init__(self, parent):
-        """Constructor."""
-        QTableView.__init__(self, parent)
-        self.parent = parent
-        self.setModel(parent.model())
-        self.setFocusPolicy(Qt.NoFocus)
-        self.verticalHeader().hide()
-        try:
-            self.horizontalHeader().setSectionResizeMode(QHeaderView.Fixed)
-        except:  # support for qtpy<1.2.0
-            self.horizontalHeader().setResizeMode(QHeaderView.Fixed)
-
-        parent.viewport().stackUnder(self)
-
-        self.setSelectionModel(parent.selectionModel())
-        for col in range(1, parent.model().columnCount()):
-            self.setColumnHidden(col, True)
-
-        self.setColumnWidth(0, parent.columnWidth(0))
-        self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
-        self.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
-        self.show()
-
-        self.setVerticalScrollMode(1)
-
-    def update_geometry(self):
-        """Update the frozen column size when an update occurs
-        in its parent table"""
-        self.setGeometry(self.parent.verticalHeader().width() +
-                         self.parent.frameWidth(),
-                         self.parent.frameWidth(),
-                         self.parent.columnWidth(0),
-                         self.parent.viewport().height() +
-                         self.parent.horizontalHeader().height())
+class DataFrameView(QTableView):
+    """
+    Data Frame view class.
 
+    Signals
+    -------
+    sig_option_changed(): Raised after a sort by column.
+    sig_sort_by_column(): Raised after more columns are fetched.
+    sig_fetch_more_rows(): Raised after more rows are fetched.
+    """
+    sig_sort_by_column = Signal()
+    sig_fetch_more_columns = Signal()
+    sig_fetch_more_rows = Signal()
 
-class DataFrameView(QTableView):
-    """Data Frame view class"""
-    def __init__(self, parent, model):
+    def __init__(self, parent, model, header, hscroll, vscroll):
+        """Constructor."""
         QTableView.__init__(self, parent)
         self.setModel(model)
-
-        self.frozen_table_view = FrozenTableView(self)
-        self.frozen_table_view.update_geometry()
-
+        self.setHorizontalScrollBar(hscroll)
+        self.setVerticalScrollBar(vscroll)
         self.setHorizontalScrollMode(1)
         self.setVerticalScrollMode(1)
 
-        self.horizontalHeader().sectionResized.connect(self.update_section_width)
-        self.verticalHeader().sectionResized.connect(self.update_section_height)
-
-        self.frozen_table_view.verticalScrollBar().valueChanged.connect(
-            self.verticalScrollBar().setValue)
-
         self.sort_old = [None]
-        self.header_class = self.horizontalHeader()
+        self.header_class = header
         self.header_class.sectionClicked.connect(self.sortByColumn)
         self.menu = self.setup_menu()
         config_shortcut(self.copy, context='variable_explorer', name='copy',
@@ -496,73 +476,22 @@ def __init__(self, parent, model):
                         lambda val: self.load_more_data(val, columns=True))
         self.verticalScrollBar().valueChanged.connect(
                         lambda val: self.load_more_data(val, rows=True))
-        self.verticalScrollBar().valueChanged.connect(
-            self.frozen_table_view.verticalScrollBar().setValue)
-    
-    def update_section_width(self, logical_index, old_size, new_size):
-        """Update the horizontal width of the frozen column when a
-        change takes place in the first column of the table"""
-        if logical_index == 0:
-            self.frozen_table_view.setColumnWidth(0, new_size)
-            self.frozen_table_view.update_geometry()
-
-    def update_section_height(self, logical_index, old_size, new_size):
-        """Update the vertical width of the frozen column when a
-        change takes place on any of the rows"""
-        self.frozen_table_view.setRowHeight(logical_index, new_size)
-
-    def resizeEvent(self, event):
-        """Update the frozen column dimensions.
-
-        Updates takes place when the enclosing window of this 
-        table reports a dimension change
-        """
-        QTableView.resizeEvent(self, event)
-        self.frozen_table_view.update_geometry()
-
-    def moveCursor(self, cursor_action, modifiers):
-        """Update the table position.
-
-        Updates the position along with the frozen column
-        when the cursor (selector) changes its position
-        """
-        current = QTableView.moveCursor(self, cursor_action, modifiers)
-        
-        col_width = (self.frozen_table_view.columnWidth(0) + 
-                     self.frozen_table_view.columnWidth(1))
-        topleft_x = self.visualRect(current).topLeft().x()
-
-        overflow = self.MoveLeft and current.column() > 1
-        overflow = overflow and topleft_x < col_width
-
-        if cursor_action == overflow:
-            new_value = (self.horizontalScrollBar().value() + 
-                         topleft_x - col_width)
-            self.horizontalScrollBar().setValue(new_value)
-        return current
-
-    def scrollTo(self, index, hint):
-        """Scroll the table.
-
-        It is necessary to ensure that the item at index is visible.
-        The view will try to position the item according to the
-        given hint. This method does not takes effect only if
-        the frozen column is scrolled.
-        """
-        if index.column() > 1:
-            QTableView.scrollTo(self, index, hint)
 
     def load_more_data(self, value, rows=False, columns=False):
+        """Load more rows and columns to display."""
         if rows and value == self.verticalScrollBar().maximum():
             self.model().fetch_more(rows=rows)
+            self.sig_fetch_more_rows.emit()
         if columns and value == self.horizontalScrollBar().maximum():
             self.model().fetch_more(columns=columns)
+            self.sig_fetch_more_columns.emit()
 
     def sortByColumn(self, index):
-        """ Implement a Column sort """
+        """Implement a column sort."""
         if self.sort_old == [None]:
             self.header_class.setSortIndicatorShown(True)
         sort_order = self.header_class.sortIndicatorOrder()
+        self.sig_sort_by_column.emit()
         if not self.model().sort(index, sort_order):
             if len(self.sort_old) != 2:
                 self.header_class.setSortIndicatorShown(False)
@@ -573,12 +502,12 @@ def sortByColumn(self, index):
         self.sort_old = [index, self.header_class.sortIndicatorOrder()]
 
     def contextMenuEvent(self, event):
-        """Reimplement Qt method"""
+        """Reimplement Qt method."""
         self.menu.popup(event.globalPos())
         event.accept()
 
     def setup_menu(self):
-        """Setup context menu"""
+        """Setup context menu."""
         copy_action = create_action(self, _('Copy'),
                                     shortcut=keybinding('Copy'),
                                     icon=ima.icon('editcopy'),
@@ -589,11 +518,7 @@ def setup_menu(self):
                      (_("To str"), to_text_string))
         types_in_menu = [copy_action]
         for name, func in functions:
-            # QAction.triggered works differently for PySide and PyQt
-            if not API == 'pyside':
-                slot = lambda _checked, func=func: self.change_type(func)
-            else:
-                slot = lambda func=func: self.change_type(func)
+            slot = lambda func=func: self.change_type(func)
             types_in_menu += [create_action(self, name,
                                             triggered=slot,
                                             context=Qt.WidgetShortcut)]
@@ -602,7 +527,7 @@ def setup_menu(self):
         return menu
 
     def change_type(self, func):
-        """A function that changes types of cells"""
+        """A function that changes types of cells."""
         model = self.model()
         index_list = self.selectedIndexes()
         [model.setData(i, '', change_type=func) for i in index_list]
@@ -615,32 +540,241 @@ def copy(self):
         (row_min, row_max,
          col_min, col_max) = get_idx_rect(self.selectedIndexes())
         index = header = False
-        if col_min == 0:
-            col_min = 1
-            index = True
         df = self.model().df
-        if col_max == 0:  # To copy indices
-            contents = '\n'.join(map(str, df.index.tolist()[slice(row_min,
-                                                            row_max+1)]))
-        else:  # To copy DataFrame
-            if (col_min == 0 or col_min == 1) and (df.shape[1] == col_max):
-                header = True
-            obj = df.iloc[slice(row_min, row_max+1), slice(col_min-1, col_max)]
-            output = io.StringIO()
-            obj.to_csv(output, sep='\t', index=index, header=header)
-            if not PY2:
-                contents = output.getvalue()
-            else:
-                contents = output.getvalue().decode('utf-8')
-            output.close()
+        obj = df.iloc[slice(row_min, row_max + 1),
+                      slice(col_min, col_max + 1)]
+        output = io.StringIO()
+        obj.to_csv(output, sep='\t', index=index, header=header)
+        if not PY2:
+            contents = output.getvalue()
+        else:
+            contents = output.getvalue().decode('utf-8')
+        output.close()
         clipboard = QApplication.clipboard()
         clipboard.setText(contents)
 
 
+class DataFrameHeaderModel(QAbstractTableModel):
+    """
+    This class is the model for the header or index of the DataFrameEditor.
+
+    Taken from gtabview project (Header4ExtModel).
+    For more information please see:
+    https://github.com/wavexx/gtabview/blob/master/gtabview/viewer.py
+    """
+
+    COLUMN_INDEX = -1  # Makes reference to the index of the table.
+
+    def __init__(self, model, axis, palette):
+        """
+        Header constructor.
+
+        The 'model' is the QAbstractTableModel of the dataframe, the 'axis' is
+        to acknowledge if is for the header (horizontal - 0) or for the
+        index (vertical - 1) and the palette is the set of colors to use.
+        """
+        super(DataFrameHeaderModel, self).__init__()
+        self.model = model
+        self.axis = axis
+        self._palette = palette
+        if self.axis == 0:
+            self.total_cols = self.model.shape[1]
+            self._shape = (self.model.header_shape[0], self.model.shape[1])
+            if self.total_cols > LARGE_COLS:
+                self.cols_loaded = COLS_TO_LOAD
+            else:
+                self.cols_loaded = self.total_cols
+        else:
+            self.total_rows = self.model.shape[0]
+            self._shape = (self.model.shape[0], self.model.header_shape[1])
+            if self.total_rows > LARGE_NROWS:
+                self.rows_loaded = ROWS_TO_LOAD
+            else:
+                self.rows_loaded = self.total_rows
+
+    def rowCount(self, index=None):
+        """Get number of rows in the header."""
+        if self.axis == 0:
+            return max(1, self._shape[0])
+        else:
+            if self.total_rows <= self.rows_loaded:
+                return self.total_rows
+            else:
+                return self.rows_loaded
+
+    def columnCount(self, index=QModelIndex()):
+        """DataFrame column number"""
+        if self.axis == 0:
+            if self.total_cols <= self.cols_loaded:
+                return self.total_cols
+            else:
+                return self.cols_loaded
+        else:
+            return max(1, self._shape[1])
+
+    def fetch_more(self, rows=False, columns=False):
+        """Get more columns or rows (based on axis)."""
+        if  self.axis == 1 and self.total_rows > self.rows_loaded:
+            reminder = self.total_rows - self.rows_loaded
+            items_to_fetch = min(reminder, ROWS_TO_LOAD)
+            self.beginInsertRows(QModelIndex(), self.rows_loaded,
+                                 self.rows_loaded + items_to_fetch - 1)
+            self.rows_loaded += items_to_fetch
+            self.endInsertRows()
+        if self.axis == 0 and self.total_cols > self.cols_loaded:
+            reminder = self.total_cols - self.cols_loaded
+            items_to_fetch = min(reminder, COLS_TO_LOAD)
+            self.beginInsertColumns(QModelIndex(), self.cols_loaded,
+                                    self.cols_loaded + items_to_fetch - 1)
+            self.cols_loaded += items_to_fetch
+            self.endInsertColumns()
+
+    def sort(self, column, order=Qt.AscendingOrder):
+        """Overriding sort method."""
+        ascending = order == Qt.AscendingOrder
+        self.model.sort(self.COLUMN_INDEX, order=ascending)
+        return True
+
+    def headerData(self, section, orientation, role):
+        """Get the information to put in the header."""
+        if role == Qt.TextAlignmentRole:
+            if orientation == Qt.Horizontal:
+                return Qt.AlignCenter | Qt.AlignBottom
+            else:
+                return Qt.AlignRight | Qt.AlignVCenter
+        if role != Qt.DisplayRole and role != Qt.ToolTipRole:
+            return None
+        if self.axis == 1 and self._shape[1] <= 1:
+            return None
+        orient_axis = 0 if orientation == Qt.Horizontal else 1
+        if self.model.header_shape[orient_axis] > 1:
+            header = section
+        else:
+            header = self.model.header(self.axis, section)
+            if isinstance(header, TEXT_TYPES):
+                # Get the proper encoding of the text in the header.
+                # Fixes Issue 3896
+                if not PY2:
+                    try:
+                        header = header.encode('utf-8')
+                        coding = 'utf-8-sig'
+                    except UnicodeEncodeError:
+                        coding = encoding.get_coding(header)
+                else:
+                    coding = encoding.get_coding(header)
+                return to_text_string(header, encoding=coding)
+            header = to_text_string(header)
+        return header
+
+    def data(self, index, role):
+        """
+        Get the data for the header.
+
+        This is used when a header has levels.
+        """
+        if not index.isValid() or \
+           index.row() >= self._shape[0] or \
+           index.column() >= self._shape[1]:
+            return None
+        row, col = ((index.row(), index.column()) if self.axis == 0
+                    else (index.column(), index.row()))
+        if role == Qt.BackgroundRole:
+            prev = self.model.header(self.axis, col - 1, row) if col else None
+            cur = self.model.header(self.axis, col, row)
+            return self._palette.midlight() if prev != cur else None
+        if role != Qt.DisplayRole:
+            return None
+        if self.axis == 0 and self._shape[0] <= 1:
+            return None
+        return to_text_string(self.model.header(self.axis, col, row))
+
+
+class DataFrameLevelModel(QAbstractTableModel):
+    """
+    Data Frame level class.
+
+    This class is used to represent index levels in the DataFrameEditor. When
+    using MultiIndex, this model creates labels for the index/header as Index i
+    for each section in the index/header
+
+    Based on the gtabview project (Level4ExtModel).
+    For more information please see:
+    https://github.com/wavexx/gtabview/blob/master/gtabview/viewer.py
+    """
+
+    def __init__(self, model, palette, font):
+        super(DataFrameLevelModel, self).__init__()
+        self.model = model
+        self._background = palette.dark().color()
+        if self._background.lightness() > 127:
+            self._foreground = palette.text()
+        else:
+            self._foreground = palette.highlightedText()
+        self._palette = palette
+        font.setBold(True)
+        self._font = font
+
+    def rowCount(self, index=None):
+        """Get number of rows (number of levels for the header)."""
+        return max(1, self.model.header_shape[0])
+
+    def columnCount(self, index=None):
+        """Get the number of columns (number of levels for the index)."""
+        return max(1, self.model.header_shape[1])
+
+    def headerData(self, section, orientation, role):
+        """
+        Get the text to put in the header of the levels of the indexes.
+
+        By default it returns 'Index i', where i is the section in the index
+        """
+        if role == Qt.TextAlignmentRole:
+            if orientation == Qt.Horizontal:
+                return Qt.AlignCenter | Qt.AlignBottom
+            else:
+                return Qt.AlignRight | Qt.AlignVCenter
+        if role != Qt.DisplayRole and role != Qt.ToolTipRole:
+            return None
+        if self.model.header_shape[0] <= 1 and orientation == Qt.Horizontal:
+            if self.model.name(1,section):
+                return self.model.name(1,section)
+            return _('Index')
+        elif self.model.header_shape[0] <= 1:
+            return None
+        elif self.model.header_shape[1] <= 1 and orientation == Qt.Vertical:
+            return None
+        return _('Index') + ' ' + to_text_string(section)
+
+    def data(self, index, role):
+        """Get the information of the levels."""
+        if not index.isValid():
+            return None
+        if role == Qt.FontRole:
+            return self._font
+        label = ''
+        if index.column() == self.model.header_shape[1] - 1:
+            label = str(self.model.name(0, index.row()))
+        elif index.row() == self.model.header_shape[0] - 1:
+            label = str(self.model.name(1, index.column()))
+        if role == Qt.DisplayRole and label:
+            return label
+        elif role == Qt.ForegroundRole:
+            return self._foreground
+        elif role == Qt.BackgroundRole:
+            return self._background
+        elif role == Qt.BackgroundRole:
+            return self._palette.window()
+        return None
+
+
 class DataFrameEditor(QDialog):
     """
     Dialog for displaying and editing DataFrame and related objects.
 
+    Based on the gtabview project (ExtTableView).
+    For more information please see:
+    https://github.com/wavexx/gtabview/blob/master/gtabview/viewer.py
+
     Signals
     -------
     sig_option_changed(str, object): Raised if an option is changed.
@@ -664,7 +798,12 @@ def setup_and_check(self, data, title=''):
         return False if data is not supported, True otherwise.
         Supported types for data are DataFrame, Series and Index.
         """
+        self._selection_rec = False
+        self._model = None
+
         self.layout = QGridLayout()
+        self.layout.setSpacing(0)
+        self.layout.setContentsMargins(0, 0, 0, 0)
         self.setLayout(self.layout)
         self.setWindowIcon(ima.icon('arredit'))
         if title:
@@ -680,10 +819,34 @@ def setup_and_check(self, data, title=''):
         self.setWindowTitle(title)
         self.resize(600, 500)
 
+        self.hscroll = QScrollBar(Qt.Horizontal)
+        self.vscroll = QScrollBar(Qt.Vertical)
+
+        # Create the view for the level
+        self.create_table_level()
+
+        # Create the view for the horizontal header
+        self.create_table_header()
+
+        # Create the view for the vertical index
+        self.create_table_index()
+
+        # Create the model and view of the data
         self.dataModel = DataFrameModel(data, parent=self)
-        self.dataTable = DataFrameView(self, self.dataModel)
+        self.create_data_table()
+
+        self.layout.addWidget(self.hscroll, 2, 0, 1, 2)
+        self.layout.addWidget(self.vscroll, 0, 2, 2, 1)
+
+        # autosize columns on-demand
+        self._autosized_cols = set()
+        self._max_autosize_ms = None
+        self.dataTable.installEventFilter(self)
+
+        avg_width = self.fontMetrics().averageCharWidth()
+        self.min_trunc = avg_width * 8  # Minimum size for columns
+        self.max_width = avg_width * 64  # Maximum size for columns
 
-        self.layout.addWidget(self.dataTable)
         self.setLayout(self.layout)
         self.setMinimumSize(400, 300)
         # Make the dialog act as a window
@@ -716,11 +879,274 @@ def setup_and_check(self, data, title=''):
         bbox.accepted.connect(self.accept)
         bbox.rejected.connect(self.reject)
         btn_layout.addWidget(bbox)
-
-        self.layout.addLayout(btn_layout, 2, 0)
+        btn_layout.setContentsMargins(4, 4, 4, 4)
+        self.layout.addLayout(btn_layout, 4, 0, 1, 2)
+        self.setModel(self.dataModel)
+        self.resizeColumnsToContents()
 
         return True
 
+    def create_table_level(self):
+        """Create the QTableView that will hold the level model."""
+        self.table_level = QTableView()
+        self.table_level.setEditTriggers(QTableWidget.NoEditTriggers)
+        self.table_level.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
+        self.table_level.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
+        self.table_level.setFrameStyle(QFrame.Plain)
+        self.table_level.horizontalHeader().sectionResized.connect(
+                                                        self._index_resized)
+        self.table_level.verticalHeader().sectionResized.connect(
+                                                        self._header_resized)
+        self.table_level.setItemDelegate(QItemDelegate())
+        self.layout.addWidget(self.table_level, 0, 0)
+        self.table_level.setContentsMargins(0, 0, 0, 0)
+        self.table_level.horizontalHeader().sectionClicked.connect(
+                                                            self.sortByIndex)
+
+    def create_table_header(self):
+        """Create the QTableView that will hold the header model."""
+        self.table_header = QTableView()
+        self.table_header.verticalHeader().hide()
+        self.table_header.setEditTriggers(QTableWidget.NoEditTriggers)
+        self.table_header.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
+        self.table_header.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
+        self.table_header.setHorizontalScrollMode(QTableView.ScrollPerPixel)
+        self.table_header.setHorizontalScrollBar(self.hscroll)
+        self.table_header.setFrameStyle(QFrame.Plain)
+        self.table_header.horizontalHeader().sectionResized.connect(
+                                                        self._column_resized)
+        self.table_header.setItemDelegate(QItemDelegate())
+        self.layout.addWidget(self.table_header, 0, 1)
+
+    def create_table_index(self):
+        """Create the QTableView that will hold the index model."""
+        self.table_index = QTableView()
+        self.table_index.horizontalHeader().hide()
+        self.table_index.setEditTriggers(QTableWidget.NoEditTriggers)
+        self.table_index.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
+        self.table_index.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
+        self.table_index.setVerticalScrollMode(QTableView.ScrollPerPixel)
+        self.table_index.setVerticalScrollBar(self.vscroll)
+        self.table_index.setFrameStyle(QFrame.Plain)
+        self.table_index.verticalHeader().sectionResized.connect(
+                                                            self._row_resized)
+        self.table_index.setItemDelegate(QItemDelegate())
+        self.layout.addWidget(self.table_index, 1, 0)
+        self.table_index.setContentsMargins(0, 0, 0, 0)
+
+    def create_data_table(self):
+        """Create the QTableView that will hold the data model."""
+        self.dataTable = DataFrameView(self, self.dataModel,
+                                       self.table_header.horizontalHeader(),
+                                       self.hscroll, self.vscroll)
+        self.dataTable.verticalHeader().hide()
+        self.dataTable.horizontalHeader().hide()
+        self.dataTable.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
+        self.dataTable.setVerticalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
+        self.dataTable.setHorizontalScrollMode(QTableView.ScrollPerPixel)
+        self.dataTable.setVerticalScrollMode(QTableView.ScrollPerPixel)
+        self.dataTable.setFrameStyle(QFrame.Plain)
+        self.dataTable.setItemDelegate(QItemDelegate())
+        self.layout.addWidget(self.dataTable, 1, 1)
+        self.setFocusProxy(self.dataTable)
+        self.dataTable.sig_sort_by_column.connect(self._sort_update)
+        self.dataTable.sig_fetch_more_columns.connect(self._fetch_more_columns)
+        self.dataTable.sig_fetch_more_rows.connect(self._fetch_more_rows)
+
+    def sortByIndex(self, index):
+        """Implement a Index sort."""
+        self.table_level.horizontalHeader().setSortIndicatorShown(True)
+        sort_order = self.table_level.horizontalHeader().sortIndicatorOrder()
+        self.table_index.model().sort(index, sort_order)
+        self._sort_update()
+
+    def model(self):
+        """Get the model of the dataframe."""
+        return self._model
+
+    def _column_resized(self, col, old_width, new_width):
+        """Update the column width."""
+        self.dataTable.setColumnWidth(col, new_width)
+        self._update_layout()
+
+    def _row_resized(self, row, old_height, new_height):
+        """Update the row height."""
+        self.dataTable.setRowHeight(row, new_height)
+        self._update_layout()
+
+    def _index_resized(self, col, old_width, new_width):
+        """Resize the corresponding column of the index section selected."""
+        self.table_index.setColumnWidth(col, new_width)
+        self._update_layout()
+
+    def _header_resized(self, row, old_height, new_height):
+        """Resize the corresponding row of the header section selected."""
+        self.table_header.setRowHeight(row, new_height)
+        self._update_layout()
+
+    def _update_layout(self):
+        """Set the width and height of the QTableViews and hide rows."""
+        h_width = max(self.table_level.verticalHeader().sizeHint().width(),
+                      self.table_index.verticalHeader().sizeHint().width())
+        self.table_level.verticalHeader().setFixedWidth(h_width)
+        self.table_index.verticalHeader().setFixedWidth(h_width)
+
+        last_row = self._model.header_shape[0] - 1
+        if last_row < 0:
+            hdr_height = self.table_level.horizontalHeader().height()
+        else:
+            hdr_height = self.table_level.rowViewportPosition(last_row) + \
+                         self.table_level.rowHeight(last_row) + \
+                         self.table_level.horizontalHeader().height()
+            # Check if the header shape has only one row (which display the
+            # same info than the horizontal header).
+            if last_row == 0:
+                self.table_level.setRowHidden(0, True)
+                self.table_header.setRowHidden(0, True)
+        self.table_header.setFixedHeight(hdr_height)
+        self.table_level.setFixedHeight(hdr_height)
+
+        last_col = self._model.header_shape[1] - 1
+        if last_col < 0:
+            idx_width = self.table_level.verticalHeader().width()
+        else:
+            idx_width = self.table_level.columnViewportPosition(last_col) + \
+                        self.table_level.columnWidth(last_col) + \
+                        self.table_level.verticalHeader().width()
+        self.table_index.setFixedWidth(idx_width)
+        self.table_level.setFixedWidth(idx_width)
+        self._resizeVisibleColumnsToContents()
+
+    def _reset_model(self, table, model):
+        """Set the model in the given table."""
+        old_sel_model = table.selectionModel()
+        table.setModel(model)
+        if old_sel_model:
+            del old_sel_model
+
+    def setAutosizeLimit(self, limit_ms):
+        """Set maximum size for columns."""
+        self._max_autosize_ms = limit_ms
+
+    def setModel(self, model, relayout=True):
+        """Set the model for the data, header/index and level views."""
+        self._model = model
+        sel_model = self.dataTable.selectionModel()
+        sel_model.currentColumnChanged.connect(
+                self._resizeCurrentColumnToContents)
+
+        # Asociate the models (level, vertical index and horizontal header)
+        # with its corresponding view.
+        self._reset_model(self.table_level, DataFrameLevelModel(model,
+                                                                self.palette(),
+                                                                self.font()))
+        self._reset_model(self.table_header, DataFrameHeaderModel(
+                                                            model,
+                                                            0,
+                                                            self.palette()))
+        self._reset_model(self.table_index, DataFrameHeaderModel(
+                                                            model,
+                                                            1,
+                                                            self.palette()))
+
+        # Needs to be called after setting all table models
+        if relayout:
+            self._update_layout()
+
+    def setCurrentIndex(self, y, x):
+        """Set current selection."""
+        self.dataTable.selectionModel().setCurrentIndex(
+            self.dataTable.model().index(y, x),
+            QItemSelectionModel.ClearAndSelect)
+
+    def _sizeHintForColumn(self, table, col, limit_ms=None):
+        """Get the size hint for a given column in a table."""
+        max_row = table.model().rowCount()
+        lm_start = time.clock()
+        lm_row = 64 if limit_ms else max_row
+        max_width = 0
+        for row in range(max_row):
+            v = table.sizeHintForIndex(table.model().index(row, col))
+            max_width = max(max_width, v.width())
+            if row > lm_row:
+                lm_now = time.clock()
+                lm_elapsed = (lm_now - lm_start) * 1000
+                if lm_elapsed >= limit_ms:
+                    break
+                lm_row = int((row / lm_elapsed) * limit_ms)
+        return max_width
+
+    def _resizeColumnToContents(self, header, data, col, limit_ms):
+        """Resize a column by its contents."""
+        hdr_width = self._sizeHintForColumn(header, col, limit_ms)
+        data_width = self._sizeHintForColumn(data, col, limit_ms)
+        if data_width > hdr_width:
+            width = min(self.max_width, data_width)
+        elif hdr_width > data_width * 2:
+            width = max(min(hdr_width, self.min_trunc), min(self.max_width,
+                        data_width))
+        else:
+            width = min(self.max_width, hdr_width)
+        header.setColumnWidth(col, width)
+
+    def _resizeColumnsToContents(self, header, data, limit_ms):
+        """Resize all the colummns to its contents."""
+        max_col = data.model().columnCount()
+        if limit_ms is None:
+            max_col_ms = None
+        else:
+            max_col_ms = limit_ms / max(1, max_col)
+        for col in range(max_col):
+            self._resizeColumnToContents(header, data, col, max_col_ms)
+
+    def eventFilter(self, obj, event):
+        """Override eventFilter to catch resize event."""
+        if obj == self.dataTable and event.type() == QEvent.Resize:
+            self._resizeVisibleColumnsToContents()
+        return False
+
+    def _resizeVisibleColumnsToContents(self):
+        """Resize the columns that are in the view."""
+        index_column = self.dataTable.rect().topLeft().x()
+        start = col = self.dataTable.columnAt(index_column)
+        width = self._model.shape[1]
+        end = self.dataTable.columnAt(self.dataTable.rect().bottomRight().x())
+        end = width if end == -1 else end + 1
+        if self._max_autosize_ms is None:
+            max_col_ms = None
+        else:
+            max_col_ms = self._max_autosize_ms / max(1, end - start)
+        while col < end:
+            resized = False
+            if col not in self._autosized_cols:
+                self._autosized_cols.add(col)
+                resized = True
+                self._resizeColumnToContents(self.table_header, self.dataTable,
+                                             col, max_col_ms)
+            col += 1
+            if resized:
+                # As we resize columns, the boundary will change
+                index_column = self.dataTable.rect().bottomRight().x()
+                end = self.dataTable.columnAt(index_column)
+                end = width if end == -1 else end + 1
+                if max_col_ms is not None:
+                    max_col_ms = self._max_autosize_ms / max(1, end - start)
+
+    def _resizeCurrentColumnToContents(self, new_index, old_index):
+        """Resize the current column to its contents."""
+        if new_index.column() not in self._autosized_cols:
+            # Ensure the requested column is fully into view after resizing
+            self._resizeVisibleColumnsToContents()
+            self.dataTable.scrollTo(new_index)
+
+    def resizeColumnsToContents(self):
+        """Resize the columns to its contents."""
+        self._autosized_cols = set()
+        self._resizeColumnsToContents(self.table_level,
+                                      self.table_index, self._max_autosize_ms)
+        self._update_layout()
+        self.table_level.resizeColumnsToContents()
+
     def change_bgcolor_enable(self, state):
         """
         This is implementet so column min/max is only active when bgcolor is
@@ -764,11 +1190,38 @@ def get_value(self):
         else:
             return df
 
+    def _update_header_size(self):
+        """Update the column width of the header."""
+        column_count = self.table_header.model().columnCount()
+        for index in range(0, column_count):
+            if index < column_count:
+                column_width = self.dataTable.columnWidth(index)
+                self.table_header.setColumnWidth(index, column_width)
+            else:
+                break
+
+    def _sort_update(self):
+        """
+        Update the model for all the QTableView objects.
+
+        Uses the model of the dataTable as the base.
+        """
+        self.setModel(self.dataTable.model())
+
+    def _fetch_more_columns(self):
+        """Fetch more data for the header (columns)."""
+        self.table_header.model().fetch_more()
+
+    def _fetch_more_rows(self):
+        """Fetch more data for the index (rows)."""
+        self.table_index.model().fetch_more()
+
     def resize_to_contents(self):
         QApplication.setOverrideCursor(QCursor(Qt.WaitCursor))
         self.dataTable.resizeColumnsToContents()
         self.dataModel.fetch_more(columns=True)
         self.dataTable.resizeColumnsToContents()
+        self._update_header_size()
         QApplication.restoreOverrideCursor()
 
 


Test patch:
diff --git a/spyder/widgets/variableexplorer/tests/test_dataframeeditor.py b/spyder/widgets/variableexplorer/tests/test_dataframeeditor.py
--- a/spyder/widgets/variableexplorer/tests/test_dataframeeditor.py
+++ b/spyder/widgets/variableexplorer/tests/test_dataframeeditor.py
@@ -31,7 +31,6 @@
 from spyder.widgets.variableexplorer import dataframeeditor
 from spyder.widgets.variableexplorer.dataframeeditor import (
     DataFrameEditor, DataFrameModel)
-from spyder.py3compat import PY2
 
 FILES_PATH = os.path.dirname(os.path.realpath(__file__))
 
@@ -49,6 +48,11 @@ def data(dfm, i, j):
 def bgcolor(dfm, i, j):
     return dfm.get_bgcolor(dfm.createIndex(i, j))
 
+
+def data_header(dfh, i, j, role=Qt.DisplayRole):
+    return dfh.data(dfh.createIndex(i, j), role)
+
+
 def generate_pandas_indexes():
     """ Creates a dictionnary of many possible pandas indexes """
     return {
@@ -63,40 +67,130 @@ def generate_pandas_indexes():
                                              categories=['a', 'b', 'c']),
         }
 
+
 # --- Tests
 # -----------------------------------------------------------------------------
 
+
+def test_dataframe_simpleindex():
+    """Test to validate proper creation and handling of a simpleindex."""
+    df = DataFrame(numpy.random.randn(6, 6))
+    editor = DataFrameEditor(None)
+    editor.setup_and_check(df)
+    header = editor.table_header.model()
+    assert header.headerData(0, Qt.Horizontal,
+                             Qt.DisplayRole) == "0"
+    assert header.headerData(1, Qt.Horizontal,
+                             Qt.DisplayRole) == "1"
+    assert header.headerData(5, Qt.Horizontal,
+                             Qt.DisplayRole) == "5"
+
+
+def test_dataframe_simpleindex_custom_columns():
+    """Test to validate proper creation and handling of custom simpleindex."""
+    df = DataFrame(numpy.random.randn(10, 5),
+                   columns=['a', 'b', 'c', 'd', 'e'])
+    editor = DataFrameEditor(None)
+    editor.setup_and_check(df)
+    header = editor.table_header.model()
+    assert header.headerData(0, Qt.Horizontal,
+                             Qt.DisplayRole) == "a"
+    assert header.headerData(1, Qt.Horizontal,
+                             Qt.DisplayRole) == "b"
+    assert header.headerData(4, Qt.Horizontal,
+                             Qt.DisplayRole) == "e"
+
+
+def test_dataframe_multiindex():
+    """Test to validate proper creation and handling of a multiindex."""
+    arrays = [numpy.array(['bar', 'bar', 'baz', 'baz',
+                           'foo', 'foo', 'qux', 'qux']),
+              numpy.array(['one', 'two', 'one', 'two',
+                           'one', 'two', 'one', 'two'])]
+    tuples = list(zip(*arrays))
+    index = MultiIndex.from_tuples(tuples, names=['first', 'second'])
+    df = DataFrame(numpy.random.randn(6, 6), index=index[:6],
+                   columns=index[:6])
+    editor = DataFrameEditor(None)
+    editor.setup_and_check(df)
+    header = editor.table_header.model()
+    assert header.headerData(0, Qt.Horizontal,
+                             Qt.DisplayRole) == 0
+    assert data_header(header, 0, 0) == 'bar'
+    assert data_header(header, 1, 0) == 'one'
+    assert data_header(header, 0, 1) == 'bar'
+    assert data_header(header, 1, 1) == 'two'
+    assert data_header(header, 0, 2) == 'baz'
+    assert data_header(header, 1, 2) == 'one'
+    assert data_header(header, 0, 3) == 'baz'
+    assert data_header(header, 1, 3) == 'two'
+    assert data_header(header, 0, 4) == 'foo'
+    assert data_header(header, 1, 4) == 'one'
+    assert data_header(header, 0, 5) == 'foo'
+    assert data_header(header, 1, 5) == 'two'
+
+
+def test_header_bom():
+    """Test for BOM data in the headers."""
+    df = read_csv(os.path.join(FILES_PATH, 'issue_2514.csv'))
+    editor = DataFrameEditor(None)
+    editor.setup_and_check(df)
+    header = editor.table_header.model()
+    assert header.headerData(0, Qt.Horizontal,
+                             Qt.DisplayRole) == "Date (MMM-YY)"
+
+
+@pytest.mark.skipif(is_module_installed('pandas', '<0.19'),
+                    reason="It doesn't work for Pandas 0.19-")
+def test_header_encoding():
+    """Test for header encoding handling."""
+    df = read_csv(os.path.join(FILES_PATH, 'issue_3896.csv'))
+    editor = DataFrameEditor(None)
+    editor.setup_and_check(df)
+    header = editor.table_header.model()
+    assert header.headerData(0, Qt.Horizontal,
+                             Qt.DisplayRole) == "Unnamed: 0"
+    assert header.headerData(1, Qt.Horizontal,
+                             Qt.DisplayRole) == "Unieke_Idcode"
+    assert header.headerData(2, Qt.Horizontal,
+                             Qt.DisplayRole) == "a"
+    assert header.headerData(3, Qt.Horizontal,
+                             Qt.DisplayRole) == "b"
+    assert header.headerData(4, Qt.Horizontal,
+                             Qt.DisplayRole) == "c"
+    assert header.headerData(5, Qt.Horizontal,
+                             Qt.DisplayRole) == "d"
+
+
 def test_dataframemodel_basic():
     df = DataFrame({'colA': [1, 3], 'colB': ['c', 'a']})
     dfm = DataFrameModel(df)
     assert dfm.rowCount() == 2
-    assert dfm.columnCount() == 3
-    assert data(dfm, 0, 0) == '0'
-    assert data(dfm, 0, 1) == '1'
-    assert data(dfm, 0, 2) == 'c'
-    assert data(dfm, 1, 0) == '1'
-    assert data(dfm, 1, 1) == '3'
-    assert data(dfm, 1, 2) == 'a'
+    assert dfm.columnCount() == 2
+    assert data(dfm, 0, 0) == '1'
+    assert data(dfm, 0, 1) == 'c'
+    assert data(dfm, 1, 0) == '3'
+    assert data(dfm, 1, 1) == 'a'
 
 def test_dataframemodel_sort():
+    """Validate the data in the model."""
     df = DataFrame({'colA': [1, 3], 'colB': ['c', 'a']})
     dfm = DataFrameModel(df)
-    dfm.sort(2)
-    assert data(dfm, 0, 0) == '1'
-    assert data(dfm, 0, 1) == '3'
-    assert data(dfm, 0, 2) == 'a'
-    assert data(dfm, 1, 0) == '0'
-    assert data(dfm, 1, 1) == '1'
-    assert data(dfm, 1, 2) == 'c'
+    dfm.sort(1)
+    assert data(dfm, 0, 0) == '3'
+    assert data(dfm, 1, 0) == '1'
+    assert data(dfm, 0, 1) == 'a'
+    assert data(dfm, 1, 1) == 'c'
 
 def test_dataframemodel_sort_is_stable():   # cf. issue 3010
+    """Validate the sort function."""
     df = DataFrame([[2,14], [2,13], [2,16], [1,3], [2,9], [1,15], [1,17],
                     [2,2], [2,10], [1,6], [2,5], [2,8], [1,11], [1,1],
                     [1,12], [1,4], [2,7]])
     dfm = DataFrameModel(df)
-    dfm.sort(2)
     dfm.sort(1)
-    col2 = [data(dfm, i, 2) for i in range(len(df))]
+    dfm.sort(0)
+    col2 = [data(dfm, i, 1) for i in range(len(df))]
     assert col2 == [str(x) for x in [1, 3, 4, 6, 11, 12, 15, 17,
                                      2, 5, 7, 8, 9, 10, 13, 14, 16]]
 
@@ -130,12 +224,12 @@ def test_dataframemodel_get_bgcolor_with_numbers():
     s = dataframeeditor.BACKGROUND_NUMBER_SATURATION
     v = dataframeeditor.BACKGROUND_NUMBER_VALUE
     a = dataframeeditor.BACKGROUND_NUMBER_ALPHA
+    assert colorclose(bgcolor(dfm, 0, 0), (h0 + dh,         s, v, a))
+    assert colorclose(bgcolor(dfm, 1, 0), (h0 + 1 / 2 * dh, s, v, a))
+    assert colorclose(bgcolor(dfm, 2, 0), (h0,              s, v, a))
     assert colorclose(bgcolor(dfm, 0, 1), (h0 + dh,         s, v, a))
-    assert colorclose(bgcolor(dfm, 1, 1), (h0 + 1 / 2 * dh, s, v, a))
+    assert colorclose(bgcolor(dfm, 1, 1), (h0 + 2 / 3 * dh, s, v, a))
     assert colorclose(bgcolor(dfm, 2, 1), (h0,              s, v, a))
-    assert colorclose(bgcolor(dfm, 0, 2), (h0 + dh,         s, v, a))
-    assert colorclose(bgcolor(dfm, 1, 2), (h0 + 2 / 3 * dh, s, v, a))
-    assert colorclose(bgcolor(dfm, 2, 2), (h0,              s, v, a))
 
 def test_dataframemodel_get_bgcolor_with_numbers_using_global_max():
     df = DataFrame([[0, 10], [1, 20], [2, 40]])
@@ -146,33 +240,27 @@ def test_dataframemodel_get_bgcolor_with_numbers_using_global_max():
     s = dataframeeditor.BACKGROUND_NUMBER_SATURATION
     v = dataframeeditor.BACKGROUND_NUMBER_VALUE
     a = dataframeeditor.BACKGROUND_NUMBER_ALPHA
-    assert colorclose(bgcolor(dfm, 0, 1), (h0 + dh,           s, v, a))
-    assert colorclose(bgcolor(dfm, 1, 1), (h0 + 39 / 40 * dh, s, v, a))
-    assert colorclose(bgcolor(dfm, 2, 1), (h0 + 38 / 40 * dh, s, v, a))
-    assert colorclose(bgcolor(dfm, 0, 2), (h0 + 30 / 40 * dh, s, v, a))
-    assert colorclose(bgcolor(dfm, 1, 2), (h0 + 20 / 40 * dh, s, v, a))
-    assert colorclose(bgcolor(dfm, 2, 2), (h0,                s, v, a))
-
-def test_dataframemodel_get_bgcolor_for_index():
-    df = DataFrame([[0]])
-    dfm = DataFrameModel(df)
-    h, s, v, dummy = QColor(dataframeeditor.BACKGROUND_NONNUMBER_COLOR).getHsvF()
-    a = dataframeeditor.BACKGROUND_INDEX_ALPHA
-    assert colorclose(bgcolor(dfm, 0, 0), (h, s, v, a))
+    assert colorclose(bgcolor(dfm, 0, 0), (h0 + dh,           s, v, a))
+    assert colorclose(bgcolor(dfm, 1, 0), (h0 + 39 / 40 * dh, s, v, a))
+    assert colorclose(bgcolor(dfm, 2, 0), (h0 + 38 / 40 * dh, s, v, a))
+    assert colorclose(bgcolor(dfm, 0, 1), (h0 + 30 / 40 * dh, s, v, a))
+    assert colorclose(bgcolor(dfm, 1, 1), (h0 + 20 / 40 * dh, s, v, a))
+    assert colorclose(bgcolor(dfm, 2, 1), (h0,                s, v, a))
 
 def test_dataframemodel_get_bgcolor_with_string():
+    """Validate the color of the cell when a string is the data."""
     df = DataFrame([['xxx']])
     dfm = DataFrameModel(df)
     h, s, v, dummy = QColor(dataframeeditor.BACKGROUND_NONNUMBER_COLOR).getHsvF()
     a = dataframeeditor.BACKGROUND_STRING_ALPHA
-    assert colorclose(bgcolor(dfm, 0, 1), (h, s, v, a))
+    assert colorclose(bgcolor(dfm, 0, 0), (h, s, v, a))
 
 def test_dataframemodel_get_bgcolor_with_object():
     df = DataFrame([[None]])
     dfm = DataFrameModel(df)
     h, s, v, dummy = QColor(dataframeeditor.BACKGROUND_NONNUMBER_COLOR).getHsvF()
     a = dataframeeditor.BACKGROUND_MISC_ALPHA
-    assert colorclose(bgcolor(dfm, 0, 1), (h, s, v, a))
+    assert colorclose(bgcolor(dfm, 0, 0), (h, s, v, a))
 
 def test_dataframemodel_with_format_percent_d_and_nan():
     """
@@ -184,8 +272,8 @@ def test_dataframemodel_with_format_percent_d_and_nan():
     np_array[1] = numpy.nan
     dataframe = DataFrame(np_array)
     dfm = DataFrameModel(dataframe, format='%d')
-    assert data(dfm, 0, 1) == '0'
-    assert data(dfm, 1, 1) == 'nan'
+    assert data(dfm, 0, 0) == '0'
+    assert data(dfm, 1, 0) == 'nan'
 
 def test_change_format_emits_signal(qtbot, monkeypatch):
     mockQInputDialog = Mock()
@@ -211,27 +299,6 @@ def test_change_format_with_format_not_starting_with_percent(qtbot, monkeypatch)
     with qtbot.assertNotEmitted(editor.sig_option_changed):
         editor.change_format()
 
-def test_header_bom():
-    df = read_csv(os.path.join(FILES_PATH, 'issue_2514.csv'))
-    editor = DataFrameEditor(None)
-    editor.setup_and_check(df)
-    model = editor.dataModel
-    assert model.headerData(1, orientation=Qt.Horizontal) == "Date (MMM-YY)"
-
-@pytest.mark.skipif(is_module_installed('pandas', '<0.19'),
-                    reason="It doesn't work for Pandas 0.19-")
-def test_header_encoding():
-    df = read_csv(os.path.join(FILES_PATH, 'issue_3896.csv'))
-    editor = DataFrameEditor(None)
-    editor.setup_and_check(df)
-    model = editor.dataModel
-    assert model.headerData(0, orientation=Qt.Horizontal) == "Index"
-    assert model.headerData(1, orientation=Qt.Horizontal) == "Unnamed: 0"
-    assert model.headerData(2, orientation=Qt.Horizontal) == "Unieke_Idcode"
-    assert model.headerData(3, orientation=Qt.Horizontal) == "a"
-    assert model.headerData(4, orientation=Qt.Horizontal) == "b"
-    assert model.headerData(5, orientation=Qt.Horizontal) == "c"
-    assert model.headerData(6, orientation=Qt.Horizontal) == "d"
 
 def test_dataframeeditor_with_various_indexes():
     for rng_name, rng in generate_pandas_indexes().items():
@@ -239,41 +306,42 @@ def test_dataframeeditor_with_various_indexes():
         editor.setup_and_check(rng)
         dfm = editor.dataModel
         assert dfm.rowCount() == 20
-        assert dfm.columnCount() == 2
-        assert data(dfm, 0, 0) == '0'
-        assert data(dfm, 1, 0) == '1'
-        assert data(dfm, 2, 0) == '2'
-        assert data(dfm, 19, 0) == '19'
+        assert dfm.columnCount() == 1
+        header = editor.table_header.model()
+        assert header.headerData(0, Qt.Horizontal,
+                                 Qt.DisplayRole) == "0"
+
         if rng_name == "Index":
-            assert data(dfm, 0, 1) == 'A'
-            assert data(dfm, 1, 1) == 'B'
-            assert data(dfm, 2, 1) == 'C'
-            assert data(dfm, 19, 1) == 'T'
+            assert data(dfm, 0, 0) == 'A'
+            assert data(dfm, 1, 0) == 'B'
+            assert data(dfm, 2, 0) == 'C'
+            assert data(dfm, 19, 0) == 'T'
         elif rng_name == "RangeIndex":
-            assert data(dfm, 0, 1) == '0'
-            assert data(dfm, 1, 1) == '1'
-            assert data(dfm, 2, 1) == '2'
-            assert data(dfm, 19, 1) == '19'
+            assert data(dfm, 0, 0) == '0'
+            assert data(dfm, 1, 0) == '1'
+            assert data(dfm, 2, 0) == '2'
+            assert data(dfm, 19, 0) == '19'
         elif rng_name == "Float64Index":
-            assert data(dfm, 0, 1) == '0'
-            assert data(dfm, 1, 1) == '0.1'
-            assert data(dfm, 2, 1) == '0.2'
-            assert data(dfm, 19, 1) == '1.9'
+            assert data(dfm, 0, 0) == '0'
+            assert data(dfm, 1, 0) == '0.1'
+            assert data(dfm, 2, 0) == '0.2'
+            assert data(dfm, 19, 0) == '1.9'
         elif rng_name == "DatetimeIndex":
-            assert data(dfm, 0, 1) == '2017-01-01 00:00:00'
-            assert data(dfm, 1, 1) == '2017-01-02 00:00:00'
-            assert data(dfm, 2, 1) == '2017-01-03 00:00:00'
-            assert data(dfm, 19, 1) == '2017-01-20 00:00:00'
+            assert data(dfm, 0, 0) == '2017-01-01 00:00:00'
+            assert data(dfm, 1, 0) == '2017-01-02 00:00:00'
+            assert data(dfm, 2, 0) == '2017-01-03 00:00:00'
+            assert data(dfm, 19, 0) == '2017-01-20 00:00:00'
         elif rng_name == "MultiIndex":
-            assert data(dfm, 0, 1) == "('A', 'foo')"
-            assert data(dfm, 1, 1) == "('A', 'bar')"
-            assert data(dfm, 2, 1) == "('B', 'foo')"
-            assert data(dfm, 19, 1) == "('J', 'bar')"
+            assert data(dfm, 0, 0) == "('A', 'foo')"
+            assert data(dfm, 1, 0) == "('A', 'bar')"
+            assert data(dfm, 2, 0) == "('B', 'foo')"
+            assert data(dfm, 19, 0) == "('J', 'bar')"
         elif rng_name == "CategoricalIndex":
-            assert data(dfm, 0, 1) == 'a'
-            assert data(dfm, 1, 1) == 'b'
-            assert data(dfm, 2, 1) == 'c'
-            assert data(dfm, 19, 1) == 'b'
+            assert data(dfm, 0, 0) == 'a'
+            assert data(dfm, 1, 0) == 'b'
+            assert data(dfm, 2, 0) == 'c'
+            assert data(dfm, 19, 0) == 'b'
+
 
 
 @pytest.mark.skipif(PYQT4, reason="It generates a strange failure in another test")
@@ -284,12 +352,12 @@ def test_sort_dataframe_with_duplicate_column(qtbot):
     editor.setup_and_check(df)
     dfm = editor.dataModel
     QTimer.singleShot(1000, lambda: close_message_box(qtbot))
+    editor.dataModel.sort(0)
+    assert [data(dfm, row, 0) for row in range(len(df))] == ['1', '3', '2']
+    assert [data(dfm, row, 1) for row in range(len(df))] == ['4', '6', '5']
     editor.dataModel.sort(1)
-    assert [data(dfm, row, 1) for row in range(len(df))] == ['1', '3', '2']
-    assert [data(dfm, row, 2) for row in range(len(df))] == ['4', '6', '5']
-    editor.dataModel.sort(2)
-    assert [data(dfm, row, 1) for row in range(len(df))] == ['1', '2', '3']
-    assert [data(dfm, row, 2) for row in range(len(df))] == ['4', '5', '6']
+    assert [data(dfm, row, 0) for row in range(len(df))] == ['1', '2', '3']
+    assert [data(dfm, row, 1) for row in range(len(df))] == ['4', '5', '6']
 
 
 if __name__ == "__main__":