Tuesday, January 6, 2015

Managing the Edit menu, right and wrong ways

Absent any news from Nuitka, I sat down to resume work on a deferred piece of the Word panel, namely, giving it a custom Edit menu. The noteview module has its own Edit menu, and Word needs its own.

Here's the thing. In Qt menus work like this. A QMenu is populated with a set of QMenuAction objects. Each action has the properties of:

  • a name string, such as "Copy" or "Save";
  • an optional icon;
  • an Enabled flag, which if False means the action is visible but inert, and probably shown differently, e.g. is "grayed-out";
  • and most significantly, a slot (function) to be called when the action is selected.

Often the slot is an existing, standard method of a standard widget. For example, a widget based on QTextEdit might set up its Edit:Copy action to call its own standard copy() method.

Hah! But what happens when an app offers multiple, different, independent widgets? As for example, PPQT offers an Edit panel where the document is edited, but also a Notes panel where the user can write a different document. If there is just one Edit menu and it contains just one Copy action, which QPlainTextEdit object's copy() method is called when the user selects Edit:Copy?

For PPQT it gets worse because the Word panel also wants to support Edit:Copy to copy the list of one or more words currently selected in the Words table. That is a QTableView object which doesn't have a built-in copy() method.

So as the user hops around, clicking the keyboard focus into one panel and then another and then another, how does the Edit:Copy action (not to mention Cut, Paste, etc) keep up?

In V1, it didn't keep up very well. The Edit menu actions operated only on the Edit panel. The keyboard accelerators such as ^c, ^v, and ^x operated kind-of correctly. The Qt application tracked the keyboard focus and directed those actions between at least the Edit panel and the Notes panel. They did not get passed into the Words panel. I spent a lot of time trying to capture ^c via a keyPressEvent function in the Words panel, and could never find a way to make Qt let me see that key combination. It was short-circuited at some higher level and never entered the key-event stack. Eventually I implemented the kludge of shift-control-c, which I could capture in the Words panel, to perform a Copy from the Words table.

For V2 I decided to use the more sophisticated kludge of giving each panel that wanted one, its own Edit menu. (The following is for historical interest only; it was a bad idea.) There is only one Menu Bar, created by the main window. It gave access to the menu bar with a module-level global and a get_menu_bar() function.

In the Notes panel, for instance, it creates its own Edit menu and adds it to the global menu bar, with its Visible set to False. When Notes receives a focusInEvent() call, it sets its Edit menu visible to True; and when it gets a focusOutEvent() it sets it back to False. Thus, I thought, the app-wide menu bar might have several Edit menus simultaneously, but only one, or sometimes zero, of them would be visible.

This seemed to be working with Qt 5.3, except it was implemented only for Notes.

When I moved to Qt 5.4 and started running the program, I noticed some oddball messages popping up in the console: "void QCocoaMenu::insertNative(QCocoaMenuItem *, QCocoaMenuItem *) Menu item is already in a menu, remove it from the other menu first before inserting". Wha? I googled that and found two Qt bugs that produced that message, but they seemed to be associated with crashes and with other circumstances. I tabled that issue temporarily.

Then today I started adding the code to wordview.py to create and display its own Edit menu, parallel to the one set up by noteview. And surprise, the number of those messages suddenly increased. Clearly, I was causing them with my duplicate Edit menus. Qt 5.4, among its other changes, has started using the Cocoa UI of Mac OS. It appears that Cocoa does not like having multiple menu items with identical names in the menu bar.

So now I need a better kludge. I am going to go back to square one on the Edit menu. I will implement an Edit menu setter-upper function in mainwindow. It will be called with a list of QMenuAction items. It will call the clear() method of the one-and-only Edit menu, repopulate it with the actions passed by the caller, and make it visible. The Edit, Notes, and Words panels will each, upon focusIn, call the main window to repopulate the Edit menu. And on focusOut, they will call the mainwindow to tell it to hide the Edit menu. In this way there will be only one Edit menu, so Cocoa will be happy, but it will have actions that point to slots in the panel that currently has the focus.


Incidentally, while working over this problem, I discovered the cause of another mysterious bug. I described this back in June in a post about the Notes panel under the heading "UI Issues". When the keyboard focus left the Notes panel, its selected text changed to gray as it should in an inactive editor; but when the focus returned, its selection did not resume the default bright-yellow hue. I actually put in code to force the correct palette.

I put that code in the focusInEvent() and focusOutEvent() methods, which I had added in order to show and hide the Edit menu.

Today I realized: I never passed the focus events on to the parent class! Each of those events should end with a call to super() to pass the event up the chain. No wonder the palette never changed on focus-in—the parent never saw the event!

No comments: