Tuesday, May 6, 2014

Signal from a module

So the fonts module knows all about QFonts. Its main purpose is to keep track of the user's preference for the mono font (used in the editor) and the general font (used by all the UI widgets), getting these from the app settings at startup and recording them in settings at shutdown, and supporting the yet-to-be written preferences dialog. It offers services like

  • get_fixed() to return a QFont set to the user's preferred monospaced family and size,
  • scale(updown,qfont) to return a qfont scaled up or down one point,
  • ask_font(mono=True) to present a QFontDialog with an appropriate title and return a selected QFont.

When the user chooses a different mono font, any editview needs to know, because it has to explicitly set the font of its QPlainTextEdit. There could be one editview, or several, or even none. Similarly the main window needs to know if the user wants to use a different font for the UI (unlikely, but why not?). This is just the right place to use a Qt signal. Any editview, or the main window can connect itself to that signal while initializing.

However this turned out to be tricky because fonts is a module, not a class. Thereby hangs a short boring story...

Modul-ism

When I was writing PPQT V.1 I didn't really understand Python namespaces. I was stuck in the mindset of assembly language and C—hey, I'm old, what do you want?—so I vaguely equated Python import with a C #include. They are sort of alike in that both bring some externally-defined code into the scope of a source module. But as I bet you know, they are very, very different in implementation.

Take a stupid example. Let's say you have the following modules:

common.py:
    GLOB = 1
unit_a.py:
    import common
    common.GLOB = 2
unit_b.py:
    import common
    print(common.GLOB)
main.py:
    import unit_a
    import unit_b

When you run main.py, what happens? The first mental hurdle to get over is to realize that import is an executable statement: any code in the imported thing is executed. Normally that's declarative code like def or class but the assignments and the print() in these modules will also execute. So something is going to be printed: what?

If import was like #include, it would print 1 because the common imported into unit_b would be an independent copy of the file common.py. But that's not what happens. The first time import common is executed—as part of executing import unit_a—Python creates a common namespace with the one entry, GLOB bound to a 1, which unit_a then reassigns to 2. The next time import common is executed—as part of executing import unit_b—all that happens is that unit_b's namespace gets a reference to the same common namespace, from which it prints 2.

Although I understood this in a theoretical way, I couldn't quite shake the suspicion that importing the same module more than once was somehow a risky proposition. So I took pains to create a class to hold global values. I instantiated one of that class early, and passed that object into each sub-module. It was over-complicated, unnecessary, and in fact a bad design because the global-holding object ended up an overstuffed portmanteau full of unrelated things.

So, V.2, we do things pythonically. As I said, anything font-related gets handled in the fonts module, which has a number of global values with names like _MONO_FAMILY. These get set when the main window calls fonts.initialize(settings), they may get reset when the yet-to-be-written preferences calls fonts.set_general() or fonts.set_fixed(), and so on. And any module that imports fonts will be using the one and only fonts namespace and the same global values.

Signalling

Fine, but what about that signal? Say that preferences calls fonts.set_fixed() with a new choice of QFont. The fontsChanged signal needs to be emitted. But how, or from what?

The new PyQt5 signal/slot API insists that a signal has to be bound to a QObject instance. Fonts is a module, and it has no need define a class or make an object. But it wants to emit a signal. So this is what I had to do:

class Signaller(QObject):
    fontChange = pyqtSignal(bool)
    def connect(self, slot):
        self.fontChange.connect(slot)
    def send(self,boola):
        self.fontChange.emit(boola)

_SIGNALLER = Signaller()

def notify_me(slot):
    _SIGNALLER.connect(slot)
def _emit_signal(boola):
    _SIGNALLER.send(boola)

Signaller is a QObject with just one attribute, a class variable fontChange that is a signal. The signal carries one parameter, a boolean. (It's True if the font changed was the mono font, False if it was the UI font.)

Signaller has two methods, one to connect its signal to a slot, and one to emit the signal. One Signaller object is created and saved in a global reference.

Now, a call to fonts.notify_me() can be used to hook up any Python executable to the fontChange signal. Within the fonts module, a function like fonts.set_fixed() can call _emit_signal(True) to send the signal.

This works fine; the unit-test driver hooked up a function, called fonts.set_fixed(), and its function was invoked.

No comments: