Repository: spyder-ide/spyder

Issue:
Make Ctrl+Tab to navigate between tabs in Most Recently Used (MRU) order
_From [y.bonne...@gmail.com](https://code.google.com/u/111370336049651795761/) on 2009-09-13T12:00:46Z_

Ideally, Ctrl + Tab could be used to navigate between tabs in Most 
Recently Used order.

You can see MRU in browsers or Notepad++ - very convenient to repeatedly 
switch between two tabs.

The Matlabesque Ctrl+ PgUP/PgDwn becomes painful if the tabs are not 
close, and the Ctrl + Tab is easy to remember, by analogy with the Alt + 
Tab.

_Original issue: http://code.google.com/p/spyderlib/issues/detail?id=43_



Discussion:
_From [pierre.raybaut](https://code.google.com/u/pierre.raybaut/) on 2009-09-14T08:26:25Z_

**Status:** Accepted  
  **Labels:** -Type-Defect Type-Enhancement  

_From [pierre.raybaut](https://code.google.com/u/pierre.raybaut/) on 2009-09-18T11:11:36Z_

**Status:** Fixed  

_From [y.bonne...@gmail.com](https://code.google.com/u/111370336049651795761/) on 2009-09-18T15:38:08Z_

Trying the brand new version. Begins to look good.

About MRU, we are nearly there. 

In tab1, I click tab3, then I click tab2.
Then 2 times Ctrl + Tab -> back to tab1, everything is fine.
Then from tab1, one time Ctrl + Tab -> back to 2. It should be back to tab3, my most 
recently used tab before my second access to tab1.

Spyder acts as is the history was updated on "click on tab to access tab" events, 
but not on "Ctrl + Tab to access tab" events.

_From [pierre.raybaut](https://code.google.com/u/pierre.raybaut/) on 2009-09-19T01:38:13Z_

Actually no, it should be back to 2: anyway, that's the behavior I have with Google 
Chrome for example.

_From [y.bonne...@gmail.com](https://code.google.com/u/111370336049651795761/) on 2009-09-19T02:44:35Z_

Damn, you're right. So I formulated the problem badly. One more time :

In tab1, I click tab3, then I click tab2.
Then 2 times Ctrl + Tab -> back to tab1, everything is fine.
Then from tab1, one time Ctrl + Tab -> back to 2. Normal.
Then from tab2, one time Ctrl + Tab -> we go to tab3. We should be going to tab1, 
the MRU tab.

Put more simply : with MRU, if you keep on Ctrl + Tab-ing, you should switch between 
your two more recent tabs - veeery handy. In this version, this doesn't happen like 
this. 

... Hope I got this right this time.

_From [pierre.raybaut](https://code.google.com/u/pierre.raybaut/) on 2009-09-22T08:32:18Z_

I understand what behavior you're expecting, but again, when I'm doing this on
Firefox, I get the same behavior as Spyder. And this is very close to the Windows
Alt+Tab feature too.

_From [y.bonne...@gmail.com](https://code.google.com/u/111370336049651795761/) on 2009-09-22T11:16:46Z_

The behavior I'm thinking about is indeed Windows' Alt+Tab but it's not what there 
is in Spyder, and it's different again from what I get from "safe mode, no plugins" 
Firefox tabs (it just cycle trough tabs on my machine).

I realize my example was needlessly convoluted. Sorry for the noise, I'll try again, 
with Windows' Alt+Tab as a benchmark :
- in Windows, open 3 apps successively (apps 1, 2 and 3 respectively). Then Alt+Tab, 
  then another Alt+Tab. What happens is that you cycle between apps 2 and 3.
- in Spyder, click on tab 1, then 2 then 3. Then Ctrl+Tab, then another Ctrl+Tab. 
  What happens is that you go to tabs 2, then 1, then 3 etc.

I hope we are not too far here in "personal preference" territory. The Alt+Tab 
behavior is for me the Right One because it allows back and forth fast between the 
recent apps (tabs) (which is a Good Thing), and that the current behavior doesn't 
allow this.

_From [pierre.raybaut](https://code.google.com/u/pierre.raybaut/) on 2009-09-22T12:03:54Z_

Ok, so in other words, you simply want that when Ctrl+Tab is pressed, the last tab is
selected -- it means that pressing several times Ctrl+Tab won't cycle through
available tabs (that is the behavior I would personnaly expect) because it would
simply cycle between the last two selected tabs.
Even if I was at first thinking of Firefox or Google Chrome's behavior, I understand
that to simply select the last tab would be useful if one has to switch between two
opened files several times.

BTW, when you have several files opened at the same time, remember that the "Opened
files" feature may be useful (that is the toolbox on the right panel below the "Class
browser") to switch quickly to another tab.
The horizontal/vertical splitting feature may also be useful in that context.

_From [y.bonne...@gmail.com](https://code.google.com/u/111370336049651795761/) on 2009-09-22T12:57:58Z_

Not exactly. I was using the "cycle between the two last tabs" just because for me, 
it is a sign that the "Ctrl+Tab in Spyder like Alt+Tab in Windows" is not there. 
Actually, I was thinking about Windows' full app switching feature : if you keep on 
Alt+Tab several times, you switch between the two most recents apps, if you Alt+Tab, 
Tab, Tab, you go to the next apps in your "focused apps" history.

Note that there is no need to have a special popup (as Windows does) and to switch 
on Alt release - you can switch directly on each Tab, it's the order that matters.

I should have begun with the Alt+Tab analogy, I think it clears things up. Others 
examples on the top of my mind are Tab Mix Plus addon in Firefox, Np++, or the Ido 
mode in Emacs.

Maybe the gain is not worth the pain? If you think so, maybe then a simple 
"alternate 2 most recent tabs" would be doable?

_From [bdholmes...@gmail.com](https://code.google.com/u/112482891714613146382/) on 2013-08-01T01:26:32Z_

Completely agree with Y. Bonnemay.  The aim should be for Ctrl+Tab in Spyder to work like Alt+Tab in Ubuntu or Windows.  He explained it best:

"If you keep on Alt+Tab several times, you switch between the two most recents apps. If you Alt+Tab, Tab, Tab, you go to the next apps in your history."

Quickly toggling back and forth between two most recently used scripts in Spyder is not possible without lifting off the keyboard and using the mouse.  This is less efficient and productive.  This is also how tab switching works with most other applications I've used.

_From [ccordoba12](https://code.google.com/u/ccordoba12/) on 2013-08-09T20:02:40Z_

We'll see what we can to improve the situation in our next release because current Tab behavior also confuses me.

**Status:** Accepted  
  **Labels:** Cat-Editor MS-v2.4  

_From [kinnison...@gmail.com](https://code.google.com/u/104685941974947235171/) on 2013-10-11T08:58:13Z_

It always surprises me when such a detailed discussion occurs regarding a question with multiple correct answers and nobody mentions config options.  If at all feasible, the tabbing behavior should clearly be configurable in the preferences.  Ideally through the keyboard shortcuts.

As things are now, all I'd need is the ability to rewire the alt tab and control page up/down shortcuts.  That should be simple, right?  Any future variants of the MRU approach can be added in as further optional keyboard shortcuts.

Any roadmap for this?  Ctrl-tab in horizontal order is useless IMO.  MRU would make a massive difference to Editor productivity.

@blink1073, could you take a look at this one?

Sure, seems feasible to at least provide the MRU capability.


Resolving patch:
diff --git a/spyder/config/main.py b/spyder/config/main.py
--- a/spyder/config/main.py
+++ b/spyder/config/main.py
@@ -405,8 +405,8 @@
               'editor/conditional breakpoint': 'Shift+F12',
               'editor/run selection': "F9",
               'editor/go to line': 'Ctrl+L',
-              'editor/go to previous file': 'Ctrl+Tab',
-              'editor/go to next file': 'Ctrl+Shift+Tab',
+              'editor/go to previous file': 'Ctrl+Shift+Tab',
+              'editor/go to next file': 'Ctrl+Tab',
               'editor/new file': "Ctrl+N",
               'editor/open last closed':"Ctrl+Shift+T",
               'editor/open file': "Ctrl+O",
@@ -658,7 +658,7 @@
 #    or if you want to *rename* options, then you need to do a MAJOR update in
 #    version, e.g. from 3.0.0 to 4.0.0
 # 3. You don't need to touch this value if you're just adding a new option
-CONF_VERSION = '33.0.0'
+CONF_VERSION = '33.1.0'
 
 # Main configuration instance
 try:
diff --git a/spyder/widgets/editor.py b/spyder/widgets/editor.py
--- a/spyder/widgets/editor.py
+++ b/spyder/widgets/editor.py
@@ -16,6 +16,7 @@
 import os
 import os.path as osp
 import sys
+from collections import MutableSequence
 
 # Third party imports
 from qtpy import is_pyqt46
@@ -25,11 +26,11 @@
 from qtpy.QtGui import QFont
 from qtpy.QtWidgets import (QAction, QApplication, QHBoxLayout, QMainWindow,
                             QMessageBox, QMenu, QSplitter, QVBoxLayout,
-                            QWidget)
+                            QWidget, QListWidget, QListWidgetItem)
 
 # Local imports
 from spyder.config.base import _, DEBUG, STDERR, STDOUT
-from spyder.config.gui import config_shortcut
+from spyder.config.gui import config_shortcut, get_shortcut
 from spyder.config.utils import (get_edit_filetypes, get_edit_filters,
                                  get_filter)
 from spyder.py3compat import qbytearray_to_str, to_text_string
@@ -274,6 +275,120 @@ def breakpoints_changed(self):
             self.save_breakpoints.emit(self.filename, repr(breakpoints))
 
 
+class StackHistory(MutableSequence):
+    """Handles editor stack history.
+
+    Works as a list of numbers corresponding to tab indexes.
+    Internally elements are saved using objects id's.
+    """
+
+    def __init__(self, editor):
+        self.history = list()
+        self.id_list = list()
+        self.editor = editor
+
+    def _update_id_list(self):
+        """Update list of corresponpding ids and tabs."""
+        self.id_list = [id(self.editor.tabs.widget(_i))
+                        for _i in range(self.editor.tabs.count())]
+
+    def refresh(self):
+        """Remove editors that are not longer open."""
+        self._update_id_list()
+        for _id in self.history[:]:
+            if _id not in self.id_list:
+                self.history.remove(_id)
+
+    def __len__(self):
+        return len(self.history)
+
+    def __getitem__(self, i):
+        return self.id_list.index(self.history[i])
+
+    def __delitem__(self, i):
+        del self.history[i]
+
+    def __setitem__(self, i, v):
+        _id = id(self.editor.tabs.widget(v))
+        self.history[i] = _id
+
+    def __str__(self):
+        return str(list(self))
+
+    def insert(self, i, tab_index):
+        """Insert the widget (at tab index) in the position i (index)."""
+        _id = id(self.editor.tabs.widget(tab_index))
+        self.history.insert(i, _id)
+
+    def remove(self, tab_index):
+        """Remove the widget at the corresponding tab_index."""
+        _id = id(self.editor.tabs.widget(tab_index))
+        if _id in self.history:
+            self.history.remove(_id)
+
+
+class TabSwitcherWidget(QListWidget):
+    """Show tabs in mru order and change between them."""
+
+    def __init__(self, parent, stack_history, tabs):
+        QListWidget.__init__(self, parent)
+        self.setWindowFlags(Qt.SubWindow | Qt.FramelessWindowHint)
+
+        self.editor = parent
+        self.stack_history = stack_history
+        self.tabs = tabs
+
+        self.setSelectionMode(QListWidget.SingleSelection)
+        self.itemActivated.connect(self.item_selected)
+
+        self.id_list = []
+        self.load_data()
+        size = CONF.get('main', 'completion/size')
+        self.resize(*size)
+        self.set_dialog_position()
+        self.setCurrentRow(0)
+
+    def load_data(self):
+        """Fill ListWidget with the tabs texts.
+
+        Add elements in inverse order of stack_history.
+        """
+
+        for index in reversed(self.stack_history):
+            text = self.tabs.tabText(index)
+            text = text.replace('&', '')
+            item = QListWidgetItem(ima.icon('FileIcon'), text)
+            self.addItem(item)
+
+    def item_selected(self, item=None):
+        """Change to the selected document and hide this widget."""
+        if item is None:
+            item = self.currentItem()
+
+        # stack history is in inverse order
+        index = self.stack_history[-(self.currentRow()+1)]
+
+        self.editor.set_stack_index(index)
+        self.editor.current_changed(index)
+        self.hide()
+
+    def select_row(self, steps):
+        """Move selected row a number of steps.
+
+        Iterates in a cyclic behaviour.
+        """
+        row = (self.currentRow() + steps) % self.count()
+        self.setCurrentRow(row)
+
+    def set_dialog_position(self):
+        """Positions the tab switcher in the top-center of the editor."""
+        parent = self.parent()
+        left = parent.geometry().width()/2 - self.width()/2
+        top = 0
+
+        self.move(left, top + self.tabs.tabBar().geometry().height())
+
+
 class EditorStack(QWidget):
     reset_statusbar = Signal()
     readonly_changed = Signal(bool)
@@ -335,8 +450,9 @@ def __init__(self, parent, actions):
 #        self.previous_btn = None
 #        self.next_btn = None
         self.tabs = None
+        self.tabs_switcher = None
 
-        self.stack_history = []
+        self.stack_history = StackHistory(self)
 
         self.setup_editorstack(parent, layout)
 
@@ -445,9 +561,10 @@ def create_shortcuts(self):
                                     parent=self)
         gotoline = config_shortcut(self.go_to_line, context='Editor',
                                    name='Go to line', parent=self)
-        tab = config_shortcut(self.go_to_previous_file, context='Editor',
+        tab = config_shortcut(lambda: self.tab_navigation_mru(forward=False),
+                              context='Editor',
                               name='Go to previous file', parent=self)
-        tabshift = config_shortcut(self.go_to_next_file, context='Editor',
+        tabshift = config_shortcut(self.tab_navigation_mru, context='Editor',
                                    name='Go to next file', parent=self)
         run_selection = config_shortcut(self.run_selection, context='Editor',
                                         name='Run selection', parent=self)
@@ -567,6 +684,8 @@ def setup_editorstack(self, parent, layout):
         self.tabs.set_close_function(self.close_file)
         self.tabs.setMovable(True)
 
+        self.stack_history.refresh()
+
         if hasattr(self.tabs, 'setDocumentMode') \
            and not sys.platform == 'darwin':
             # Don't set document mode to true on OSX because it generates
@@ -1463,16 +1582,11 @@ def current_changed(self, index, set_focus=True):
             self.reset_statusbar.emit()
         self.opened_files_list_changed.emit()
 
-        # Index history management
-        id_list = [id(self.tabs.widget(_i))
-                   for _i in range(self.tabs.count())]
-        for _id in self.stack_history[:]:
-            if _id not in id_list:
-                self.stack_history.pop(self.stack_history.index(_id))
-        current_id = id(self.tabs.widget(index))
-        while current_id in self.stack_history:
-            self.stack_history.pop(self.stack_history.index(current_id))
-        self.stack_history.append(current_id)
+        self.stack_history.refresh()
+
+        while index in self.stack_history:
+            self.stack_history.remove(index)
+        self.stack_history.append(index)
         if DEBUG_EDITOR:
             print("current_changed:", index, self.data[index].editor, end=' ', file=STDOUT)
             print(self.data[index].editor.get_document_id(), file=STDOUT)
@@ -1487,32 +1601,26 @@ def _get_previous_file_index(self):
             last = len(self.stack_history)-1
             w_id = self.stack_history.pop(last)
             self.stack_history.insert(0, w_id)
-            last_id = self.stack_history[last]
-            for _i in range(self.tabs.count()):
-                if id(self.tabs.widget(_i)) == last_id:
-                    return _i
-
-    def go_to_previous_file(self):
-        """Ctrl+Tab"""
-        prev_index = self._get_previous_file_index()
-        if prev_index is not None:
-            self.set_stack_index(prev_index)
-        elif len(self.stack_history) == 0 and self.get_stack_count():
-            self.stack_history = [id(self.tabs.currentWidget())]
-
-    def go_to_next_file(self):
-        """Ctrl+Shift+Tab"""
-        if len(self.stack_history) > 1:
-            last = len(self.stack_history)-1
-            w_id = self.stack_history.pop(0)
-            self.stack_history.append(w_id)
-            last_id = self.stack_history[last]
-            for _i in range(self.tabs.count()):
-                if id(self.tabs.widget(_i)) == last_id:
-                    self.set_stack_index(_i)
-                    break
-        elif len(self.stack_history) == 0 and self.get_stack_count():
-            self.stack_history = [id(self.tabs.currentWidget())]
+
+            return self.stack_history[last]
+
+    def tab_navigation_mru(self, forward=True):
+        """
+        Tab navigation with "most recently used" behaviour.
+
+        It's fired when pressing 'go to previous file' or 'go to next file'
+        shortcuts.
+
+        forward:
+            True: move to next file
+            False: move to previous file
+        """
+        if self.tabs_switcher is None or not self.tabs_switcher.isVisible():
+            self.tabs_switcher = TabSwitcherWidget(self, self.stack_history,
+                                                   self.tabs)
+            self.tabs_switcher.show()
+
+        self.tabs_switcher.select_row(1 if forward else -1)
 
     def focus_changed(self):
         """Editor focus has changed"""
@@ -1670,6 +1778,7 @@ def modification_changed(self, state=None, index=None, editor_id=None):
         # Refreshing eol mode
         eol_chars = finfo.editor.get_line_separator()
         self.refresh_eol_chars(eol_chars)
+        self.stack_history.refresh()
 
     def refresh_eol_chars(self, eol_chars):
         os_name = sourcecode.get_os_name_from_eol_chars(eol_chars)
@@ -1979,6 +2088,25 @@ def dropEvent(self, event):
                 editor.insert_text( source.text() )
         event.acceptProposedAction()
 
+    def keyReleaseEvent(self, event):
+        """Reimplement Qt method.
+
+        Handle "most recent used" tab behavior,
+        When ctrl is released and tab_switcher is visible, tab will be changed.
+        """
+        if self.tabs_switcher is not None and self.tabs_switcher.isVisible():
+            qsc = get_shortcut(context='Editor', name='Go to next file')
+
+            for key in qsc.split('+'):
+                key = key.lower()
+                if ((key == 'ctrl' and event.key() == Qt.Key_Control) or
+                   (key == 'alt' and event.key() == Qt.Key_Alt)):
+                        self.tabs_switcher.item_selected()
+                        self.tabs_switcher = None
+                        return
+
+        super(EditorStack, self).keyPressEvent(event)
+
 
 class EditorSplitter(QSplitter):
     def __init__(self, parent, plugin, menu_actions, first=False,


Test patch: