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.