Message System

Preface:

The purpose of this document is to give an in depth overview of the message api used in Editra for broadcasting information and notification messages throughout the editor. This system provides a very small and simple api to work with, but it is a very flexible and powerful system. The message bus is used throughout various parts of Editra, mostly for broadcasting notification of actions to interested listeners. The log system is also built on top of this message system. This system is also designed with the intent of being public api for plugins to take advantage of for easy integration in Editra as well as for use to communicate with different objects in the Editor without having to know how to find the object.

Requirements:

  • Editra > 0.2.29, or svn checkout

Message System Api Overview

The message system's api is currently composed of one module (ed_msg.py) that contains three functions and the definitions of the core message types. Shown below are the three functions signatures and their docstrings.

def PostMessage(msgtype, msgdata=None):
    """Post a message containing the msgdata to all listeners that are
    interested in the given msgtype.
    @param msgtype: Message Type EDMSG_*
    @keyword msgdata: Message data to pass to listener (can be anything)
 
    """
 
def Subscribe(callback, msgtype=EDMSG_ALL):
    """Subscribe your listener function to listen for an action of type msgtype.
    The callback must be a function or a _bound_ method that accepts one
    parameter for the actions message. The message that is sent to the callback
    is a class object that has two attributes, one for the message type and the
    other for the message data. See below example for how these two values can
    be accessed.
    @param callback: Callable function or bound method
    @keyword msgtype: Message to subscribe to (default to all)
    @example:
        def MyCallback(msg):
            print "Msg Type: ", msg.GetType(), "Msg Data: ", msg.GetData()
 
        class Foo:
            def MyCallbackMeth(self, msg):
                print "Msg Type: ", msg.GetType(), "Msg Data: ", msg.GetData()
 
        Subscribe(MyCallback, EDMSG_SOMETHING)
        myfoo = Foo()
        Subscribe(myfoo.MyCallBackMeth, EDMSG_SOMETHING)
 
    """
 
def Unsubscribe(callback, messages=None):
    """Remove a listener so that it doesn't get sent messages for msgtype. If
    msgtype is not specified the listener will be removed for all msgtypes that
    it is associated with.
    @param callback: Function or bound method to remove subscription for
    @keyword messages: EDMSG_* val or list of EDMSG_* vals
 
    """

The basic idea behind the working of this api is that there are senders of messages and there may or may not be receivers of the messages sent. The sender of the message does so by a call to PostMessage, the sender needs not know if anyone is listening for this message or not it just needs to specify the type of message and optionally some data to send with it. The receivers of the message are any Function or Bound Method that has been subscribed to a type of message by making a call to Subscribe and providing it with a callback function to associate with. The callbacks are called upon when a message of the type they are associated with is sent by a sender. The callbacks are kept with weak references so they are only kept on the list of receivers as long as its still alive.

Message Types:

All messages sent with PostMessage are associated with a message type. Message types are really a tree structure that breaks messages down into more specific types as they traverse down the hierarchy. The message types used internally by Editra are mostly defined in ed_msg all being constants with names that match the following pattern EDMSG_*. Before explaining how the message hierarchy works lets look at a simple diagram that shows the basic message type hierarchy used by the logging system.

        [editra]  # Main Root of all messages (EDMSG_ALL)
            \
             \
            [log] # Logs main node (EDMSG_LOG_ALL)
              |
            [info] # All core log messages with labeled types (EDMSG_LOG_INFO)
            / |  \
           /  |   \    
          /   |    \
         /    |     \
        /     |      \
    [evt]   [warn]  [err] # (EDMSG_EVENT, EDMSG_WARN, EDMSG_ERROR)

Each node in the tree is associated with a message type. By subscribing to a given message type the subscriber also subscribes to all messages that are sent to its leaves.

Example 1:

import ed_msg
def MySubscriber(msg):
    print "Got a log message", msg
 
# Receive anything sent to the log
ed_msg.Subscribe(MySubscriber, ed_msg.EDMSG_LOG_ALL)

In the above example the function MySubscriber will be called anytime a message is sent with any of the log message types (EDMSG_LOG_ALL, EDMSG_LOG_INFO, EDMSG_LOG_EVENT, EDMSG_LOG_WARN, EDMSG_LOG_ERROR).

Example 2:

import ed_msg
def MySubscriber(msg):
    print "Got an error message", msg
 
# Receive only error messages
ed_msg.Subscribe(MySubscriber, ed_msg.EDMSG_LOG_ERROR)

This time by subscribing to a node at the bottom of the tree the subscribed function will only be called when an error message is sent. Given this behavior if a subscriber was to subscribe to EDMSG_LOG_ALL and EDMSG_LOG_ERROR the subscriber would be called twice for any error messages that are sent. Shown below is a listing of the core message types that are available as of (09/15/2009). Core Message Types:

# Message Type Definitions
#---- General Messages ----#
 
# Listen to all messages
EDMSG_ALL = ('editra',)
 
#---- End General Messages ----#
 
#---- Log Messages ----#
# Used internally by the log system. Listed by priority lowest -> highest
# All message data from these functions are a LogMsg object which is a
# container object for the message string / timestamp / type
#
# Using these message types with the PostMessage method is not suggested for
# use in user code instead use the logging facilities (wx.GetApp().GetLog() or
# util.Getlog() ) as they will handle the formatting that is expected by the 
# log messaging listeners.
 
# Recieve all log messages (i.e anything put on the logging system)
EDMSG_LOG_ALL = EDMSG_ALL + ('log',)
 
# Recieve all messages that have been labled (info, events, warnings, errors)
EDMSG_LOG_INFO = EDMSG_LOG_ALL + ('info',)
 
# Messages generated by ui events
EDMSG_LOG_EVENT = EDMSG_LOG_INFO + ('evt',)
 
# Recieve only warning messages
EDMSG_LOG_WARN = EDMSG_LOG_INFO + ('warn',)
 
# Recieve only error messages
EDMSG_LOG_ERROR = EDMSG_LOG_INFO + ('err',)
 
#---- End Log Messages ----#
 
#---- File Action Messages ----#
 
# Recieve notification of all file actions
EDMSG_FILE_ALL = EDMSG_ALL + ('file',)
 
# File open was just requested / msgdata == file path
EDMSG_FILE_OPENING = EDMSG_FILE_ALL + ('opening',)
 
# File was just opened / msgdata == file path
# context == MainWindows ID
EDMSG_FILE_OPENED = EDMSG_FILE_ALL + ('opened',)
 
# TODO: using MainWindow as context for now, but may make more sense to use
#       the buffer instead.
 
# File save requested / msgdata == (filename, filetypeId)
# context == MainWindows ID
# Note: All listeners of this message are processed *before* the save takes
#       place. Meaning the listeners block the save action until they are
#       finished.
EDMSG_FILE_SAVE = EDMSG_FILE_ALL + ('save',)
 
# File just written to disk / msgdata == (filename, filetypeId)
# context == MainWindows ID
EDMSG_FILE_SAVED = EDMSG_FILE_ALL + ('saved',)
 
#---- End File Action Messages ----#
 
#---- UI Action Messages ----#
 
# Recieve notification of all ui typed messages
EDMSG_UI_ALL = EDMSG_ALL + ('ui',)
 
#- Recieve all Main Notebook Messages
EDMSG_UI_NB = EDMSG_UI_ALL + ('mnotebook',)
 
# Notebook page changing
# msgdata == (ref to notebook, 
#             index of previous selection,
#             index of current selection)
# context == MainWindow ID
EDMSG_UI_NB_CHANGING = EDMSG_UI_NB + ('pgchanging',)
 
# Notebook page changed
# msgdata == (ref to notebook, index of currently selected page)
# context == MainWindow ID
EDMSG_UI_NB_CHANGED = EDMSG_UI_NB + ('pgchanged',)
 
# Page is about to close
# msgdata == (ref to notebook, index of page that is closing)
# context == MainWindow ID
EDMSG_UI_NB_CLOSING = EDMSG_UI_NB + ('pgclosing',)
 
# Page has just been closed
# msgdata == (ref to notebook, index of page that is now selected)
# context == MainWindow ID
EDMSG_UI_NB_CLOSED = EDMSG_UI_NB + ('pgclosed',)
 
# Post message to show the progress indicator of the MainWindow
# msgdata == (frame id, True / False)
EDMSG_PROGRESS_SHOW = EDMSG_UI_ALL + ('statbar', 'progbar', 'show')
 
# Post this message to manipulate the state of the MainWindows status bar
# progress indicator. The message data should be a three tuple of the recipient
# frames id, current progress and the total range (current, total). If both 
# values are 0 then the bar will be hidden. If both are negative the bar will 
# be set into pulse mode. This message can safely be sent from background 
# threads.
EDMSG_PROGRESS_STATE = EDMSG_UI_ALL + ('statbar', 'progbar', 'state')
 
# Set the status text
# msgdata == (field id, text)
EDMSG_UI_SB_TXT = EDMSG_UI_ALL + ('statbar', 'text')
 
## Text Buffer ##
 
# Root message for the text buffer
EDMSG_UI_STC_ALL = EDMSG_UI_ALL + ('stc',)
 
# msgdata == ((x, y), keycode)
EDMSG_UI_STC_KEYUP = EDMSG_UI_STC_ALL + ('keyup',)
 
# msgdata == dict(lnum=line, cnum=column)
# context == MainWindows ID
EDMSG_UI_STC_POS_CHANGED = EDMSG_UI_STC_ALL + ('position',)
 
# msgdata == dict(fname=fname,
#                 prepos=pos, preline=line,
#                 lnum=cline, pos=cpos)
# context == MainWIndow ID
EDMSG_UI_STC_POS_JUMPED = EDMSG_UI_STC_ALL + ('jump',)
 
# Editor control size restored (msgdata == None)
EDMSG_UI_STC_RESTORE = EDMSG_UI_STC_ALL + ('restore',)
 
# Lexer Changed
# msgdata == (filename, filetype id)
# context == MainWindows ID
EDMSG_UI_STC_LEXER = EDMSG_UI_STC_ALL + ('lexer',)
 
# Buffer Changed
# NOTE: this gets called ALOT so be very efficient in any handlers of it!
# msgdata == None
EDMSG_UI_STC_CHANGED = EDMSG_UI_STC_ALL + ('changed',)
 
#---- End UI Action Messages ----#
 
#---- Menu Messages ----#
EDMSG_MENU = EDMSG_ALL + ('menu',)
 
# Signal to all windows to update keybindings (msgdata == None)
EDMSG_MENU_REBIND = EDMSG_MENU + ('rebind',)
 
# Message to set key profile
# msgdata == keyprofile name
EDMSG_MENU_LOADPROFILE = EDMSG_MENU + ('load',)
 
#---- End Menu Messages ----#
 
#---- Find Actions ----#
 
EDMSG_FIND_ALL = EDMSG_ALL + ('find',)
 
# Show or modify an existing find dialog
# msgdata = dict(mw, lookin, searchtxt, replacetxt)
EDMSG_FIND_SHOW_DLG = EDMSG_FIND_ALL + ('show',)
 
# Message to request a search job
# msgdata == (callable, args, kwargs)
# msgdata == (callable)
EDMSG_START_SEARCH = EDMSG_FIND_ALL + ('results',)
 
#---- End Find Actions ----#
 
#---- Misc Messages ----#
# Signal that the icon theme has changed. Respond to this to update icon
# resources from the ArtProvider.
EDMSG_THEME_CHANGED = EDMSG_ALL + ('theme',) # All theme listeners
 
# Update the theme the notebook specifically to the current preferences
EDMSG_THEME_NOTEBOOK = EDMSG_ALL + ('nb', 'theme')
 
# Signal that the font preferences for the ui have changed (msgdata == font)
EDMSG_DSP_FONT = EDMSG_ALL + ('dfont',)
 
#---- End Misc Messages ----#

Defining New Message Types

Often times it may be desirable to define a new message type to use for sending messages to different parts of the application to add notification of new action types or events. This is quite easily done by creating a new leaf in a given message categories node.

Example:

# Make a new top level message category
# Note: its not required but it is suggested to append all 
#       new top level message categories to EDMSG_ALL
MYMSG_MYCOMPONENT = ed_msg.EDMSG_ALL + ('mycomponent',)
 
# Add a new message type to the category
MYMSG_MYCOMPONENT_HELLO = MYMSG_MYCOMPONENT + ('hello',)
 
# Add a new message type to existing node
MYMSG_SUPER_FILE_ACTION = ed_msg.EDMSG_FILE_ALL + ('mynewfileaction',)

Here is an abbreviated version of what the message tree would look like with new message types defined

          [editra]
           /    \
          /      \
         /        \
        /          \
  [mycomponent]  [file]  # MYMSG_MYCOMPONENT, EDMSG_FILE_ALL
      |          /  |  \
      |         /   |   \
      |        /    |    \
   [hello]  [save][saved][mynewfileaction] # MYMSG_MYCOMPONENT_HELLO, EDMSG_FILE_SAVE,
                                           # EDMSG_FILE_SAVED, MYMSG_SUPER_FILE_ACTION

Message Objects

The functions/methods that are subscribed to a given message type will receive a Message object that contains the message type identifier and optionally any data that may have been sent with the message. These two pieces of information are accessed using two getter functions. Simple usage example shown below, following from the example in the above section.

Example:

>>> import ed_msg
>>> def MyReciever(msg):
...     """Receives messages and prints the data"""
...     print "Message Type:", msg.GetType()
...     print "Message Data:", msg.GetData()
>>> ed_msg.Subscribe(MyReciever, MYMSG_MYCOMPONENT)
>>> ed_msg.PostMessage(MYMSG_MYCOMPONENT, "Hello World"
Message Type: ('editra', 'mycomponent')
Message Data: Hello World
>>>

The data returned by the GetData method will be what ever was packed into the message when it was sent. The data will vary greatly from one message type to the next. All of the core message types defined in ed_msg are documented and say what the structure of the data object is. The message data defines the interface that listeners expect to receive when they get a message. So when sending messages on existing channels its important to follow the interface of the data defined by that message type.


Links: