Repository: spyder-ide/spyder

Issue:
Variable explorer column widths are re-generated in every console evaluation
## Description

The variable explorer window re-generates the column widths on creation of every new variable, over-riding any user modification of the column widths. This is annoying as it also tends to make th type column wide enough to display full type information even though other columns (e.g. the Value) would be the ones the user is more interested in.

**What steps will reproduce the problem?**

1.  Set numpy and pylab to be automatically loaded. Observe that the variable explorer window has  entries for a number of items with long type names (e.g. cast and nbytes).
2.  Shrink the size the of the type column to allow the Value column to be larger
3.  Create a new variable in the console
4. Goto 2

**What is the expected output? What do you see instead?**

Either a smarter layout of columns  when the variable explorer is horizontally constrained, or some mechanism to 'freeze' the column widths or hide/collapse columns.

## Version and main components

* Spyder Version: 3.2.4 
* Python Version: 3.6.1
* Qt Versions: 5.6.2, PyQt5 5.6 on Windows

## Dependencies
```
pyflakes >=0.6.0 :  1.5.0 (OK)
pycodestyle >=2.3:  2.3.1 (OK)
pygments >=2.0   :  2.2.0 (OK)
pandas >=0.13.1  :  0.20.1 (OK)
numpy >=1.7      :  1.12.1 (OK)
sphinx >=0.6.6   :  1.5.6 (OK)
rope >=0.9.4     :  0.9.4-1 (OK)
jedi >=0.9.0     :  0.10.2 (OK)
nbconvert >=4.0  :  5.1.1 (OK)
sympy >=0.7.3    :  1.0 (OK)
cython >=0.21    :  0.25.2 (OK)
qtconsole >=4.2.0:  4.3.0 (OK)
IPython >=4.0    :  5.3.0 (OK)
pylint >=0.25    :  1.6.4 (OK)

```



Discussion:
> The variable explorer window re-generates the column widths on creation of every new variable, over-riding any user modification of the column widths.

Seems reasonable to me. We'll take a look at it in a future release.
@ccordoba12 I could work on that one. What would be the best solution according to you? Maybe an option button (close to the `reset_namespace` button) which, when on, would "freeze" the column width?
Thanks @Prikers!

> Maybe an option button

I don't think an option is necessary. We should simply preserve the column width after each evaluation. That's the intuitive/expected result, as @gb119 pointed out.
So it would be the very first evaluation that would determine the column widths? Or we just set some default rules like name is 20% of the total width, size is 10%, etc.?
I think some default rules would be fine. @goanpeca, what do you think?
If I might butt in.... :-)
I would think the user manually adjusting the column widths by dragging the splitters in the header should set some flag in the internal state. If that flag is not set then the current behaviour should happen, but if the flag is set, then the columns widths should not longer change.
I guess one could have some UI to reset that - perhaps in a context menu - the existing context menu has a 'resize row to contents' you could add a 'automatically resize column' entry too.
Very good ideas @gb119! I really like them ;-)

@Prikers?
>If I might butt in.... :-)
I would think the user manually adjusting the column widths by dragging the splitters in the header should set some flag in the internal state. If that flag is not set then the current behaviour should happen, but if the flag is set, then the columns widths should not longer change.
I guess one could have some UI to reset that - perhaps in a context menu - the existing context menu has a 'resize row to contents' you could add a 'automatically resize column' entry too.

Sounds good!
Sounds good to me too!

Resolving patch:
diff --git a/spyder/plugins/variableexplorer/widgets/collectionseditor.py b/spyder/plugins/variableexplorer/widgets/collectionseditor.py
--- a/spyder/plugins/variableexplorer/widgets/collectionseditor.py
+++ b/spyder/plugins/variableexplorer/widgets/collectionseditor.py
@@ -32,7 +32,7 @@
                          Signal, Slot)
 from qtpy.QtGui import QColor, QKeySequence
 from qtpy.QtWidgets import (QAbstractItemDelegate, QApplication, QDateEdit,
-                            QDateTimeEdit, QDialog, QHBoxLayout,
+                            QDateTimeEdit, QDialog, QHBoxLayout, QHeaderView,
                             QInputDialog, QItemDelegate, QLineEdit, QMenu,
                             QMessageBox, QPushButton, QTableView,
                             QVBoxLayout, QWidget)
@@ -685,6 +685,32 @@ def setModelData(self, editor, model, index):
         self.set_value(index, value)
 
 
+class BaseHeaderView(QHeaderView):
+    """
+    A header view for the BaseTableView that emits a signal when the width of
+    one of its sections is resized by the user.
+    """
+    sig_user_resized_section = Signal(int, int, int)
+
+    def __init__(self, parent=None):
+        super(BaseHeaderView, self).__init__(Qt.Horizontal, parent)
+        self._handle_section_is_pressed = False
+        self.sectionResized.connect(self.sectionResizeEvent)
+
+    def mousePressEvent(self, e):
+        self._handle_section_is_pressed = (self.cursor().shape() ==
+                                           Qt.SplitHCursor)
+        super(BaseHeaderView, self).mousePressEvent(e)
+
+    def mouseReleaseEvent(self, e):
+        self._handle_section_is_pressed = False
+        super(BaseHeaderView, self).mouseReleaseEvent(e)
+
+    def sectionResizeEvent(self, logicalIndex, oldSize, newSize):
+        if self._handle_section_is_pressed:
+            self.sig_user_resized_section.emit(logicalIndex, oldSize, newSize)
+
+
 class BaseTableView(QTableView):
     """Base collection editor table view"""
     sig_option_changed = Signal(str, object)
@@ -711,7 +737,11 @@ def __init__(self, parent):
         self.duplicate_action = None
         self.delegate = None
         self.setAcceptDrops(True)
-        
+        self.automatic_column_width = True
+        self.setHorizontalHeader(BaseHeaderView(parent=self))
+        self.horizontalHeader().sig_user_resized_section.connect(
+            self.user_resize_columns)
+
     def setup_table(self):
         """Setup table"""
         self.horizontalHeader().setStretchLastSection(True)
@@ -728,6 +758,10 @@ def setup_menu(self, minmax):
         
         resize_action = create_action(self, _("Resize rows to contents"),
                                       triggered=self.resizeRowsToContents)
+        resize_columns_action = create_action(
+            self,
+            _("Resize columns to contents"),
+            triggered=self.resize_column_contents)
         self.paste_action = create_action(self, _("Paste"),
                                           icon=ima.icon('editpaste'),
                                           triggered=self.paste)
@@ -775,14 +809,14 @@ def setup_menu(self, minmax):
                         self.insert_action, self.remove_action,
                         self.copy_action, self.paste_action,
                         None, self.rename_action, self.duplicate_action,
-                        None, resize_action]
+                        None, resize_action, resize_columns_action]
         if ndarray is not FakeObject:
             menu_actions.append(self.minmax_action)
         add_actions(menu, menu_actions)
         self.empty_ws_menu = QMenu(self)
         add_actions(self.empty_ws_menu,
                     [self.insert_action, self.paste_action,
-                     None, resize_action])
+                     None, resize_action, resize_columns_action])
         return menu
     
     #------ Remote/local API --------------------------------------------------
@@ -867,12 +901,22 @@ def refresh_plot_entries(self, index):
         self.hist_action.setVisible(condition_hist or is_list)
         self.imshow_action.setVisible(condition_imshow)
         self.save_array_action.setVisible(is_array)
-        
+
+    def resize_column_contents(self):
+        """Resize columns to contents."""
+        self.automatic_column_width = True
+        self.adjust_columns()
+
+    def user_resize_columns(self, logical_index, old_size, new_size):
+        """Handle the user resize action."""
+        self.automatic_column_width = False
+
     def adjust_columns(self):
         """Resize two first columns to contents"""
-        for col in range(3):
-            self.resizeColumnToContents(col)
-        
+        if self.automatic_column_width:
+            for col in range(3):
+                self.resizeColumnToContents(col)
+
     def set_data(self, data):
         """Set table data"""
         if data is not None:
diff --git a/spyder/plugins/variableexplorer/widgets/namespacebrowser.py b/spyder/plugins/variableexplorer/widgets/namespacebrowser.py
--- a/spyder/plugins/variableexplorer/widgets/namespacebrowser.py
+++ b/spyder/plugins/variableexplorer/widgets/namespacebrowser.py
@@ -140,7 +140,7 @@ def setup(self, check_all=None, exclude_private=None,
         self.setLayout(layout)
 
         self.sig_option_changed.connect(self.option_changed)
-        
+
     def set_shellwidget(self, shellwidget):
         """Bind shellwidget instance to namespace browser"""
         self.shellwidget = shellwidget
@@ -347,6 +347,7 @@ def import_data(self, filenames=None):
     def reset_namespace(self):
         warning = CONF.get('ipython_console', 'show_reset_namespace_warning')
         self.shellwidget.reset_namespace(warning=warning, message=True)
+        self.editor.automatic_column_width = True
 
     @Slot()
     def save_data(self, filename=None):


Test patch:
diff --git a/spyder/plugins/variableexplorer/widgets/tests/test_namespacebrowser.py b/spyder/plugins/variableexplorer/widgets/tests/test_namespacebrowser.py
--- a/spyder/plugins/variableexplorer/widgets/tests/test_namespacebrowser.py
+++ b/spyder/plugins/variableexplorer/widgets/tests/test_namespacebrowser.py
@@ -28,5 +28,25 @@ def test_setup_sets_dataframe_format(qtbot):
     assert browser.editor.model.dataframe_format == '%10.5f'
 
 
+def test_automatic_column_width(qtbot):
+    browser = NamespaceBrowser(None)
+    browser.set_shellwidget(Mock())
+    browser.setup(exclude_private=True, exclude_uppercase=True,
+                  exclude_capitalized=True, exclude_unsupported=True,
+                  minmax=False)
+    col_width = [browser.editor.columnWidth(i) for i in range(4)]
+    browser.set_data({'a_variable':
+            {'type': 'int', 'size': 1, 'color': '#0000ff', 'view': '1'}})
+    new_col_width = [browser.editor.columnWidth(i) for i in range(4)]
+    assert browser.editor.automatic_column_width
+    assert col_width != new_col_width  # Automatic col width is on
+    browser.editor.horizontalHeader()._handle_section_is_pressed = True
+    browser.editor.setColumnWidth(0, 100)  # Simulate user changing col width
+    assert browser.editor.automatic_column_width == False
+    browser.set_data({'a_lengthy_variable_name_which_should_change_width':
+            {'type': 'int', 'size': 1, 'color': '#0000ff', 'view': '1'}})
+    assert browser.editor.columnWidth(0) == 100  # Automatic col width is off
+
+
 if __name__ == "__main__":
     pytest.main()