1
###############################################################################
3
# Purpose: Provide a messaging/notification system for actions performed in #
5
# Author: Cody Precord <cprecord@editra.org> #
6
# Copyright: (c) 2008 Cody Precord <staff@editra.org> #
7
# License: wxWindows License #
8
###############################################################################
11
This module provides a light wrapping of a slightly modified pubsub module
12
to give it a lighter and simpler syntax for usage. It exports three main
13
methods. The first L{PostMessage} which is used to post a message for all
14
interested listeners. The second L{Subscribe} which allows an object to
15
subscribe its own listener function for a particular message type, all of
16
Editra's core message types are defined in this module using a naming
17
convention that starts each identifier with I{EDMSG_}. These identifier
18
constants can be used to identify the message type by comparing them with the
19
value of msg.GetType in a listener method. The third method is L{Unsubscribe}
20
which can be used to remove a listener from recieving messages.
22
@summary: Message system api and message type definitions
26
__author__ = "Cody Precord <cprecord@editra.org>"
27
__svnid__ = "$Id: ed_msg.py 71697 2012-06-08 15:20:22Z CJP $"
28
__revision__ = "$Revision: 71697 $"
30
__all__ = ['PostMessage', 'Subscribe', 'Unsubscribe']
32
#--------------------------------------------------------------------------#
34
from wx import PyDeadObjectError
35
from extern.pubsub import Publisher
37
#--------------------------------------------------------------------------#
38
# Message Type Definitions
40
#---- General Messages ----#
42
# Listen to all messages
43
EDMSG_ALL = ('editra',)
45
#---- End General Messages ----#
47
#---- Log Messages ----#
48
# Used internally by the log system. Listed by priority lowest -> highest
49
# All message data from these functions are a LogMsg object which is a
50
# container object for the message string / timestamp / type
52
# Using these message types with the PostMessage method is not suggested for
53
# use in user code instead use the logging facilities (wx.GetApp().GetLog() or
54
# util.Getlog() ) as they will handle the formatting that is expected by the
55
# log messaging listeners.
57
# Recieve all log messages (i.e anything put on the logging system)
58
EDMSG_LOG_ALL = EDMSG_ALL + ('log',)
60
# Recieve all messages that have been labled (info, events, warnings, errors)
61
EDMSG_LOG_INFO = EDMSG_LOG_ALL + ('info',)
63
# Messages generated by ui events
64
EDMSG_LOG_EVENT = EDMSG_LOG_INFO + ('evt',)
66
# Recieve only warning messages
67
EDMSG_LOG_WARN = EDMSG_LOG_INFO + ('warn',)
69
# Recieve only error messages
70
EDMSG_LOG_ERROR = EDMSG_LOG_INFO + ('err',)
72
#---- End Log Messages ----#
74
#---- Configuration Messages ----#
76
# These messages will be sent when there are configuration
77
# changes in the current user profile. Messages will be in
78
# the format of (editra,config,PROFILE_KEY)
79
# mdata == Profile[PROFILE_KEY]
80
EDMSG_PROFILE_CHANGE = EDMSG_ALL + ('config',)
82
#---- End Configuration Messages ----#
84
#---- File Action Messages ----#
86
# Recieve notification of all file actions
87
EDMSG_FILE_ALL = EDMSG_ALL + ('file',)
89
# File open was just requested / msgdata == file path
90
EDMSG_FILE_OPENING = EDMSG_FILE_ALL + ('opening',)
92
# File was just opened / msgdata == file path
93
# context == MainWindows ID
94
EDMSG_FILE_OPENED = EDMSG_FILE_ALL + ('opened',)
96
# Get a list of all opened files
97
# msgdata == list of file paths (out param)
98
EDMSG_FILE_GET_OPENED = EDMSG_FILE_ALL + ('allopened',)
100
# TODO: using MainWindow as context for now, but may make more sense to use
101
# the buffer instead.
103
# File save requested / msgdata == (filename, filetypeId)
104
# context == MainWindows ID
105
# Note: All listeners of this message are processed *before* the save takes
106
# place. Meaning the listeners block the save action until they are
108
EDMSG_FILE_SAVE = EDMSG_FILE_ALL + ('save',)
110
# File just written to disk / msgdata == (filename, filetypeId)
111
# context == MainWindows ID
112
EDMSG_FILE_SAVED = EDMSG_FILE_ALL + ('saved',)
114
#---- End File Action Messages ----#
116
#---- UI Action Messages ----#
118
# Recieve notification of all ui typed messages
119
EDMSG_UI_ALL = EDMSG_ALL + ('ui',)
121
#- Receive all Main Notebook Messages
122
EDMSG_UI_NB = EDMSG_UI_ALL + ('mnotebook',)
124
# MainWindow Activated
125
# msgdata == dict(active=bool)
126
# context = MainWindow ID
127
EDMSG_UI_MW_ACTIVATE = EDMSG_UI_ALL + ('mwactivate',)
129
# Notebook page changing
130
# msgdata == (ref to notebook,
131
# index of previous selection,
132
# index of current selection)
133
# context == MainWindow ID
134
EDMSG_UI_NB_CHANGING = EDMSG_UI_NB + ('pgchanging',)
136
# Notebook page changed
137
# msgdata == (ref to notebook, index of currently selected page)
138
# context == MainWindow ID
139
EDMSG_UI_NB_CHANGED = EDMSG_UI_NB + ('pgchanged',)
141
# Page is about to close
142
# msgdata == (ref to notebook, index of page that is closing)
143
# context == MainWindow ID
144
EDMSG_UI_NB_CLOSING = EDMSG_UI_NB + ('pgclosing',)
146
# Page has just been closed
147
# msgdata == (ref to notebook, index of page that is now selected)
148
# context == MainWindow ID
149
EDMSG_UI_NB_CLOSED = EDMSG_UI_NB + ('pgclosed',)
152
# msgdata == ContextMenuManager
153
# menu = ContextMenuManager.GetMenu()
154
# ContextMenuManager.AddHandler(ID_MENU_ID, handler(buffer, event))
155
# page = ContextMenuManager.GetUserData("page")
156
EDMSG_UI_NB_TABMENU = EDMSG_UI_NB + ('tabmenu',)
158
# Post message to show the progress indicator of the MainWindow
159
# msgdata == (frame id, True / False)
160
EDMSG_PROGRESS_SHOW = EDMSG_UI_ALL + ('statbar', 'progbar', 'show')
162
# Post this message to manipulate the state of the MainWindows status bar
163
# progress indicator. The message data should be a three tuple of the recipient
164
# frames id, current progress and the total range (current, total). If both
165
# values are 0 then the bar will be hidden. If both are negative the bar will
166
# be set into pulse mode. This message can safely be sent from background
168
EDMSG_PROGRESS_STATE = EDMSG_UI_ALL + ('statbar', 'progbar', 'state')
170
# Set the status text
171
# msgdata == (field id, text)
172
EDMSG_UI_SB_TXT = EDMSG_UI_ALL + ('statbar', 'text')
176
# Root message for the text buffer
177
EDMSG_UI_STC_ALL = EDMSG_UI_ALL + ('stc',)
179
# msgdata == ((x, y), keycode)
180
# context == MainWindows ID
181
EDMSG_UI_STC_KEYUP = EDMSG_UI_STC_ALL + ('keyup',)
183
# msgdata == dict(lnum=line, cnum=column)
184
# context == MainWindows ID
185
EDMSG_UI_STC_POS_CHANGED = EDMSG_UI_STC_ALL + ('position',)
187
# msgdata == dict(fname=fname,
188
# prepos=pos, preline=line,
189
# lnum=cline, pos=cpos)
190
# context == MainWindow ID
191
EDMSG_UI_STC_POS_JUMPED = EDMSG_UI_STC_ALL + ('jump',)
193
# Editor control size restored (msgdata == None)
194
EDMSG_UI_STC_RESTORE = EDMSG_UI_STC_ALL + ('restore',)
197
# msgdata == (filename, filetype id)
198
# context == MainWindows ID
199
EDMSG_UI_STC_LEXER = EDMSG_UI_STC_ALL + ('lexer',)
202
# NOTE: this gets called ALOT so be very efficient in any handlers of it!
204
EDMSG_UI_STC_CHANGED = EDMSG_UI_STC_ALL + ('changed',)
206
# Customize Context Menu
207
# Add custom menu items and handlers to the buffers right click menu
208
# msgdata = ContextMenuManager
209
# ContextMenuManager.AddHandler(menu_id, handler)
210
# menu = ContextMenuManager.GetMenu()
211
# def handler(buffer, event_obj)
212
# ContextMenuManager.GetData('buffer')
213
EDMSG_UI_STC_CONTEXT_MENU = EDMSG_UI_STC_ALL + ('custommenu',)
216
# msgdata == dict(ltype=int, text=string, stc=EditraStc)
217
EDMSG_UI_STC_USERLIST_SEL = EDMSG_UI_STC_ALL + ('userlistsel',)
220
# mdata = dict(stc=self, pos=position,
222
# word=word_under_cursor
224
# If the handler for this method wants to show a calltip
225
# it should set the rdata value
226
EDMSG_UI_STC_DWELL_START = EDMSG_UI_STC_ALL + ('dwellstart',)
230
EDMSG_UI_STC_DWELL_END = EDMSG_UI_STC_ALL + ('dwellend',)
232
# Bookmark (added/deleted)
233
# mdata = dict(stc=EditraStc, added=bool, line=line, handle=bookmarkhandle)
234
# NOTE: if line < 0, then all bookmarks removed
235
EDMSG_UI_STC_BOOKMARK = EDMSG_UI_STC_ALL + ('bookmark',)
238
# mdata = dict(stc=EditraStc, line=line, handled=bool)
239
# handled is an out param in the message data. Set to True
240
# to indicate that the click was handled.
241
EDMSG_UI_STC_MARGIN_CLICK = EDMSG_UI_STC_ALL + ('marginclick',)
243
#---- End UI Action Messages ----#
245
#---- Menu Messages ----#
246
EDMSG_MENU = EDMSG_ALL + ('menu',)
248
# Signal to all windows to update keybindings (msgdata == None)
249
EDMSG_MENU_REBIND = EDMSG_MENU + ('rebind',)
251
# Message to set key profile
252
# msgdata == keyprofile name
253
EDMSG_MENU_LOADPROFILE = EDMSG_MENU + ('load',)
255
# Message to recreate the lexer menu
257
EDMSG_CREATE_LEXER_MENU = EDMSG_MENU + ('lexer',)
259
#---- End Menu Messages ----#
261
#---- Find Actions ----#
263
EDMSG_FIND_ALL = EDMSG_ALL + ('find',)
265
# Show or modify an existing find dialog
266
# msgdata = dict(mw, lookin, searchtxt, replacetxt)
267
EDMSG_FIND_SHOW_DLG = EDMSG_FIND_ALL + ('show',)
269
# Message to request a search job
270
# msgdata == (callable, args, kwargs)
271
# msgdata == (callable)
272
EDMSG_START_SEARCH = EDMSG_FIND_ALL + ('results',)
274
#---- End Find Actions ----#
276
#---- Session Related Actions ----#
278
# Root Session Message
279
EDMSG_SESSION_ALL = ('session',)
281
# Initiate a Session Save to save current session under new name.
282
# Note: This invokes a UI action to prompt the user on what to name
283
# the session. DO NOT call from background threads.
284
# send(msgdata == None) | context - MainWindow
285
EDMSG_SESSION_DO_SAVE = EDMSG_SESSION_ALL + ('dosave',)
287
# Initiate loading a session
288
# Causes all currently open files to be closed and the files from the
289
# specified session to be loaded. Not thread safe.
290
# send(msgdata == session_name)
291
EDMSG_SESSION_DO_LOAD = EDMSG_SESSION_ALL + ('doload',)
293
#---- End Session Related Actions ----#
295
#---- Misc Messages ----#
296
# Signal that the icon theme has changed. Respond to this to update icon
297
# resources from the ArtProvider.
298
EDMSG_THEME_CHANGED = EDMSG_ALL + ('theme',) # All theme listeners
300
# Update the theme the notebook specifically to the current preferences
301
EDMSG_THEME_NOTEBOOK = EDMSG_ALL + ('nb', 'theme')
303
# Signal that the font preferences for the ui have changed (msgdata == font)
304
EDMSG_DSP_FONT = EDMSG_ALL + ('dfont',)
306
# Add file to file history
307
# msgdata == filename
308
EDMSG_ADD_FILE_HISTORY = EDMSG_ALL + ('filehistory',)
310
#---- End Misc Messages ----#
312
#--------------------------------------------------------------------------#
314
_ThePublisher = Publisher()
316
def PostMessage(msgtype, msgdata=None, context=None):
317
"""Post a message containing the msgdata to all listeners that are
318
interested in the given msgtype from the given context. If context
319
is None than default context is assumed.
320
Message is always propagated to the default context.
321
@param msgtype: Message Type EDMSG_*
322
@keyword msgdata: Message data to pass to listener (can be anything)
323
@keyword context: Context of the message.
326
_ThePublisher.sendMessage(msgtype, msgdata, context=context)
328
def Subscribe(callback, msgtype=EDMSG_ALL):
329
"""Subscribe your listener function to listen for an action of type msgtype.
330
The callback must be a function or a _bound_ method that accepts one
331
parameter for the actions message. The message that is sent to the callback
332
is a class object that has two attributes, one for the message type and the
333
other for the message data. See below example for how these two values can
335
>>> def MyCallback(msg):
336
print "Msg Type: ", msg.GetType(), "Msg Data: ", msg.GetData()
339
def MyCallbackMeth(self, msg):
340
print "Msg Type: ", msg.GetType(), "Msg Data: ", msg.GetData()
342
>>> Subscribe(MyCallback, EDMSG_SOMETHING)
344
>>> Subscribe(myfoo.MyCallBackMeth, EDMSG_SOMETHING)
346
@param callback: Callable function or bound method
347
@keyword msgtype: Message to subscribe to (default to all)
350
_ThePublisher.subscribe(callback, msgtype)
352
def Unsubscribe(callback, messages=None):
353
"""Remove a listener so that it doesn't get sent messages for msgtype. If
354
msgtype is not specified the listener will be removed for all msgtypes that
355
it is associated with.
356
@param callback: Function or bound method to remove subscription for
357
@keyword messages: EDMSG_* val or list of EDMSG_* vals
360
Publisher().unsubscribe(callback, messages)
363
#---- Helper Decorators ----#
366
"""Helper decorator for checking if the message is in context of the
367
main window. Class that uses this to wrap its message handlers must
368
have a GetMainWindow method that returns a reference to the MainWindow
369
instance that owns the object.
370
@param funct: callable(self, msg)
373
def ContextWrap(self, msg):
374
"""Check and only call the method if the message is in the
375
context of the main window or no context was specified.
378
if hasattr(self, 'GetMainWindow'):
379
mw = self.GetMainWindow()
380
elif hasattr(self, 'MainWindow'):
383
assert False, "Must declare a GetMainWindow method"
384
context = msg.GetContext()
385
if context is None or mw.GetId() == context:
388
ContextWrap.__name__ = func.__name__
389
ContextWrap.__doc__ = func.__doc__
392
def wincontext(funct):
393
"""Decorator to filter messages based on a window. Class must declare
394
a GetWindow method that returns the window that the messages context
395
should be filtered on.
396
@param funct: callable(self, msg)
399
def ContextWrap(self, msg):
400
assert hasattr(self, 'GetWindow'), "Must define a GetWindow method"
401
context = msg.GetContext()
402
if isinstance(context, wx.Window) and context is self.GetWindow():
405
ContextWrap.__name__ = funct.__name__
406
ContextWrap.__doc__ = funct.__doc__
409
#-----------------------------------------------------------------------------#
412
EDREQ_ALL = ('editra', 'req')
414
EDREQ_DOCPOINTER = EDREQ_ALL + ('docpointer',)
416
#-----------------------------------------------------------------------------#
419
"""Null value to signify that a callback method should be skipped or that
420
no callback could answer the request.
426
def __nonzero__(self):
429
def RegisterCallback(callback, msgtype):
430
"""Register a callback method for the given message type
431
@param callback: callable
432
@param msgtype: message type
435
if isinstance(msgtype, tuple):
436
mtype = '.'.join(msgtype)
440
if mtype not in _CALLBACK_REGISTRY:
441
_CALLBACK_REGISTRY[mtype] = list()
443
if callback not in _CALLBACK_REGISTRY[mtype]:
444
_CALLBACK_REGISTRY[mtype].append(callback)
446
def RequestResult(msgtype, args=list()):
447
"""Request a return value result from a registered function/method.
448
If multiple callbacks have been registered for the given msgtype, the
449
first callback to return a non-NullValue will be used for the return
450
value. If L{NullValue} is returned then no callback could answer the
452
@param msgtype: Request message
453
@keyword args: Arguments to pass to the callback
456
if isinstance(msgtype, tuple):
457
mtype = '.'.join(msgtype)
463
for idx, meth in enumerate(_CALLBACK_REGISTRY.get(mtype, list())):
469
except PyDeadObjectError:
470
to_remove.append(meth)
472
if not isinstance(rval, NullValue):
475
# Remove any dead objects that may have been found
476
for val in reversed(to_remove):
478
_CALLBACK_REGISTRY.get(mtype, list()).pop(val)
484
def UnRegisterCallback(callback):
485
"""Un-Register a callback method
486
@param callback: callable
489
for key, val in _CALLBACK_REGISTRY.iteritems():
491
_CALLBACK_REGISTRY[key].remove(callback)
493
# Callback Registry for storing the methods sent in with RegisterCallback
494
_CALLBACK_REGISTRY = {}
496
#-----------------------------------------------------------------------------#