Tuesday, June 14, 2016

And Now For Something Completely Different

For a couple of years, this blog has been totally focused on my adventures with Python, Qt, and PyQt as I developed a major-ish app, PPQT2.

Well, with the latest round of fixes (see two prior posts) I believe that PPQT2 is about as done as it can be. I will leave the distribution files up indefinitely. But I don't intend to add any function, and I don't believe it contains any bugs serious enough to prevent its productive use. A couple of people are using PPQT2, and I wish them well. I am not among them, because I no longer spend any of my own time editing books for Distributed Proofreaders. The first PPQT was done to meet my own requirements for a book editor. The second one was done, really, to do the job right from a software engineering standpoint, and to meet the evolving needs of the Distributed Proofreading community. It succeeded in the first goal, but, based on limited uptake, failed to meet the second. So it goes.

Back to Authorship

A decade ago I wrote a book. Well, I've written several books over the years, mostly software. But that latest book was not about software. It was in a way, an extended answer to a question my mother asked me a few months before she died. I wrote the book and self-published it.

You can read about the book at its home website. In fact, you can read the whole book online there if you wish. Or you can follow links from there to buy your very own hard-copy for a very reasonable fee.

Don't do that!

Well, you can read it if you really want to. But don't order a copy (not that you were likely to do so, but still).

Why? Because I've embarked on a major revision, is why.

Adam Osborne is famous for killing his company by pre-announcing a better model before it was ready to ship. Sales of the existing Osborne computer dried up, choking off the cash flow that he needed to finish development. I'm not afraid of killing sales for my existing book because mostly, it has none to kill.

But why am I doing a revision? What improvements do I mean to make? How am I going about it?

Such questions will be the focus of forthcoming posts in this blog. In the meantime, if you want to get an early look, go to the new publishing site, slide the "You Pay" slider all the way to the left, to a cost of $0.00, and "buy" a copy of the book as it stands. If you give leanpub your email in that process, you'll get a notification as new versions are published, which is about monthly as I add chapters.

Friday, June 10, 2016

Fixing the Edit Menu Fix

A year and a half ago, I wrote an incomplete description of my solution for separate edit menus. While using PPQT2 in Windows 7 to test the logging/dictionary bug, I noticed a problem with this system and fixed it.

To review, under Qt there is a single menu bar that is owned by the main window. (The main window is a parentless widget based on the QMainWindow class.) In the menu bar there can be just one Edit menu.

In the Qt system, any menu is populated by QAction objects. In the Edit menu, if there is, say, a "Cut" item, it is installed in the menu by adding a QAction('Cut'). Any QAction has four properties:

  • A name string, like 'Cut'
  • An optional icon
  • An optional accelerator keystroke, like control-x
  • A "slot", which is an executable

The slot is the important item. It is a function that receives the "triggered" signal when the user selects that menu item or keys that accelerator.

This is a fine system, provided that there is no ambiguity about which slot to call. In an app with a simple UI, the one main window perhaps, the slot for File:Save or Edit:Cut is a method of the QMainWindow object, or of some sub-widget that it owns, and that never changes.

In PPQT2, there are multiple different widgets that can implement Edit actions. In the simplest case, the user has a single book open. But there is an Edit panel with the book text, a Notes panel with a separate simple document to edit, and a Words panel which supports copying selected words from a table. If the focus is in the Edit panel, we want the Edit:Cut action to be implemented by the cut() method of the QPlainTextEdit object that is part of that panel. But if the focus is in the Notes panel, we want Edit:Cut to be implemented by the cut() method that is part of the QPlainTextEdit object that is part of that panel. And if the focus is in the Words panel, we don't want to implement Edit:Cut at all, but we do want to support Edit:Copy via a method of the QTableView that implements the vocabulary table.

Worse: if the user has two or three books open, each book has its own Edit, Notes and Words panels. When the user hits control-x, it is important that the cut be implemented by the Edit or Notes panel for the book that is in front -- not by some panel containing the text of a different book that isn't visible.

The only solution is to continually change out the actions in the one-and-only Edit menu whenever the focus changes. When the Notes panel gets focus, it loads the Edit menu with the set of actions it supports, including slots that are methods of that unique Notes widget (distinguished by the "self" parameter). The code to do this, before this week, was as follows:

def set_up_edit_menu( action_list ) :
    global _EDIT_MENU, _LAST_MENU

    _EDIT_MENU.setEnabled(True)
    if id( action_list ) != _LAST_MENU :
        _LAST_MENU = id( action_list )
        _EDIT_MENU.clear()
        for (title, slot, key) in action_list :
            if title is None :
                _EDIT_MENU.addSeparator()
            else :
                _EDIT_MENU.addAction(title, slot, key)
def hide_edit_menu():
    global _EDIT_MENU
    _EDIT_MENU.setEnabled(False)

Any widget that wanted to support the edit menu had to do three things. It had to set up a list of Edit menu actions it supported, including the title, the slot, and a key code for each. It had to catch the focus-in event and call set_up_edit_menu() with that list. It had to catch the focus-out event and call hide_edit_menu(). As a result the Edit menu would be disabled (grayed-out) whenever focus was in a widget that didn't support it. When focus entered a widget that did support the Edit menu, it would be enabled and populated with that widget's actions, including references to that widget's slots.

I observed that it was quite frequent for a widget to get a focus-out followed by a focus-in, with no intervening visit to another widget. That's why the work of changing the menu is only done if the id() of the list is different. The Python id() of an object is basically its memory address. In my first description of this code, I was requiring the caller to pass a unique key, but I soon figured out that was not needed; id() did the job.

This all worked fine under Mac OS. Unfortunately I'd never really checked that it worked under Windows or Linux. This week I noticed that it did not. (imagine a blushing embarrassed emoji here)

In Windows and under Ubuntu Unity at least, the Edit menu was disabled, grayed-out, all the time! Why?!? The contents of the menu were changing properly, for example when focus was in the Edit panel, the menu contained the to-Upper and to-Lower actions that only the Edit panel supports. They went away when the focus moved to the Notes panel. So set_up_edit_menu() was being called. Which meant _Edit_MENU.setEnabled(True) was being executed.

It took a good half-hour of swearing and inserting print() statements before I realized that hide_edit_menu() was being called when I clicked on the word Edit in the menu bar. (imagine an open-mouth amazed emoji)

It seems that in Windows (and in Ubuntu, I later found out), a click anywhere in the menu bar causes a change of focus! You click in the Edit panel; the focus-in event calls set_up_edit_menu(). You click on the word Edit in the menu bar and the Edit panel gets a focus-out event and immediately calls hide_edit_menu(). It's a beautiful catch-22: the menu is enabled all the time until you try to use the menu, then it is disabled.

It took some pondering to figure out how to fix this. My solution was to ask, in hide_edit_menu(), if the current mouse location was within the global menu bar. If so, do not disable the menu. Here's the new code:

def hide_edit_menu():
    global _EDIT_MENU, _MENU_BAR
    
    if not C.PLATFORM_IS_MAC :
        relative_cursor_pos = _MENU_BAR.mapFromGlobal( QCursor.pos() )
        if _MENU_BAR.geometry().contains( relative_cursor_pos ) :
            return
    # either this is Mac OS, or the cursor is not over the menu bar
    _EDIT_MENU.setEnabled(False)

That made the Edit menu behave the way I wanted in all three systems.

A Logging problem in Windows -- and a pivot!

This is the first of a few posts about PPQT2, the hobby software project that's been the focus of the blog for two years. Then there will be a post announcing a turn to something completely different.

This week I had to deal with a reported bug in PPQT -- note, the first one in six months or more, which is more a reflection of its very low usage than on its code quality -- and found a couple other things that needed work.

Dictionary bug

The bug was present on all platforms, but was only visible on Windows. Here's the problem. The user opens a file that contains some non-Latin-1 characters, maybe some words in Greek. The default dictionary is set to en_US and the Greek words are not tagged for an alternate dict. So when the user refreshes the Words panel, every Greek word gets presented to a Hunspell object initialized with the en_US dict.

The encoding of any dict is specified in its name.aff file. The en_US dict is encoded ISO8859-1, and Hunspell expects any word checked against it to be encoded the same. Obviously a Greek word will have non-Latin characters, and Hunspell will correctly suffer an encoding error.

Not a problem! I was ready for this:

            try :
                return dict_to_use.spell(word)
            except UnicodeError as UE :
                dictionaries_logger.error("error encoding spelling word {}".format(word))
                return False

However, in Windows the call to the logger .error() itself raised an encoding error! Because the logged string contained the offending Greek word, and the log file had been opened using the default encoding which in Windows, was some wonky code page that couldn't encode Greek.

So there was an exception raised in the Except block, and the return False to indicate misspelling was never executed. But that did not cause a problem! Because the caller used the return value in an if statement, if check(word):. After Python displayed the traceback in the console window, it resumed execution in the calling function with the conventional default return value of None. None is just as False as False is, so everything worked, except for a ton of error messages in the console window.

The fix was to go back to the top-level module where I set up the logging handler for the whole program, and add encoding='UTF-8' to that call. A one-line fix and the error messages disappeared.