Repository: spyder-ide/spyder

Issue:
Part of file switcher always outside of screen when full-screen
## Description of your problem

**What steps will reproduce the problem?**

1. Opening file switcher with shortcut or menu when Spyder in full-screen mode or expanded

**What is the expected output? What do you see instead?**

I expect the entire file switcher window to be visible. Here's a screenshot of what happens:

![image](https://cloud.githubusercontent.com/assets/20071483/25477257/32664e22-2b3d-11e7-8074-6b15d52e39dd.png)

**Please provide any additional information below**
The file switcher window appears to have a more or less fixed position relative to the editor. When I reduce Spyder to half of screen, it looks like this:

![image](https://cloud.githubusercontent.com/assets/20071483/25478158/5060e5f6-2b40-11e7-9efa-55a6b1204d06.png)

## Versions and main components

* Spyder Version: 3.1.3
* Python Version: 3.6.0
* Qt Version: 5.6.2
* PyQt Version: 5.6.0
* Operating system: OS X El Capitan 10.11.16 (Retina)


## Dependencies

Please go to the menu entry `Help > Optional Dependencies` (or
`Help > Dependencies`), press the button `Copy to clipboard`
and paste the contents below:

jedi =0.9.0      :  0.9.0 (OK)
matplotlib >=1.0 :  2.0.0 (OK)
nbconvert >=4.0  :  5.1.1 (OK)
numpy >=1.7      :  1.12.1 (OK)
pandas >=0.13.1  :  0.19.2 (OK)
pep8 >=0.6       :  1.7.0 (OK)
psutil >=0.3     :  5.2.2 (OK)
pyflakes >=0.6.0 :  1.5.0 (OK)
pygments >=2.0   :  2.2.0 (OK)
pylint >=0.25    :  1.6.4 (OK)
qtconsole >=4.2.0:  4.3.0 (OK)
rope >=0.9.4     :  0.9.4-1 (OK)
sphinx >=0.6.6   :  1.5.4 (OK)
sympy >=0.7.3    :  1.0 (OK)


Discussion:
Thanks for reporting. This is a very odd bug indeed.

Resolving patch:
diff --git a/spyder/widgets/fileswitcher.py b/spyder/widgets/fileswitcher.py
--- a/spyder/widgets/fileswitcher.py
+++ b/spyder/widgets/fileswitcher.py
@@ -140,12 +140,6 @@ def recurse_level(level_idx):
                 short_form = sample_toks[0]
             elif s == 2:
                 short_form = sample_toks[0] + sep + sample_toks[1]
-            elif s == 3:
-                short_form = (sample_toks[0] + sep + sample_toks[1] +
-                              sep + sample_toks[2])
-            elif s == 4:
-                short_form = (sample_toks[0] + sep + sample_toks[1] +
-                              sep + sample_toks[2] + sep + sample_toks[3])
             else:
                 short_form = "..." + sep + sample_toks[s-1]
             for idx in level_idx:
@@ -180,13 +174,7 @@ def recurse_level(level_idx):
                 short_form = sample_toks[0]
             elif k == 2:
                 short_form = sample_toks[0] + sep + sample_toks[1]
-            elif k == 3:
-                short_form = (sample_toks[0] + sep + sample_toks[1] + sep +
-                              sample_toks[2])
-            elif k == 4:
-                short_form = (sample_toks[0] + sep + sample_toks[1] + sep +
-                              sample_toks[2] + sep + sample_toks[3])
-            else:  # k > 4
+            else:  # k > 2
                 short_form = sample_toks[0] + "..." + sep + sample_toks[k-1]
             for idx in group.keys():
                 new_path_list[idx] += short_form + (sep if k > 0 else '')
@@ -240,6 +228,7 @@ class FileSwitcher(QDialog):
     # FILE_MODE is for a list of files, SYMBOL_MODE if for a list of symbols
     # in a given file when using the '@' symbol.
     FILE_MODE, SYMBOL_MODE = [1, 2]
+    MAX_WIDTH = 600
 
     def __init__(self, parent, plugin, tabs, data, icon):
         QDialog.__init__(self, parent)
@@ -416,6 +405,7 @@ def set_dialog_position(self):
                    parent.menuBar().geometry().height() + 1)
         else:
             top = self.plugins_tabs[0][0].tabBar().geometry().height() + 1
+
         while parent:
             geo = parent.geometry()
             top += geo.top()
@@ -424,12 +414,11 @@ def set_dialog_position(self):
 
         self.move(left, top)
 
-    def fix_size(self, content, extra=50):
+    def get_item_size(self, content):
         """
-        Adjusts the width and height of the file switcher,
-        based on its content.
+        Get the max size (width and height) for the elements of a list of
+        strings as a QLabel.
         """
-        # Update size of dialog based on longest shortened path
         strings = []
         if content:
             for rich_text in content:
@@ -438,20 +427,34 @@ def fix_size(self, content, extra=50):
                 strings.append(label.text())
                 fm = label.fontMetrics()
 
-            # Max width
-            max_width = max([fm.width(s) * 1.3 for s in strings])
-            self.list.setMinimumWidth(max_width + extra)
+            return (max([fm.width(s) * 1.3 for s in strings]), fm.height())
 
-            # Max height
-            if len(strings) < 8:
-                max_entries = len(strings)
+    def fix_size(self, content):
+        """
+        Adjusts the width and height of the file switcher
+        based on the relative size of the parent and content.
+        """
+        # Update size of dialog based on relative size of the parent
+        if content:
+            width, height = self.get_item_size(content)
+
+            # Width
+            parent = self.parent()
+            relative_width = parent.geometry().width() * 0.65
+            if relative_width > self.MAX_WIDTH:
+                relative_width = self.MAX_WIDTH
+            self.list.setMinimumWidth(relative_width)
+
+            # Height
+            if len(content) < 8:
+                max_entries = len(content)
             else:
                 max_entries = 8
-            max_height = fm.height() * max_entries * 2.5
+            max_height = height * max_entries * 2.5
             self.list.setMinimumHeight(max_height)
 
-            # Set position according to size
-            self.set_dialog_position()
+            # Resize
+            self.list.resize(relative_width, self.list.height())
 
     # --- Helper methods: List widget
     def count(self):
@@ -481,9 +484,13 @@ def previous_row(self):
             self.select_row(-1)
             return
         prev_row = self.current_row() - 1
-        if prev_row == 0:
+        if prev_row >= 0:
+            title = self.list.item(prev_row).text()
+        else:
+            title = ''
+        if prev_row == 0 and '</b></big><br>' in title:
             self.list.scrollToTop()
-        elif '</b></big><br>' in self.list.item(prev_row).text():
+        elif '</b></big><br>' in title:
             # Select the next previous row, the one following is a title
             self.select_row(-2)
         else:
@@ -609,6 +616,10 @@ def setup_file_list(self, filter_text, current_path):
         scores = get_search_scores(filter_text, self.filenames,
                                    template="<b>{0}</b>")
 
+        # Get max width to determine if shortpaths should be used
+        max_width = self.get_item_size(paths)[0]
+        self.fix_size(paths)
+
         # Build the text that will appear on the list widget
         for index, score in enumerate(scores):
             text, rich_text, score_value = score
@@ -617,7 +628,10 @@ def setup_file_list(self, filter_text, current_path):
                 if trying_for_line_number:
                     text_item += " [{0:} {1:}]".format(self.line_count[index],
                                                        _("lines"))
-                text_item += u"<br><i>{0:}</i>".format(short_paths[index])
+                if max_width > self.list.width():
+                    text_item += u"<br><i>{0:}</i>".format(short_paths[index])
+                else:
+                    text_item += u"<br><i>{0:}</i>".format(paths[index])
                 results.append((score_value, index, text_item))
 
         # Sort the obtained scores and populate the list widget
@@ -658,7 +672,6 @@ def setup_file_list(self, filter_text, current_path):
             self.set_current_row(self.filtered_path.index(current_path))
         elif self.filtered_path:
             self.set_current_row(0)
-        self.fix_size(short_paths, extra=100)
 
         # If a line number is searched look for it
         self.line_number = line_number
@@ -673,10 +686,13 @@ def setup_symbol_list(self, filter_text, current_path):
         oedata = self.get_symbol_list()
         icons = get_python_symbol_icons(oedata)
 
-        # The list of shorten paths here is needed in order to have the same
-        # point of measurement for the list widget width as in the file list
+        # The list of paths here is needed in order to have the same
+        # point of measurement for the list widget size as in the file list
         # See issue 4648
-        short_paths = shorten_paths(self.paths, self.save_status)
+        paths = self.paths
+        # Update list size
+        self.fix_size(paths)
+
         symbol_list = process_python_symbol_data(oedata)
         line_fold_token = [(item[0], item[2], item[3]) for item in symbol_list]
         choices = [item[1] for item in symbol_list]
@@ -718,9 +734,6 @@ def setup_symbol_list(self, filter_text, current_path):
         # That's why this line is commented!
         # self.set_current_row(0)
 
-        # Update list size
-        self.fix_size(short_paths, extra=100)
-
     def setup(self):
         """Setup list widget content."""
         if len(self.plugins_tabs) == 0:
@@ -730,7 +743,6 @@ def setup(self):
         self.list.clear()
         current_path = self.current_path
         filter_text = self.filter_text
-        self.set_dialog_position()
 
         # Get optional line or symbol to define mode and method handler
         trying_for_symbol = ('@' in self.filter_text)
@@ -742,6 +754,9 @@ def setup(self):
             self.mode = self.FILE_MODE
             self.setup_file_list(filter_text, current_path)
 
+        # Set position according to size
+        self.set_dialog_position()
+
     def add_plugin(self, plugin, tabs, data, icon):
         """Add a plugin to display its files."""
         self.plugins_tabs.append((tabs, plugin))


Test patch:
diff --git a/spyder/app/tests/test_mainwindow.py b/spyder/app/tests/test_mainwindow.py
--- a/spyder/app/tests/test_mainwindow.py
+++ b/spyder/app/tests/test_mainwindow.py
@@ -51,6 +51,8 @@
 # Test for PyQt 5 wheels
 PYQT_WHEEL = PYQT_VERSION > '5.6'
 
+# Temporary directory
+TEMP_DIRECTORY = tempfile.gettempdir()
 
 #==============================================================================
 # Utility functions
@@ -126,7 +128,7 @@ def close_window():
 # Tests
 #==============================================================================
 # IMPORTANT NOTE: Please leave this test to be the first one here to
-# avoid possible timeouts in Appyevor
+# avoid possible timeouts in Appveyor
 @flaky(max_runs=3)
 @pytest.mark.skipif(os.name != 'nt' or not PY2,
                     reason="It times out on Linux and Python 3")
@@ -768,5 +770,57 @@ def test_varexp_magic_dbg(main_window, qtbot):
     assert shell._control.toHtml().count('img src') == 1
 
 
+@flaky(max_runs=3)
+def test_fileswitcher(main_window, qtbot):
+    """Test the use of shorten paths when necessary in the fileswitcher."""
+    # Load tests files
+    dir_b = osp.join(TEMP_DIRECTORY, 'temp_dir_a', 'temp_b')
+    filename_b =  osp.join(dir_b, 'c.py')
+    if not osp.isdir(dir_b):
+        os.makedirs(dir_b)
+    if not osp.isfile(filename_b):
+        file_c = open(filename_b, 'w+')
+        file_c.close()
+    if PYQT5:
+        dir_d = osp.join(TEMP_DIRECTORY, 'temp_dir_a', 'temp_c', 'temp_d', 'temp_e')
+    else:
+        dir_d = osp.join(TEMP_DIRECTORY, 'temp_dir_a', 'temp_c', 'temp_d')
+        dir_e = osp.join(TEMP_DIRECTORY, 'temp_dir_a', 'temp_c', 'temp_dir_f', 'temp_e')
+        filename_e = osp.join(dir_e, 'a.py')
+        if not osp.isdir(dir_e):
+            os.makedirs(dir_e)
+        if not osp.isfile(filename_e):
+            file_e = open(filename_e, 'w+')
+            file_e.close()
+    filename_d =  osp.join(dir_d, 'c.py')
+    if not osp.isdir(dir_d):
+        os.makedirs(dir_d)
+    if not osp.isfile(filename_d):
+        file_d = open(filename_d, 'w+')
+        file_d.close()
+    main_window.editor.load(filename_b)
+    main_window.editor.load(filename_d)
+
+    # Assert that all the path of the file is shown
+    main_window.open_fileswitcher()
+    if os.name == 'nt':
+        item_text = main_window.fileswitcher.list.currentItem().text().replace('\\', '/').lower()
+        dir_d = dir_d.replace('\\', '/').lower()
+    else:
+        item_text = main_window.fileswitcher.list.currentItem().text()
+    assert dir_d in item_text
+
+    # Resize Main Window to a third of its width
+    size = main_window.window_size
+    main_window.resize(size.width() / 3, size.height())
+    main_window.open_fileswitcher()
+
+    # Assert that the path shown in the fileswitcher is shorter
+    if PYQT5:
+       main_window.open_fileswitcher()
+       item_text = main_window.fileswitcher.list.currentItem().text()
+       assert '...' in item_text
+
+
 if __name__ == "__main__":
     pytest.main()