Saturday, May 31, 2014

What a tangled web we weave...

The mainwindow class defines everything to do with the File menu: new, open, close, save-as, and a submenu of "recent" files. The latter is a feature I often find useful. I don't have to remember anything about a file except that not long ago I edited it. Just go to the end of the File menu and there it is, without having to navigate the file system to find it. However this is a feature that has some subtle implementation details, some of which I am only just now remembering as I start to code the version 2 of it. I should have reviewed V1 more carefully.

When coding the initial form of this I decided rather hastily that I would keep the list of "recent" files as a dict with filenames as keys and their paths as values: {filename.typ:path-to-file}. Of course you see immediately the problem with this, don't you?

Duplicate filenames! What if the user opens ~/documents/fred.txt and (then or another day) opens /pgdp/history/fred.txt? They are (presumably) different files but a Python dict allows only one value for the key fred.txt. Looking back at V1 code I see that I kept a simple list of full pathnames, with the most recent at the front. Much better.

Unfortunately the {filename:path} dictionary idea is in several places in the code and in the unit test driver. So now I am looking at changing 50 lines of code or more scattered in two or three modules. Because I didn't take the time to look at the working V1 code. Oh, bleagh.

Another subtlety of the recent-files list is, what if the file no longer exists? Or, much the same thing, what if it was opened from a mountable drive (e.g. a USB dongle) that isn't mounted just now? The file could appear or disappear between uses of the File menu.

The answer to that is, that the Recent sub-menu has to be (re)built dynamically, on the fly, when the File menu is about to be displayed. There is an aboutToShow signal emitted by the File menu action itself. The slot for that signal must have the code to clear and repopulate the Recent sub-menu with an action for each file in its list that: (a) is not already open but (b) still exists and is accessible. (Note that if a file is not now accessible, it doesn't go in the menu, but does stay in the list, so if it comes back at some future time, we'll show it.) I modeled the V1 code after an example in Summerfield's book. I remember thinking at the time, really? All this code gets executed between the mouse's click on the word File, and the painting of the menu? And nobody notices a delay? I still think it's remarkable.

A related issue is one that did not arise in V1. When the user selects File>Open and chooses a file, what to do if that's a file that's already open? We do not want to open a second copy for sure. But that means it needs to be easy to detect when a chosen filepath is identical to one of the possibly-several files already open. If it is, don't proceed with the open, but do "focus" that file: make it the currently selected tab in the edit tabset, so it's visible.

Yet another difference from V1: when the close signal is received, V1 looked to see if its one-and-only document was modified, and if so, gave the user the choice Yes to save, No to not save and continue with the Quit, or Cancel to not Quit. But with V2, it is possible there are multiple modified documents at Quit time. So the warning message at least needs to say how many modified documents there are. But should a Yes reply mean, save everything? That is, do a File>Save for each modified document? A problem with that is, some of them might be modified New documents with filenames of "Untitled-n" and no related path string. They need to be treated as Save-As, with a file-save dialog so a proper name and folder can be chosen.

Or, should the close event just loop through all open documents, and present a separate modal dialog for each one that's modified: "File filename.typ is modified. Save it now?" Yes/No/Cancel. Or better, Save/Don't Save/Cancel Quit. For normal files, clicking Yes gets an instant save. For "Untitled-n" documents, a Yes is immediately followed by presentation of the standard Save-As dialog.

That makes everything clear, but it could mean that the user who tried to Quit or clicked the [x] button in the window border is presented with a sequence of modal dialogs one after another. If the user is under stress (told to hurry up and get off that machine for some reason) this could be quite annoying. But what alternative is there?

BBEdit's way

Well, here's one alternative. When I tell BBEdit to quit with two open, modified documents, it... quits! No dialogs, no warnings. And the files on disk do not reflect the changes, so there was no secret saving going on.

When BBEdit is restarted, it shows a small progress message "Restoring BBEdit State" and then it opens the two modified documents and shows them with the modified text and a state of modified. How does it do that?!? It must save a secret copy of any modified file so it can recover that state on startup. But where?

No comments: