Repository: psychopy/psychopy

Issue:
Order of routine tabs is not preserved when re-opening Builder experiments
I've just started working with the Builder a bit, and it occurs to me that the routine tabs are always ordered alphabetically automatically when opening a Builder experiment, disregarding previous re-arrangements I had done before saving the experiment. I would like to have the routine tabs ordered the way they appear in the experiment, but right now I have to re-arrange them manually  that way every time I re-open the Builder.



Discussion:
Yes, this is one of those things that would be good but I've never cared enough to spend time on it. 
Two ways to go about it:
- detect the events thrown by the Routine panes (wx notebook pages) being moved and reorder each time that occurs
- when it comes to saving the experiment simply clear the list of Routines and recreate it by iterating through the notebook pages

I expect the second one will be easier to implement

Thanks for these hints. If nobody else is planning to work on this, I think I would try to implement the appropriate changes as soon as I'm finished with my staircase project. :) Because this was one of the things that made using the builder a bit unpleasant to me.


Resolving patch:
diff --git a/psychopy/app/builder/builder.py b/psychopy/app/builder/builder.py
--- a/psychopy/app/builder/builder.py
+++ b/psychopy/app/builder/builder.py
@@ -7,7 +7,7 @@
 Copyright (C) 2002-2018 Jonathan Peirce (C) 2019-2022 Open Science Tools Ltd.
 Distributed under the terms of the GNU General Public License (GPL).
 """
-
+import collections
 import os, sys
 import subprocess
 import webbrowser
@@ -1516,6 +1516,7 @@ def __init__(self, frame, id=-1):
         aui.AuiNotebook.__init__(self, frame, id,
             agwStyle=aui.AUI_NB_TAB_MOVE | aui.AUI_NB_CLOSE_ON_ACTIVE_TAB)
         self.Bind(aui.EVT_AUINOTEBOOK_PAGE_CLOSE, self.onClosePane)
+        self.Bind(aui.EVT_AUINOTEBOOK_END_DRAG, self.onMoveTab)
 
         # double buffered better rendering except if retina
 
@@ -1627,6 +1628,29 @@ def onClosePane(self, event=None):
             self.frame.flowPanel.draw()
         self.frame.addToUndoStack("REMOVE Routine `%s`" % (name))
 
+    def onMoveTab(self, evt=None):
+        """
+        After moving tabs around, sorts Routines in the Experiment accordingly
+        and marks experiment as changed.
+
+        Parameters
+        ----------
+        evt : wx.aui.AUI_NB_TAB_MOVE
+            Event generated by moving the tab (not used)
+        """
+
+        # Get tab names in order
+        names = []
+        for i in range(self.GetPageCount()):
+            names.append(self.GetPageText(i))
+        # Reorder routines in experiment to match tab order
+        routines = collections.OrderedDict()
+        for name in names:
+            routines[name] = self.frame.exp.routines[name]
+        self.frame.exp.routines = routines
+        # Set modified
+        self.frame.setIsModified(True)
+
     def increaseSize(self, event=None):
         self.appData['routineSize'] = min(
             self.routineMaxSize, self.appData['routineSize'] + 1)
@@ -1644,8 +1668,7 @@ def redrawRoutines(self):
         """
         currPage = self.GetSelection()
         self.removePages()
-        displayOrder = sorted(self.frame.exp.routines.keys())  # alphabetical
-        for routineName in displayOrder:
+        for routineName in self.frame.exp.routines:
             if isinstance(self.frame.exp.routines[routineName], (Routine, BaseStandaloneRoutine)):
                 self.addRoutinePage(
                     routineName, self.frame.exp.routines[routineName])
diff --git a/psychopy/experiment/_experiment.py b/psychopy/experiment/_experiment.py
--- a/psychopy/experiment/_experiment.py
+++ b/psychopy/experiment/_experiment.py
@@ -15,7 +15,7 @@
         which will call the .writeBody() methods from each component
     settings.SettingsComponent.writeEndCode()
 """
-
+import collections
 import os
 import codecs
 import xml.etree.ElementTree as xml
@@ -110,7 +110,7 @@ def __init__(self, prefs=None):
         self.name = ''
         self.filename = ''  # update during load/save xml
         self.flow = Flow(exp=self)  # every exp has exactly one flow
-        self.routines = {}
+        self.routines = collections.OrderedDict()
         # get prefs (from app if poss or from cfg files)
         if prefs is None:
             prefs = psychopy.prefs


Test patch: