Repository: psychopy/psychopy

Issue:
Clicking the "close tab" control in Builder deletes routine
Surprising discovery today: I have a task with a bunch of routines I'm editing, and decide I have too many tabs open, so I click the little "X" on a tab to close it. Builder happily deleted the routine.

There's no confirmation dialog, no tooltip when hovering over the "X," your routine is just gone.

Fortunately! Undo works in this case and you can get back out if you think to do that.

I guess a confirmation dialog would help here, but really, that X is for closing tabs which should not be a destructive operation. Probably, tabs should open when you click on routines and close when you click that "X" and deleting routines should just live in the right-click menu.


Discussion:
Yes, this was brought up in a single mega-issue #2132 but I'm happy with it being here too.

Achieving what you want (close a tab and then bring it back later) is going to be harder to implement because I'm not sure where one would 're-open' the Routine. 

But providing a warning that this deletes the Routine is possible or the other option discussed was whether not to have the close-tab icon at all to prevent the mistake you've come across.
You'd re-open the Routine's tab by clicking (or double-clicking) on it in the Flow pane.

The other reasonable option would be to not have tabs at all. The top pane would be the currently-selected Routine, and you'd indicate which Routine is selected in the Flow pane (and probably with a title in the top pane).

Really, the tab bar and flow pane are redundant ways to navigate between Routines.
I think that isn't quite true. You might have Routines in the study that never appear in the flow - it's easy to swap a routine in or out that way.

We could maintain a list of all the Routines in the Experiment menu so that they can be brought back when 'closed' and when not on the Flow I guess but that's a bit more effort than simply warning people that 'close' == 'delete'
My mental model aligns a lot with @njvack here, that close != delete in this case. I've also resorted to undo a few times. We may be in the minority, though. But i think there's a chance to improve the way you work with routines in a way where there is just one way to do everything. PsychoPy's routine flow is already the best out there AFAIK, so I just put this up here for inspiration.

1. To insert an existing routine or create a new: Click the relevant location in the flow, then a context menu appears with options 'new' followed by a list of existing routines. CTRL+drag would also be awesome :-)
2. To edit: Clicking the relevant routine.
3. Insert uninstantiated routine: select from the list (or graphical section) called 'unused routines'.
4. Delete: Right click and press 'delete'. If this is the only instance, perhaps a message would appear saying something to the effect that "This routine has been sent to 'unused routines', from where you can still access it".
5. Now we just need a way to delete unused routines. Easy if it's graphical (same colored boxes which are not in the exp flow).


@peircej How can you have routines that don't appear in the flow? It sounds useful but I don't know how to make that happen...
@njvack you go to the Experiment menu and Add Routine

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
@@ -1545,19 +1545,46 @@ def createNewRoutine(self, returnName=False):
             return routineName
 
     def onClosePane(self, event=None):
-        """Close the pane and remove the routine from the exp
+        """Close the pane and remove the routine from the exp.
         """
-        routine = self.GetPage(event.GetSelection()).routine
+        currentPage = self.GetPage(event.GetSelection())
+        routine = currentPage.routine
         name = routine.name
-        # update experiment object, namespace, and flow window (if this is
-        # being used)
-        if name in self.frame.exp.routines:
-            # remove names of the routine and its components from namespace
-            _nsp = self.frame.exp.namespace
-            for c in self.frame.exp.routines[name]:
-                _nsp.remove(c.params['name'].val)
-            _nsp.remove(self.frame.exp.routines[name].name)
-            del self.frame.exp.routines[name]
+
+        # name is not valid for some reason
+        if name not in self.frame.exp.routines:
+            event.Skip()
+            return
+
+        # check if the user wants a prompt
+        showDlg = self.app.prefs.builder.get('confirmRoutineClose', False)
+        if showDlg:
+            # message to display
+            msg = _translate(
+                "Do you want to remove routine '{}' from the experiment?")
+
+            # dialog asking if the user wants to remove the routine
+            dlg = wx.MessageDialog(
+                self,
+                _translate(msg).format(name),
+                _translate('Remove routine?'),
+                wx.YES_NO | wx.NO_DEFAULT | wx.CENTRE | wx.STAY_ON_TOP)
+
+            # show the dialog and get the response
+            dlgResult = dlg.ShowModal()
+            dlg.Destroy()
+
+            if dlgResult == wx.ID_NO:  # if NO, stop the tab from closing
+                event.Veto()
+                return
+
+        # remove names of the routine and its components from namespace
+        _nsp = self.frame.exp.namespace
+        for c in self.frame.exp.routines[name]:
+            _nsp.remove(c.params['name'].val)
+        _nsp.remove(self.frame.exp.routines[name].name)
+        del self.frame.exp.routines[name]
+
         if routine in self.frame.exp.flow:
             self.frame.exp.flow.removeComponent(routine)
             self.frame.flowPanel.draw()


Test patch: