Notes for plugin developers

Do you have questions about writing plugins or scripts in Python? Meet the coders here.

Notes for plugin developers

Postby Bitmonster » Tue Nov 20, 2007 5:03 pm

I will use this thread to inform all plugin developers of some changes that they need to be aware of.

Here is the first one:
In the beginning all configuration dialogs only had an "OK" and "Cancel" button. A pair of month ago I added an "Apply" button to them, but the implementation wasn't very good. EG simply called your Configure() method again if the user pressed the apply-button and eg.ConfigurationDialog() returned the same dialog again (after clearing all controls). To avoid screen flicker it simply disabled screen refreshes till AffirmedShowModal() was called. This approach did its job, but wasn't very efficient of course. And now the latest update of wxPython will not allow to disable the screen refreshes completely, so the dialogs will start to flicker on apply.

So it was time to rework the mechanism. Lets look at a typical Configure() method as it was before and after the new implementation.

The old one:
Code: Select all
    def Configure(self, myString=""):
        dialog = eg.ConfigurationDialog(self)
        textControl = wx.TextCtrl(dialog, -1, myString)
        dialog.sizer.Add(textControl)
        if dialog.AffirmedShowModal():
            return (textControl.GetValue(), )


Must be changed in new versions to:
Code: Select all
    def Configure(self, myString=""):
        panel = eg.ConfigPanel(self)
        textControl = wx.TextCtrl(panel, -1, myString)
        panel.sizer.Add(textControl)
        while panel.Affirmed():
            panel.SetResult(textControl.GetValue())


Here is the list of differences:
1. You don't call eg.ConfigurationDialog(self) anymore, but eg.ConfigPanel(self) instead. The returned object is a wx.Panel instance and not a wx.Dialog instance.
2. You create your controls with the panel as parent and add it to the panel.sizer. This is actually no real change, as the panel has the same methods and attributes as the old dialog.
3. Instead of doing a "if dialog.AffirmedShowModal(): return (...)" you now have to create a while loop with "panel.Affirmed()" as condition and return the results of your controls through "panel.SetResult(...)". This way your Configure() method can be kept alive till the user presses "OK" or "Cancel".

I already have updated all your plugins in the latest beta.
So you don't need to rewrite anything yourself. But if you have uncommitted changes, be aware of the new implementation and update them accordingly. The new betas will print a small error message if you use the old eg.ConfigurationDialog() but will work as before. In some time I will remove the old implementation completely.

I accordingly have updated the documentation also:
http://www.eventghost.org/docs/writing_plugins
Please post software-related questions in the forum - PMs will only be answered, if really private, thanks!
User avatar
Bitmonster
Site Admin
 
Posts: 2239
Joined: Mon Feb 06, 2006 10:28 pm

Re: Notes for plugin developers

Postby Bitmonster » Thu Nov 22, 2007 4:20 pm

And here is the next note:

In a little while EventGhost will disable the apply button on start of the configuration dialogs and only enable it, if the user has made some changes to one of its controls. This works through monitoring the basic events in the ConfigPanel class. Therefore it is important, the ConfigPanel can actually receive the events. There is one little misfortune, that can prevent this from happening: binding an event handler to a control and missing to call event.Skip().

So:
If you bind an event handler to a control, make sure you call Skip() in the event handler, because otherwise the event will not propagate to the ConfigPanel.

As an example, lets assume a Configure() wants to return a string, but only if the CheckBox is enabled. If it is unchecked, the TextCtrl should get disabled and the dialog shall return None. The code might look like this:

Code: Select all
    def Configure(self, myString="Hello World"):
        panel = eg.ConfigPanel(self)
       
        myCheckBox = wx.CheckBox(panel, -1, "Enable TextCtrl:")
        myTextCtrl = wx.TextCtrl(panel)
        if myString is None:
            myCheckBox.SetValue(False)
            myTextCtrl.Enable(False)
            myTextCtrl.SetValue("")
        else:
            myCheckBox.SetValue(True)
            myTextCtrl.Enable(True)
            myTextCtrl.SetValue(myString)

        def OnCheckBox(event):
            myTextCtrl.Enable(myCheckBox.GetValue())
            event.Skip() # <= This is important! Don't forget it!
        myCheckBox.Bind(wx.EVT_CHECKBOX, OnCheckBox)
       
        panel.sizer.Add(myCheckBox)
        panel.sizer.Add(myTextCtrl)
       
        while panel.Affirmed():
            if myCheckBox.GetValue():
                result = myTextCtrl.GetValue()
            else:
                result = None
            panel.SetResult(result)

I myself have missed to call event.Skip() in nearly every plugin I wrote. So I have to review them all. Please do the same for your plugins and inform my if you have any code that needs to be updated this way.

If you create some kind of control, that doesn't use the normal wxPython event scheme, you can also call "panel.SetIsDirty()" to force the apply button to get enabled.
Please post software-related questions in the forum - PMs will only be answered, if really private, thanks!
User avatar
Bitmonster
Site Admin
 
Posts: 2239
Joined: Mon Feb 06, 2006 10:28 pm

Re: Notes for plugin developers

Postby Pako » Fri Nov 23, 2007 1:09 pm

Is some better solving, when I need call function OnCheckBox()?
Code: Select all
        def OnCheckBox(event):
            OnCheckBox_()
            event.Skip()
           
        def OnCheckBox_():
            myTextCtrl.Enable(myCheckBox.GetValue())
        myCheckBox.Bind(wx.EVT_CHECKBOX, OnCheckBox)
        OnCheckBox_()
Pako
User avatar
Pako
Plugin Developer
 
Posts: 2222
Joined: Sat Nov 11, 2006 1:31 pm
Location: Czech Republic

Re: Notes for plugin developers

Postby Bitmonster » Fri Nov 23, 2007 2:02 pm

Sure, this also works. But since you have to check for None in this case also, it is actually not much shorter if you look at the complete source.
Please post software-related questions in the forum - PMs will only be answered, if really private, thanks!
User avatar
Bitmonster
Site Admin
 
Posts: 2239
Joined: Mon Feb 06, 2006 10:28 pm

Re: Notes for plugin developers

Postby Pako » Fri Nov 23, 2007 3:31 pm

A this is more smart way?
Code: Select all
        def OnCheckBox(event):
            myTextCtrl.Enable(myCheckBox.GetValue())
            if event=="dummy":
                pass
            else:
                event.Skip()
        myCheckBox.Bind(wx.EVT_CHECKBOX, OnCheckBox)
        OnCheckBox("dummy")
Thanks,
Pako
User avatar
Pako
Plugin Developer
 
Posts: 2222
Joined: Sat Nov 11, 2006 1:31 pm
Location: Czech Republic

Re: Notes for plugin developers

Postby Bitmonster » Fri Nov 23, 2007 5:22 pm

You can even write it:
Code: Select all
        def OnCheckBox(event=None):
            myTextCtrl.Enable(myCheckBox.GetValue())
            if event:
                event.Skip()
        myCheckBox.Bind(wx.EVT_CHECKBOX, OnCheckBox)
        OnCheckBox()

It's all the same and just a matter of personal preferences.
Please post software-related questions in the forum - PMs will only be answered, if really private, thanks!
User avatar
Bitmonster
Site Admin
 
Posts: 2239
Joined: Mon Feb 06, 2006 10:28 pm

Re: Notes for plugin developers

Postby Bartman » Thu Nov 29, 2007 7:59 pm

I have done something like
Code: Select all
onRadioButton(wx.CommandEvent())
to initially trigger the function that enables the controls according to the selected functions. Could this cause some problems?
I have not seen a single disabled "Apply button".
Bartman
Plugin Developer
 
Posts: 881
Joined: Sun Feb 12, 2006 9:03 am

Re: Notes for plugin developers

Postby Bitmonster » Thu Nov 29, 2007 8:07 pm

I guess wx.CommandEvent() will work.

I haven't enabled the code, to give us time to change all plugins accordingly. The next version will only disable the apply button if started with -debug. And some versions later it will be the default.
Please post software-related questions in the forum - PMs will only be answered, if really private, thanks!
User avatar
Bitmonster
Site Admin
 
Posts: 2239
Joined: Mon Feb 06, 2006 10:28 pm

Re: Notes for plugin developers

Postby Bitmonster » Wed Dec 12, 2007 5:36 pm

An increasing number of plugins is trying to use the functionality of the Window.FindWindow action to get the handle to a window. Window.FindWindow has many more options then the win32api.FindWindow (like searching for the process name, wildcards and so on). To make good use of it, you have to keep some things in mind:

1. Don't use it! :)
Window.FindWindow is a plugin action and not a function. All actions of the Window plugin can have side effects, because the plugin holds an internal list of the windows it should target. So if you use Window.FindWindow in your plugin, you might trash a macro a user has made.

2. But you can use eg.WindowMatcher() instead.
eg.WindowMatcher() is actually the core of the Window.FindWindow action but has no side effects. It takes the same parameters (even though the last one is ignored, as it is used to stop a macro on some condition), so you can create and test the target configuration with Window.FindWindow and transfer the arguments to eg.WindowMatcher().

3. eg.WindowMatcher is a class used as a factory function.
It creates a new object out of its parameters, that you can call without any parameter. The idea behind the WindowMatcher is, that it compiles its parameters (like the wildcards) to a more computation friendly form, which could then be used to enumerate the windows faster. So you actually have to do something like:
myWindowMatcher = eg.WindowMatcher(...)
hwnds = myWindowMatcher()

4. Creation of a WindowMatcher is expensive.
Don't create a WindowMatcher every time, if you don't need other parameters. Instead create one at module level as global and re-use it in your functions.

5. Keep an eye on the timeout parameter.
Window.FindWindow defaults to a timeout value of one second. Normally in your plugins you don't want to have a timeout, so set this value to 0.00 before you gather the parameters.

This all applies to the beta 0.3.6.1253 and greater. I have rearranged the code of the WindowMatcher (like no need to use the .Enumerate member anymore) a bit in this version.

I also have changed some plugins that used WindowMatcher or Window.FindWindow before. So grab the new source and look if everything works the way you have intended.
Please post software-related questions in the forum - PMs will only be answered, if really private, thanks!
User avatar
Bitmonster
Site Admin
 
Posts: 2239
Joined: Mon Feb 06, 2006 10:28 pm

Re: Notes for plugin developers

Postby Bitmonster » Fri Dec 14, 2007 4:01 pm

If you want to have a runnable SVN checkout that needs no Python install on the system, you can simply do this:

1. Install the latest Setup.exe somewhere. Might be preferable to change the installation path to something like "C:\EventGhostDev" or something.
2. In this folder delete the folders "eg", "languages", "plugins" (or in other words: every folder, except "lib") and the files "CHANGELOG.TXT" and "Example.xml".
3. Now you can checkout from http://www.eventghost.org/svn/trunk into this folder.

I use TortoiseSVN for that, as it is very convenient. Now you can always right-click the folder and select "SVN Update" to get the newest files. Since anonymous read access is allowed for everybody, this works for everybody.
Please post software-related questions in the forum - PMs will only be answered, if really private, thanks!
User avatar
Bitmonster
Site Admin
 
Posts: 2239
Joined: Mon Feb 06, 2006 10:28 pm

Re: Notes for plugin developers

Postby Bitmonster » Sun Dec 16, 2007 1:01 pm

Some of you might have noticed, that I often use code like:
Code: Select all
panel = eg.ConfigPanel(self)
aTextCtrl = panel.TextCtrl(someValue)
aCheckBox = panel.CheckBox(someOtherValue, "A description")
and so on.

I added a bunch of controls as members of the ConfigPanel class, because it is more convenient to use them this way (no useless -1 parameter for example). All controls follow the same scheme:

aCtrl = panel.ControlClassName(initialValue, [optionalLabel])

Not all controls have a label, but if they have one, it is the second parameter. Normally this is a string, but for choice controls it is a list of strings. To unify them even more, all controls that are created this way have a GetValue() and SetValue() method. So for choices you can also write GetValue() instead of GetSelection().

If you are missing an important control as ConfigPanel member, let me know.
Please post software-related questions in the forum - PMs will only be answered, if really private, thanks!
User avatar
Bitmonster
Site Admin
 
Posts: 2239
Joined: Mon Feb 06, 2006 10:28 pm

Re: Notes for plugin developers

Postby Bitmonster » Mon Dec 17, 2007 8:33 pm

Some plugins can get into trouble, if the computer is going into suspend state or resumes from there. Formerly the recommended approach was to use eg.Bind("System.Suspend") to register a callback for handling this. Since this is very bothersome (registering both for resume and suspend and taking care to also unregister it), I have added two methods to the PluginClass, that you can simply override.

def OnComputerSuspend(self, suspendType):
Is called before the system goes into suspend state if the plugin is currently enabled. I think if a plugin is disabled, it will never need to take any actions. The suspendType parameter is currently not used (always None), but I might change this, so the routine can see if the system goes into S3 or S4 or whatever.

def OnComputerResume(self, suspendType):
This is the counterpart and called if the system resumes from suspend state.
Please post software-related questions in the forum - PMs will only be answered, if really private, thanks!
User avatar
Bitmonster
Site Admin
 
Posts: 2239
Joined: Mon Feb 06, 2006 10:28 pm

Re: Notes for plugin developers

Postby Bitmonster » Tue Dec 18, 2007 7:28 pm

Here is a problem, where I need some opinions from the developers.

First a little history:
In the beginning every plugin has created an object in eg.plugins and all actions of the plugin created objects as named members of the plugin. So you could access every action like:
eg.plugins.System.MuteOff()
and the plugin could access its actions with :
self.MuteOff()

The problem with this approach was, that it was easily possible to accidentally override an attribute of a plugin with an action with the same name. Imagine a developer would define a method named self.Stop() somewhere to stop some task. Now he adds a bunch of actions from a list and overlooks that there is also an action with the name "Stop" that will become the new self.Stop().

So I decided to avoid this by using a proxy object in eg.plugins and to don't add actions as direct members of the plugin. This means that eg.plugins.System is not the actual plugin, but a proxy object that has all actions of the plugin as members, so that eg.plugins.System.MuteOff() will access the action as before, but eg.plugins.System is not the actual plugin. And from the plugin's point of view there is no self.MuteOff(). If it wants to access one of its actions (or an attribute of this action) it has to use the not much documented attribute "self.info". So to call the action it would need to use:
self.info.actions.MuteOff().
And to get the real plugin object from a global point of view, the user would need to use:
eg.plugins.System.plugin

As a result eg.plugins.System.MuteOff() could also be written as eg.plugins.System.plugin.info.actions.MuteOff() (even though it would be stupid to do that).

Complicated, isn't it?
Now I'm a bit in a difficult situation. The old approach was easy to understand, but has the danger to accidentally override a member and polluting the plugin's namespace with potentially hundreds of actions, that it will seldom need to directly access.

The new approach is much harder to explain and possibly because of that might be a bad idea.
Please post software-related questions in the forum - PMs will only be answered, if really private, thanks!
User avatar
Bitmonster
Site Admin
 
Posts: 2239
Joined: Mon Feb 06, 2006 10:28 pm

Re: Notes for plugin developers

Postby Bitmonster » Thu Apr 03, 2008 5:24 pm

I currently refactor many modules to use ctypes instead of PyWin32. The long term goal is to remove all references to PyWin32 completely, so this package can be dropped. But it will take many time and some problems aren't resolved till now (like building the COM server with ctypes). Anyhow, I want to encourage all developers, to use ctypes instead of PyWin32 in the future, because ctypes is part of the standard library and PyWin32 has some bugs and doesn't seem to be maintained very well currently.

I also changed the name of eg.WinAPI to eg.WinApi (only case changed). Because of this, a simple SVN Update won't work to get the newest revision (Windows will complain a folder of this name already exists). You need to delete the old WinAPI by hand in your working copy, before you can make an SVN Update. Or do a completely fresh SVN Checkout.
Please post software-related questions in the forum - PMs will only be answered, if really private, thanks!
User avatar
Bitmonster
Site Admin
 
Posts: 2239
Joined: Mon Feb 06, 2006 10:28 pm

Re: Notes for plugin developers

Postby Bitmonster » Fri Jun 27, 2008 10:50 pm

I don't constrain any plugin developer to use any certain coding style, but please do me a favour and configure your editor to replace tabs with spaces. If you use tabs, it is sometimes hard to guess which indention was intended and my development tools go crazy, if they see a mix of tabs and spaces.
Please post software-related questions in the forum - PMs will only be answered, if really private, thanks!
User avatar
Bitmonster
Site Admin
 
Posts: 2239
Joined: Mon Feb 06, 2006 10:28 pm

Next

Return to Coding Corner

Who is online

Users browsing this forum: No registered users and 8 guests