Repository: spyder-ide/spyder
Pull request: PR: Add action and shortcut to Variable explorer editors to easily close them
<!--- Make sure to read the Contributing Guidelines:                   --->
<!--- https://github.com/spyder-ide/spyder/blob/master/CONTRIBUTING.md --->
<!--- and follow PEP 8, PEP 257 and Spyder's code style:               --->
<!--- https://github.com/spyder-ide/spyder/wiki/Dev:-Coding-Style      --->

## Description of Changes


<!--- Explain what you've done and why --->




### Issue(s) Resolved

<!--- List the issue(s) below, in the form "Fixes #1234"; one per line --->

Fixes #21814


### Affirmation

By submitting this Pull Request or typing my (user)name below,
I affirm the [Developer Certificate of Origin](https://developercertificate.org)
with respect to all commits and content included in this PR,
and understand I am releasing the same under Spyder's MIT (Expat) license.

<!--- TYPE YOUR USER/NAME AFTER THE FOLLOWING: --->
I certify the above statement is true and correct:

<!--- Thanks for your help making Spyder better for everyone! --->
@jsbautista 

Problem statement:
Variable explorer : multiple windows open and impossible to close them with shortcuts
## Description

### What steps will reproduce the problem?

<!--- You can use Markdown here --->

In the variable explorer pane multiple windows open when clicking on a variable (say a list) but they do not update whenever we run the code again, nor get closed automatically. This results in a lot of windows open and no shortcut to clear all of that. Thanks for your help.



## Versions

* Spyder version: 5.4.3  (conda)
* Python version: 3.11.3 64-bit
* Qt version: 5.15.2
* PyQt5 version: 5.15.7
* Operating System: Darwin 22.4.0

### Dependencies

```
# Mandatory:
applaunchservices >=0.3.0                        :  0.3.0 (OK)
atomicwrites >=1.2.0                             :  1.4.0 (OK)
chardet >=2.0.0                                  :  4.0.0 (OK)
cloudpickle >=0.5.0                              :  2.2.1 (OK)
cookiecutter >=1.6.0                             :  1.7.3 (OK)
diff_match_patch >=20181111                      :  20200713 (OK)
intervaltree >=3.0.2                             :  3.1.0 (OK)
IPython >=7.31.1,<9.0.0,!=8.8.0,!=8.9.0,!=8.10.0 :  8.12.0 (OK)
jedi >=0.17.2,<0.19.0                            :  0.18.1 (OK)
jellyfish >=0.7                                  :  0.9.0 (OK)
jsonschema >=3.2.0                               :  4.17.3 (OK)
keyring >=17.0.0                                 :  23.13.1 (OK)
nbconvert >=4.0                                  :  6.5.4 (OK)
numpydoc >=0.6.0                                 :  1.5.0 (OK)
parso >=0.7.0,<0.9.0                             :  0.8.3 (OK)
pexpect >=4.4.0                                  :  4.8.0 (OK)
pickleshare >=0.4                                :  0.7.5 (OK)
psutil >=5.3                                     :  5.9.0 (OK)
pygments >=2.0                                   :  2.15.1 (OK)
pylint >=2.5.0,<3.0                              :  2.16.2 (OK)
pylint_venv >=2.1.1                              :  2.3.0 (OK)
pyls_spyder >=0.4.0                              :  0.4.0 (OK)
pylsp >=1.7.2,<1.8.0                             :  1.7.2 (OK)
pylsp_black >=1.2.0                              :  1.2.1 (OK)
qdarkstyle >=3.0.2,<3.2.0                        :  3.0.2 (OK)
qstylizer >=0.2.2                                :  0.2.2 (OK)
qtawesome >=1.2.1                                :  1.2.2 (OK)
qtconsole >=5.4.2,<5.5.0                         :  5.4.2 (OK)
qtpy >=2.1.0                                     :  2.2.0 (OK)
rtree >=0.9.7                                    :  1.0.1 (OK)
setuptools >=49.6.0                              :  67.8.0 (OK)
sphinx >=0.6.6                                   :  5.0.2 (OK)
spyder_kernels >=2.4.3,<2.5.0                    :  2.4.3 (OK)
textdistance >=4.2.0                             :  4.2.1 (OK)
three_merge >=0.1.1                              :  0.1.1 (OK)
watchdog >=0.10.3                                :  2.1.6 (OK)
zmq >=22.1.0                                     :  25.1.0 (OK)

# Optional:
cython >=0.21                                    :  None (NOK)
matplotlib >=3.0.0                               :  3.8.0 (OK)
numpy >=1.7                                      :  1.25.2 (OK)
pandas >=1.1.1                                   :  2.0.3 (OK)
scipy >=0.17.0                                   :  1.11.3 (OK)
sympy >=0.7.3                                    :  None (NOK)
```

Hints from issue discussion:
Hey @JulietteCx, thanks for reporting. You said:

> In the variable explorer pane multiple windows open when clicking on a variable (say a list) but they do not update whenever we run the code again

This will change in Spyder 6 because we added a refresh button to our viewers. So users will be able to update them without closing and reopening variables.

Unfortunately, we can't refresh our viewers automatically after every run because viewing a variable requires making a copy in memory of it. So, if you have a lot of variables open, Spyder will start consuming a lot of memory.

> nor get closed automatically. This results in a lot of windows open and no shortcut to clear all of that. Thanks for your help.

We haven't considered this use case before, so thanks for bringing it to our attention. I understand this can be a nuisance with many viewers open. And I like the idea of having a shortcut to close them all at once (which shouldn't be that hard to implement).

What do you think about also adding a shortcut to close the current viewer? Would you find that useful?
Hi Carlos,

Thanks for your reply. Good to know for the next version I think that will be very helpful. I get your point about refreshing. If there was a shortcut to clear all the windows I think that would be a huge improvement already! :)

For the shortcut to close the current viewer, I am sorry but not so sure what you’re referring to. Might you clarify and I can let you know? Thanks!

Best wishes,

Juliette

> If there was a shortcut to clear all the windows I think that would be a huge improvement already! :)

I totally agree. Since we're wrapping up Spyder 6.0, it's probably not going to be available for that version. But I hope we'll have time to implement it for 6.1, to be released during the second half of the year.

> For the shortcut to close the current viewer, I am sorry but not so sure what you’re referring to. Might you clarify and I can let you know? Thanks!

I meant the following:

* Click on a variable to quickly check its contents.
* A viewer is open for it.
* Since I just needed that for a quick peek, I don't want to keep that viewer open. So perhaps it'd be useful to simply press a shortcut (say `Ctrl+W`) to close the viewer and go back to Spyder to keep working on it.
Hi Carlos,

Thanks for your reply. Such a shame that I’ll have to wait for Spyder 6.1 ... I should have reached out earlier, I had noticed that very early on when I was using it.

Regarding the shortcut, thanks for clarifying. This is exactly what I meant and I think it would be tremendously helpful to have it!! :-)

Looking forward to the next version then ;-)

Best wishes,

Juliette

Thanks for the extra input @JulietteCx! We'll take it into account when implementing this feature.

Commit message: add shortcuts to menu

Patch to review:
diff --git a/spyder/api/widgets/mixins.py b/spyder/api/widgets/mixins.py
index 691c13f604b..b4e276311d6 100644
--- a/spyder/api/widgets/mixins.py
+++ b/spyder/api/widgets/mixins.py
@@ -257,14 +257,18 @@ class SpyderMenuMixin:
     """
 
     def add_item_to_menu(self, action_or_menu, menu, section=None,
-                         before=None):
+                         before=None, before_section=None):
         """
         Add a SpyderAction or a QWidget to the menu.
         """
         if not isinstance(menu, SpyderMenu):
             raise SpyderAPIError('Menu must be an instance of SpyderMenu!')
 
-        menu.add_action(action_or_menu, section=section, before=before)
+        menu.add_action(
+            action_or_menu,
+            section=section,
+            before=before,
+            before_section=before_section)
 
     def _create_menu(
         self,
@@ -442,7 +446,7 @@ def _update_action_state(self, action_name, value):
     # name where it is applied).
     def create_action(self, name, text, icon=None, icon_text='', tip=None,
                       toggled=None, triggered=None, data=None,
-                      shortcut_context=None,
+                      shortcut=None, shortcut_context=None,
                       context=Qt.WidgetWithChildrenShortcut, initial=None,
                       register_shortcut=False, section=None, option=None,
                       parent=None, register_action=True, overwrite=False,
@@ -538,6 +542,7 @@ def create_action(self, name, text, icon=None, icon_text='', tip=None,
             data=data,
             context=context,
             section=section,
+            shortcut=shortcut,
             option=option,
             id_=name,
             plugin=self.PLUGIN_NAME,
diff --git a/spyder/config/main.py b/spyder/config/main.py
index a785ddd00eb..fbaa9bcae81 100644
--- a/spyder/config/main.py
+++ b/spyder/config/main.py
@@ -557,6 +557,7 @@
               'ipython_console/enter array inline': "Ctrl+Alt+M",
               'ipython_console/enter array table': "Ctrl+M",
               # -- Variable explorer --
+              'variable_explorer/close': 'Ctrl+W',
               'variable_explorer/copy': 'Ctrl+C',
               'variable_explorer/search': 'Ctrl+F',
               'variable_explorer/refresh': 'Ctrl+R',
diff --git a/spyder/plugins/variableexplorer/widgets/arrayeditor.py b/spyder/plugins/variableexplorer/widgets/arrayeditor.py
index 84173cef759..34a341f1710 100644
--- a/spyder/plugins/variableexplorer/widgets/arrayeditor.py
+++ b/spyder/plugins/variableexplorer/widgets/arrayeditor.py
@@ -54,6 +54,7 @@
 # =============================================================================
 
 class ArrayEditorActions:
+    Close = 'close'
     Copy = 'copy_action'
     Edit = 'edit_action'
     Preferences = 'preferences_action'
@@ -773,6 +774,15 @@ def do_nothing():
             triggered=self.show_preferences_dialog,
             register_action=False
         )
+        self.close_action = self.create_action(
+            name=ArrayEditorActions.Close,
+            icon=self.create_icon('close_pane'),
+            text=_('Close'),
+            triggered=self.reject,
+            shortcut=self.get_shortcut(ArrayEditorActions.Close),
+            register_shortcut=True
+        )
+        self.register_shortcut_for_widget(name='close', triggered=self.reject)
         self.refresh_action = self.create_action(
             ArrayEditorActions.Refresh,
             text=_('Refresh'),
@@ -797,7 +807,8 @@ def do_nothing():
             ArrayEditorMenus.Options,
             register=False
         )
-        options_menu.add_action(self.preferences_action)
+        for item in [self.preferences_action, self.close_action]:
+            self.add_item_to_menu(item, options_menu)
 
         options_button = self.create_toolbutton(
             name=ArrayEditorWidgets.OptionsToolButton,
diff --git a/spyder/plugins/variableexplorer/widgets/dataframeeditor.py b/spyder/plugins/variableexplorer/widgets/dataframeeditor.py
index 0f45534f502..8c70c6eb9c4 100644
--- a/spyder/plugins/variableexplorer/widgets/dataframeeditor.py
+++ b/spyder/plugins/variableexplorer/widgets/dataframeeditor.py
@@ -96,6 +96,7 @@
 # =============================================================================
 
 class DataframeEditorActions:
+    Close = 'close'
     ConvertToBool = 'convert_to_bool_action'
     ConvertToComplex = 'convert_to_complex_action'
     ConvertToFloat = 'convert_to_float_action'
@@ -1803,6 +1804,16 @@ def __init__(
             triggered=self.show_preferences_dialog,
             register_action=False
         )
+        self.close_action = self.create_action(
+            name=DataframeEditorActions.Close,
+            icon=self.create_icon('close_pane'),
+            text=_('Close'),
+            triggered=self.reject,
+            shortcut=self.get_shortcut(DataframeEditorActions.Close),
+            context=Qt.WindowShortcut,
+            register_shortcut=True
+        )
+        self.register_shortcut_for_widget(name='close', triggered=self.reject)
 
         # Destroying the C++ object right after closing the dialog box,
         # otherwise it may be garbage-collected in another QThread
@@ -1967,7 +1978,8 @@ def set_data_and_check(self, data) -> bool:
             DataframeEditorMenus.Options,
             register=False
         )
-        self.add_item_to_menu(self.preferences_action, options_menu)
+        for item in [self.preferences_action, self.close_action]:
+            self.add_item_to_menu(item, options_menu)
 
         options_button = self.create_toolbutton(
             name=DataframeEditorWidgets.OptionsToolButton,
diff --git a/spyder/plugins/variableexplorer/widgets/objectexplorer/objectexplorer.py b/spyder/plugins/variableexplorer/widgets/objectexplorer/objectexplorer.py
index a1a6fc546bd..b2a6f3ee825 100644
--- a/spyder/plugins/variableexplorer/widgets/objectexplorer/objectexplorer.py
+++ b/spyder/plugins/variableexplorer/widgets/objectexplorer/objectexplorer.py
@@ -39,6 +39,7 @@
 
 
 class ObjectExplorerActions:
+    Close = "close"
     Refresh = 'refresh_action'
     ShowCallable = 'show_callable_action'
     ShowSpecialAttributes = 'show_special_attributes_action'
@@ -48,6 +49,11 @@ class ObjectExplorerMenus:
     Options = 'options_menu'
 
 
+class ObjectExplorerOptionsMenuSections:
+    Toggle = 'toggle_section'
+    Close = 'close_section'
+
+
 class ObjectExplorerWidgets:
     OptionsToolButton = 'options_button_widget'
     Toolbar = 'toolbar'
@@ -57,7 +63,7 @@ class ObjectExplorerWidgets:
 EDITOR_NAME = 'Object'
 
 
-class ObjectExplorer(BaseDialog, SpyderFontsMixin, SpyderWidgetMixin):
+class ObjectExplorer(BaseDialog, SpyderWidgetMixin, SpyderFontsMixin):
     """Object explorer main widget window."""
     CONF_SECTION = 'variable_explorer'
 
@@ -182,7 +188,11 @@ def set_value(self, obj):
 
         # Add menu item for toggling columns to the Options menu
         for action in self.obj_tree.toggle_column_actions_group.actions():
-            self.add_item_to_menu(action, self.show_cols_submenu)
+            self.add_item_to_menu(
+                action,
+                menu=self.show_cols_submenu,
+                section=ObjectExplorerOptionsMenuSections.Toggle,
+                before_section=ObjectExplorerOptionsMenuSections.Close)
         column_visible = [col.col_visible for col in self._attr_cols]
         for idx, visible in enumerate(column_visible):
             elem = self.obj_tree.toggle_column_actions_group.actions()[idx]
@@ -247,12 +257,29 @@ def do_nothing():
             triggered=self.refresh_editor,
             register_action=False
         )
+
+        self.close_action = self.create_action(
+            name=ObjectExplorerActions.Close,
+            icon=self.create_icon('close_pane'),
+            text=_('Close'),
+            triggered=self.reject,
+            shortcut=self.get_shortcut(ObjectExplorerActions.Close),
+            register_shortcut=True
+        )
+        self.register_shortcut_for_widget(name='close', triggered=self.reject)
+
         self.refresh_action.setEnabled(self.data_function is not None)
 
         self.show_cols_submenu = self.create_menu(
             ObjectExplorerMenus.Options,
             register=False
         )
+        for item in [self.close_action]:
+            self.add_item_to_menu(
+                item,
+                menu=self.show_cols_submenu,
+                section=ObjectExplorerOptionsMenuSections.Close,)
+
         self.options_button = self.create_toolbutton(
             name=ObjectExplorerWidgets.OptionsToolButton,
             text=_('Options'),
diff --git a/spyder/plugins/variableexplorer/widgets/texteditor.py b/spyder/plugins/variableexplorer/widgets/texteditor.py
index 05834c86a5f..b5231c8dbc5 100644
--- a/spyder/plugins/variableexplorer/widgets/texteditor.py
+++ b/spyder/plugins/variableexplorer/widgets/texteditor.py
@@ -13,10 +13,12 @@
 
 # Third party imports
 from qtpy.QtCore import Qt, Slot
-from qtpy.QtWidgets import QHBoxLayout, QPushButton, QTextEdit, QVBoxLayout
+from qtpy.QtWidgets import (
+    QHBoxLayout, QPushButton, QTextEdit, QToolButton, QVBoxLayout)
 
 # Local import
 from spyder.api.fonts import SpyderFontsMixin, SpyderFontType
+from spyder.api.widgets.mixins import SpyderWidgetMixin
 from spyder.config.base import _
 from spyder.py3compat import (is_binary_string, to_binary_string,
                               to_text_string)
@@ -24,8 +26,32 @@
 from spyder.plugins.variableexplorer.widgets.basedialog import BaseDialog
 
 
-class TextEditor(BaseDialog, SpyderFontsMixin):
+# =============================================================================
+# ---- Constants
+# =============================================================================
+
+class TextEditorActions:
+    Close = 'close'
+    Copy = 'copy_action'
+
+
+class TextEditorMenus:
+    Options = 'options_menu'
+
+
+class TextEditorWidgets:
+    OptionsToolButton = 'options_button_widget'
+    Toolbar = 'toolbar'
+    ToolbarStretcher = 'toolbar_stretcher'
+
+class TextEditorToolbarSections:
+    Copy = 'copy_section'
+
+
+class TextEditor(BaseDialog, SpyderWidgetMixin, SpyderFontsMixin):
     """Array Editor Dialog"""
+    CONF_SECTION = 'variable_explorer'
+
     def __init__(self, text, title='', parent=None, readonly=False):
         super().__init__(parent)
 
@@ -38,6 +64,16 @@ def __init__(self, text, title='', parent=None, readonly=False):
         self.text = None
         self.btn_save_and_close = None
 
+        self.close_action = self.create_action(
+            name=TextEditorActions.Close,
+            icon=self.create_icon('close_pane'),
+            text=_('Close'),
+            triggered=self.reject,
+            shortcut=self.get_shortcut(TextEditorActions.Close),
+            register_shortcut=True
+        )
+        self.register_shortcut_for_widget(name='close', triggered=self.reject)
+
         # Display text as unicode if it comes as bytes, so users see
         # its right representation
         if is_binary_string(text):
@@ -49,6 +85,13 @@ def __init__(self, text, title='', parent=None, readonly=False):
         self.layout = QVBoxLayout()
         self.setLayout(self.layout)
 
+        self.toolbar = self.create_toolbar(
+                TextEditorWidgets.Toolbar,
+                register=False
+            )
+
+        self.layout.addWidget(self.toolbar)
+
         # Text edit
         self.edit = QTextEdit(parent)
         self.edit.setReadOnly(readonly)
@@ -73,6 +116,8 @@ def __init__(self, text, title='', parent=None, readonly=False):
         self.btn_close.clicked.connect(self.reject)
         btn_layout.addWidget(self.btn_close)
 
+        
+
         self.layout.addLayout(btn_layout)
 
         # Make the dialog act as a window
@@ -95,6 +140,38 @@ def __init__(self, text, title='', parent=None, readonly=False):
         self.setWindowTitle(_("Text editor") + \
                             u"%s" % (u" - " + unicode_title
                                      if unicode_title else u""))
+        
+        stretcher = self.create_stretcher(
+            TextEditorWidgets.ToolbarStretcher
+        )
+        options_menu = self.create_menu(
+            TextEditorMenus.Options,
+            register=False
+        )
+        for item in [self.close_action]:
+            self.add_item_to_menu(item, options_menu)
+        options_button = self.create_toolbutton(
+            name=TextEditorWidgets.OptionsToolButton,
+            text=_('Options'),
+            icon=ima.icon('tooloptions'),
+            register=False
+        )
+        options_button.setPopupMode(QToolButton.InstantPopup)
+        options_button.setMenu(options_menu)
+        self.toolbar.clear()
+        self.toolbar._section_items.clear()
+        self.toolbar._item_map.clear()
+        
+        for item in [
+            stretcher,
+            options_button
+        ]:
+            self.add_item_to_toolbar(
+                item,
+                self.toolbar,
+                section=TextEditorToolbarSections.Copy
+            )
+        self.toolbar.render()
 
     @Slot()
     def text_changed(self):
diff --git a/spyder/widgets/collectionseditor.py b/spyder/widgets/collectionseditor.py
index f7a80167023..ec53eca985b 100644
--- a/spyder/widgets/collectionseditor.py
+++ b/spyder/widgets/collectionseditor.py
@@ -36,7 +36,7 @@
 from qtpy.QtGui import QColor, QKeySequence
 from qtpy.QtWidgets import (
     QApplication, QHBoxLayout, QHeaderView, QInputDialog, QLineEdit,
-    QMessageBox, QPushButton, QTableView, QVBoxLayout, QWidget)
+    QMessageBox, QPushButton, QTableView, QToolButton, QVBoxLayout, QWidget)
 from spyder_kernels.utils.lazymodules import (
     FakeObject, numpy as np, pandas as pd, PIL)
 from spyder_kernels.utils.misc import fix_reference_name
@@ -71,6 +71,7 @@
 # ---- Constants
 # =============================================================================
 class CollectionsEditorActions:
+    Close = 'close'
     Copy = 'copy_action'
     Duplicate = 'duplicate_action'
     Edit = 'edit_action'
@@ -100,6 +101,7 @@ class CollectionsEditorMenus:
 
 
 class CollectionsEditorWidgets:
+    OptionsToolButton = 'options_button_widget'
     Toolbar = 'toolbar'
     ToolbarStretcher = 'toolbar_stretcher'
 
@@ -1736,7 +1738,7 @@ def set_filter(self, dictfilter=None):
 class CollectionsEditorWidget(QWidget, SpyderWidgetMixin):
     """Dictionary Editor Widget"""
     # Dummy conf section to avoid a warning from SpyderConfigurationObserver
-    CONF_SECTION = ""
+    CONF_SECTION = "variable_explorer"
 
     sig_refresh_requested = Signal()
 
@@ -1752,6 +1754,8 @@ def __init__(self, parent, data, namespacebrowser=None,
                 self, data, namespacebrowser, data_function, readonly, title
             )
 
+        self.parent = parent
+
         self.refresh_action = self.create_action(
             name=CollectionsEditorActions.Refresh,
             text=_('Refresh'),
@@ -1761,6 +1765,17 @@ def __init__(self, parent, data, namespacebrowser=None,
             register_action=None
         )
 
+        self.close_action = self.create_action(
+            name=CollectionsEditorActions.Close,
+            icon=self.create_icon('close_pane'),
+            text=_('Close'),
+            triggered=self.close_window,
+            shortcut=self.get_shortcut(CollectionsEditorActions.Close),
+            register_shortcut=True
+        )
+        self.register_shortcut_for_widget(
+            name='close', triggered=self.close_window)
+
         toolbar = self.create_toolbar(
             CollectionsEditorWidgets.Toolbar,
             register=False
@@ -1783,6 +1798,23 @@ def __init__(self, parent, data, namespacebrowser=None,
                 section=CollectionsEditorToolbarSections.AddDelete
             )
 
+        options_menu = self.create_menu(
+            CollectionsEditorMenus.Options,
+            register=False
+        )
+
+        for item in [self.close_action]:
+            self.add_item_to_menu(item, options_menu)
+
+        options_button = self.create_toolbutton(
+            name=CollectionsEditorWidgets.OptionsToolButton,
+            text=_('Options'),
+            icon=ima.icon('tooloptions'),
+            register=False
+        )
+        options_button.setPopupMode(QToolButton.InstantPopup)
+        options_button.setMenu(options_menu)
+
         for item in [
             self.editor.view_action,
             self.editor.plot_action,
@@ -1791,7 +1823,8 @@ def __init__(self, parent, data, namespacebrowser=None,
             stretcher,
             self.editor.resize_action,
             self.editor.resize_columns_action,
-            self.refresh_action
+            self.refresh_action,
+            options_button
         ]:
             self.add_item_to_toolbar(
                 item,
@@ -1819,6 +1852,10 @@ def set_data(self, data):
     def get_title(self):
         """Get model title"""
         return self.editor.source_model.title
+    
+    def close_window(self):
+        if self.parent:
+            self.parent.reject
 
 
 class CollectionsEditor(BaseDialog):


Code review comments:
diff_hunk: @@ -182,7 +188,11 @@ def set_value(self, obj):
 
         # Add menu item for toggling columns to the Options menu
         for action in self.obj_tree.toggle_column_actions_group.actions():
-            self.add_item_to_menu(action, self.show_cols_submenu)
+            self.add_item_to_menu(
+                action,
+                menu=self.show_cols_submenu,
+                section=ObjectExplorerOptionsMenuSections.Toggle,
+                before_section=ObjectExplorerOptionsMenuSections.Close)
path: spyder/plugins/variableexplorer/widgets/objectexplorer/objectexplorer.py
text: @user1:
```suggestion
                before_section=ObjectExplorerOptionsMenuSections.Close
            )
```

Style improvement.

diff_hunk: @@ -95,6 +140,38 @@ def __init__(self, text, title='', parent=None, readonly=False):
         self.setWindowTitle(_("Text editor") + \
                             u"%s" % (u" - " + unicode_title
                                      if unicode_title else u""))
+        
+        stretcher = self.create_stretcher(
+            TextEditorWidgets.ToolbarStretcher
+        )
+        options_menu = self.create_menu(
+            TextEditorMenus.Options,
+            register=False
+        )
+        for item in [self.close_action]:
+            self.add_item_to_menu(item, options_menu)
+        options_button = self.create_toolbutton(
+            name=TextEditorWidgets.OptionsToolButton,
+            text=_('Options'),
+            icon=ima.icon('tooloptions'),
+            register=False
+        )
+        options_button.setPopupMode(QToolButton.InstantPopup)
+        options_button.setMenu(options_menu)
+        self.toolbar.clear()
+        self.toolbar._section_items.clear()
+        self.toolbar._item_map.clear()
+        
+        for item in [
+            stretcher,
+            options_button
+        ]:
path: spyder/plugins/variableexplorer/widgets/texteditor.py
text: @user1:
```suggestion
        for item in [stretcher, options_button]:
```

Style improvement.

diff_hunk: @@ -49,6 +85,13 @@ def __init__(self, text, title='', parent=None, readonly=False):
         self.layout = QVBoxLayout()
         self.setLayout(self.layout)
 
+        self.toolbar = self.create_toolbar(
+                TextEditorWidgets.Toolbar,
+                register=False
+            )
path: spyder/plugins/variableexplorer/widgets/texteditor.py
text: @user1:
```suggestion
            TextEditorWidgets.Toolbar,
            register=False
        )
```

Style improvement.

diff_hunk: @@ -247,12 +257,29 @@ def do_nothing():
             triggered=self.refresh_editor,
             register_action=False
         )
+
+        self.close_action = self.create_action(
+            name=ObjectExplorerActions.Close,
+            icon=self.create_icon('close_pane'),
+            text=_('Close'),
+            triggered=self.reject,
+            shortcut=self.get_shortcut(ObjectExplorerActions.Close),
+            register_shortcut=True
+        )
+        self.register_shortcut_for_widget(name='close', triggered=self.reject)
+
         self.refresh_action.setEnabled(self.data_function is not None)
 
         self.show_cols_submenu = self.create_menu(
             ObjectExplorerMenus.Options,
             register=False
         )
+        for item in [self.close_action]:
+            self.add_item_to_menu(
+                item,
+                menu=self.show_cols_submenu,
+                section=ObjectExplorerOptionsMenuSections.Close,)
path: spyder/plugins/variableexplorer/widgets/objectexplorer/objectexplorer.py
text: @user1:
```suggestion
                section=ObjectExplorerOptionsMenuSections.Close
            )
```

Style improvement.

diff_hunk: @@ -1761,6 +1765,17 @@ def __init__(self, parent, data, namespacebrowser=None,
             register_action=None
         )
 
+        self.close_action = self.create_action(
+            name=CollectionsEditorActions.Close,
+            icon=self.create_icon('close_pane'),
+            text=_('Close'),
+            triggered=self.close_window,
+            shortcut=self.get_shortcut(CollectionsEditorActions.Close),
+            register_shortcut=True
+        )
+        self.register_shortcut_for_widget(
+            name='close', triggered=self.close_window)
path: spyder/widgets/collectionseditor.py
text: @user1:
```suggestion
            name='close', triggered=self.close_window
        )
```

Style improvement.

diff_hunk: @@ -1783,6 +1798,23 @@ def __init__(self, parent, data, namespacebrowser=None,
                 section=CollectionsEditorToolbarSections.AddDelete
             )
 
+        options_menu = self.create_menu(
+            CollectionsEditorMenus.Options,
+            register=False
+        )
+
path: spyder/widgets/collectionseditor.py
text: @user1:
```suggestion
```

No need of this line.