Friday, May 30, 2014

Dude, where's my tab?

Well, rolling along here at ppqt central, we have the basics of a main window, now it is time to add the File menu and the actions it commands. First up is File>New. Easy-peasy one would think, since the _new() method is already in place and being used as part of initialization.

        self.file_menu = self.menu_bar.addMenu(_TR('Menu bar', '&File'))
        # Populate the File menu with actions.
        #  New -> _new()
        work = self.file_menu.addAction( _TR('File menu','&New') )
        work.setShortcut(C.CTL_N)
        work.setToolTip( _TR('File:New tooltip','Create a new, empty document') )
        work.triggered.connect(self._new)

Add a QMenu('File') (appropriately translated) to the application QMenuBar. To it, add an action 'New' and connect its signal to the proper slot. And this should work. So test it. Up comes the app and oooh! It has a File menu with only one item, New. And selecting that executes the code of the _new() method. I have verified this and also that each time File>New is selected, the app creates an empty Book with the names Untitled-0, Untitled-1, Untitled-2 and so on, and adds them to the edit tabset on the left side. And the tabset.addTab() method returns a tab index of 0, 1, 2 as expected.

Except ... the tabs don't appear. The added tabs are not shown; the only visible tab is the first one, for "Untitled-0". Wut?

The code for adding a New document is exactly the same as for adding an existing opened file, and that works. The unit test driver fakes up settings showing two previously opened files and the app opens them and they appear in separate tabs (as shown two posts back).

OK run the unit test and it opens two existing books. Now do New. Hmm. The debug printout reports "added Untitled-1 at index 1" even though there are two open books, so the name should be "Untitled-2" and the index, 2. Add the printout to the _open() method as well.

added realbook.txt at index 0
added small_book.txt at index 1
added Untitled-1 at index 1

No, that's not right. Should be Untitled-2 at index 2. It's like the _new() was happening in a completely different Mainwindow object, operating on a different book sequence number and different tabset. Could that be?

Yes it could be! The unit test driver created three MainWindow objects in sequence. The first is used to check that various settings are properly saved, like this:

settings.clear()
mw = mainwindow.MainWindow(settings)
...do things like mw.move(new position)...
mw.close() # force writing settings
...read values out of settings and assert things about them...
mw = None # destroy main window

And then another round of that: load the QSettings with fake info; create a new MainWindow object in mw; close it; look at settings values; assign mw=None.

Then finally do that one more time: load the QSettings object this time with two valid "previously-open" files; create mw=mainwindow.MainWindow(settings); call app._exec() and interact with the window.

If the first two rounds are commented out, so only the third MainWindow is created, it works: File>New makes a new file that appears in the edit tab bar.

Conclusion: somehow the QApplication is holding onto one or both of the first-created MainWindow objects. And it remembers how that (or those) objects created File menus. And somehow the File>New action is being directed to the local data owned by one of those zombie main windows, even though both of them had executed their .close() methods and all references to both had been overwritten in the Python code.

So: split the unit-test into two or three separate programs... boring.

Edit: On later consideration, I think the link is in the File menu QActions. Each QAction has a reference to the "slot" to handle its the triggered() signal. Each slot is specified using the main window's "self" pointer, e.g.

    act.triggered.connect(self._new)

That "self" value is a reference to the current instance of the MainWindow class, a QWidget derivative. Each time I created a MainWindow instance, it initialized by creating a QMenuBar and putting File>New and File>Open actions in it. Somehow the first of these "stuck" in the app's memory, and kept that object alive, not garbage-collected, even though I'd deleted every reference I had to it. Any use of the Mac OS menu bar's File>New, regardless of the fact that MainWindow #3 was running and active, still invoked the code in the zombie MainWindow #1 (or #2?).

No comments: