Creating a macro API from an application

Enabling users to execute any bit of Python code they might have lying around, from a document, the menu or keyboard, isn't enough to macro-enable Kalam. For this, we must offer a clean and clear set of functions that can be used to manipulate the data and interface of our application. This is the hardest part, actually. If you can't accomplish this, you might as well tell users to hack your application directly.

One problem is already apparent in the startup script we created in the previous section. The macro writer needs to call a nebulous entity named self, but how is he to know that this is a reference to the application itself?

It would be more effective to allow access to the application using a logical name. The solution is to add an extra entry to the namespace that is used to initialize the macro manager. This is as simple as adding a key to a dictionary. Let's revisit the initMacroManager() function and add the current KalamApp object:

    def initMacroManager(self):
        g=globals()
        g["kalam"]=self
        self.macroManager = MacroManager(self, g)
    

Another, and perhaps slightly less hackish way of adding items to the global namespace is the use of the global keyword:

    def initMacroManager(self):
        global kalam
        kalam = self
        self.macroManager = MacroManager(self, globals())
    

By declaring a variable global, it becomes part of the global namespace. Passing that namespace to exec gives all executed code access to the variable.

Accessing the application itself

As an example, I have created a few functions that simplify the creation of new documents and macro's. Because a macro is wrapped in MacroAction, which is a subclass of QAction, it's very easy to add them to the menu.

    # kalamapp.py
    #
    # Macro API
    #
    def installMacro(self,
                     action,
                     menubar = None,
                     toolbar = None):
        """
        Installs a certain macro action in the menu and/or the toolbar
        """
        if menubar != None:
            action.addTo(menubar)
        if toolbar != None:
            action.addTo(toolbar)

    def removeMacro(self, action):
        action.remove()

    def createDocument(self):
        doc, view = self.docManager.createDocument(KalamDoc, KalamView)
        return (doc, view)

    def createMacro(self, name, code):
        return self.macroManager.addMacro(name, code)
      

These methods are part of the KalamApp class, but it would be nice to not have to prefix class with kalam from every macro. So these functions are added to the global namespace, too:

    def initMacroManager(self):
        g=globals()
        g["kalam"]=self
        g["docManager"]=self.docManager
        g["workspace"]=self.workspace
        g["installMacro"]=self.installMacro
        g["removeMacro"]=self.removeMacro
        g["createDocument"]=self.createDocument
        g["createMacro"]=self.createMacro
        self.macroManager = MacroManager(self, g)
      

Later, we will be writing a nice macro that resides in a file called edmund.py. Here's how the startup.py script uses the API to install the macro:

#
# startup.py - Kalam startup macro file"
#
edmund = createMacro("edmund", open("edmund.py").read())
edmund.setMenuText("Edmund")
edmund.setText("Edmund")
edmund.setToolTip("Psychoanalyze Edmund")
edmund.setStatusTip("Psychoanalyze Edmund")
installMacro(edmund, kalam.macroMenu)
      

Using the kalam instance of KalamApp, the macro writer has access to all menus. In this case the edmund macro is added to the macro menu, kalam.macroMenu.

Accessing application data

This application collects its data in the KalamDoc object and shows it using the KalamView object. By giving a user access to the entire internal object model via the DocManager object, we make it possible to script the creation, modification and saving of documents.

Accessing and extending the GUI

The function installMacro, which we have already seen, is used to add a macro to any menubar or toolbar. What text the macro shows, what tooltips, what icon and what accelerator key is all determined by the settings of the underlying QAction object. In the example above, we didn't set a shortcut or an icon.

By not hiding the underlying gui toolkit, clever users can do almost anything to your application. It would be a trivial exercise to integrate a Python class browser into Kalam, especially since I have already made a PyQt based standalone class browser, which you can find at http://www.valdyas.org/python. However, let's not be so serious and sensible, and implement something a little more frivolous.

Kalam rivals Emacs: an Eliza macro

One of the first extensions to the Emacs editor, way back in the beginning of the eighties, was an Eliza application. A kind of Rogerian psychoanalyst that took the user's input, analyzed it a bit, and said something comprehensible back.

This is actually a very nice example of working with documents in an editor, since the macro must be aware (more or less) of what the user typed in, and be able react to the pressing of the Enter key. Surely having all the power of Python at our disposal means that we can at least achieve equal status with the doyen of editors!

So, without further ado, I present Edmund - who doesn't really listen, but does answer back, in his accustomed vein:

import random

class Edmund(QObject):
    """
    An Edmund macro for the Kalam Editor.

    Of course, if I really re-implemented Eliza, the responses would bear
    some relevance to the input. Anyway.
    """

    def __init__(self, *args):
        QObject.__init__(self)
        self.responses = [
            "First Name?",
            "Come on, you MUST have a first name.",
            "Sod Off?",
            "Madam, without you, life was like a broken pencil...pointless.",
            "So what you are saying, Percy, is something you have never" +
            " seen is slightly less blue than something else . . that you " +
            "have never seen?",
            "I'm afraid that might not be far enough. " +
            "Apparently the head Mongol and the Duke are good friends. " +
            "They were at Eton together.",
            "Ah ah, not so fast! Not that it would make any difference. " +
            "We have the preliminary sketches...",
            "You have absolutely no idea what irony is, have you Baldrick?",
            "Baldric, you wouldn't recognize a subtle plan if it painted  " +
            "itself purple and danced naked on a harpsichord singing " +
            "'subtle plans are here again'.",
            "Baldric, you have the intellectual capacity of a dirty potato.",
            "Ah, yes. A maternally crazed gorilla would come in handy " +
            "at this very moment.",
            "That would be as hard as finding a piece of hay in an " +
            "incredibly large stack of needles.",
            "Normal procedure, Lieutenant, is to jump 200 feet in the air " +
            "and scatter oneself over a wide area.",
            "I think I'll write my tombstone - Here lies Edmund Blackadder" +
            ", and he's bloody annoyed.",
            "As a reward, Baldrick, take a short holiday. " +
            ".... Did you enjoy it?"
    ]

        self.doc, self.view = createDocument()
        self.doc.setTitle("Talk to Edmund BlackAdder")
        self.connect(self.view,
                     PYSIGNAL("returnPressed"),
                     self.respond)
        self.view.append("Welcome\n")
        self.view.goEnd()

    def respond(self):
        input = str(self.view.textLine(self.view.numLines() - 2))
        if input.find("love") > 0:
            response = self.responses[3]
        elif input.find("dead") > 0:
            response = self.responses[15]
        elif input.find("fear") > 0:
            response = self.responses[5]
        else:
            choice = random.randrange(0,len(self.responses),1)
            response = self.responses[choice]
        self.view.append(response + "\n\n")
        self.view.goEnd()

edmund = Edmund()
      

Of course, this is an extremely primitive form of amusement, but you get the idea. By accessing the API's of the KalamDoc and KalamView classes, the macro author can do all kinds of fun things, like reading out lines of the text or adding text to the document.

Talking heart to heart with your computer.