source: trunk/GSASIIlog.py @ 1513

Last change on this file since 1513 was 1513, checked in by toby, 7 years ago

update and rebuild docs

  • Property svn:eol-style set to native
File size: 20.4 KB
Line 
1# -*- coding: utf-8 -*-
2#GSASIIlog - Routines used to track and replay "actions"
3########### SVN repository information ###################
4# $Date: $
5# $Author: toby $
6# $Revision: $
7# $URL: $
8# $Id: $
9########### SVN repository information ###################
10'''
11*GSASIIlog: Logging of "Actions"*
12---------------------------------
13
14Module to provide logging services, e.g. track and replay "actions"
15such as menu item, tree item, button press, value change and so on.
16'''
17import wx
18import GSASIIgrid as G2gd
19import GSASIIpath
20# Global variables
21MenuBindingLookup = {}
22'Lookup table for Menu buttons'
23ButtonBindingLookup = {}
24'Lookup table for button objects'
25G2logList = [None]
26'Contains a list of logged actions; first item is ignored'
27LogInfo = {'Logging':False, 'Tree':None, 'LastPaintAction':None}
28'Contains values that are needed in the module for past actions & object location'
29
30# TODO:
31# Might want to save the last displayed screen with some objects to make sure
32# the commands are executed in a sensible order
33# 1) save tree press under tab press item.
34# 2) save tree & tab for button press
35
36# TODO:
37# probably need an object for saving calls and arguments to call specific functions/methods.
38# The items to be called need to register themselves so that they can be found
39#   This needs to be done with a Register(function,'unique-string') call after every def
40#   and then a LogCall('unique-string',pargs,kwargs) call inside the routine
41
42debug = GSASIIpath.GetConfigValue('logging_debug')
43#===========================================================================
44# objects for logging variables
45def _l2s(lst,separator='+'):
46    'Combine a list of objects into a string, with a separator'
47    s = ''
48    for i in lst: 
49        if s != '': s += separator
50        s += '"'+str(i)+'"'
51    return s
52
53class LogEntry(object):
54    '''Base class to define logging objects. These store information on events
55    in a manner that can be pickled and saved -- direct references to wx objects
56    is not allowed.
57
58    Each object must define:
59   
60     *  __init__: stores the information needed to log & later recreate the action
61     *  __str__ : shows a nice ASCII string for each action
62     *  Replay:   recreates the action when the log is played
63     
64    optional:
65   
66      * Repaint:  redisplays the current window
67     
68    '''
69    def __init__(self):
70        # Must be overridden
71        raise Exception('No __init__ defined')
72    def __str__(self):
73        # Must be overridden
74        raise Exception('No __str__ defined')
75    def Replay(self):
76        # Must be overridden
77        raise Exception('No Replay defined')
78    def Repaint(self):
79        # optional
80        pass
81
82class VarLogEntry(LogEntry):
83    'object that tracks changes to a variable'
84    def __init__(self,treeRefs,indexRefs,value):
85        self.treeRefs = treeRefs
86        self.indexRefs = indexRefs
87        self.value = value
88        if debug: print 'Logging var change: w/treeRefs',treeRefs,'indexRefs',indexRefs,'new value=',value
89    def __str__(self):
90        treeList = self.treeRefs[:]
91        if type(treeList[0]) is tuple:
92            treeList[0] = 'Hist #'+str(treeList[0][1]+1)+' of type '+treeList[0][0]
93        elif len(treeList) > 1 and type(treeList[1]) is int:
94            treeList[1] = 'Phase #'+str(treeList[1]+1)
95        return 'Variable change: Key(s)= '+_l2s(self.indexRefs)+' to value='+str(self.value)
96    def Replay(self):
97        'Perform a Variable Change action, when read from the log'
98        parentId = LogInfo['Tree'].root
99        for i,treeitem in enumerate(self.treeRefs):
100            if i == 0 and type(treeitem) is tuple:
101                treeitem = LogInfo['Tree'].ConvertRelativeHistNum(*treeitem)
102            item, cookie = LogInfo['Tree'].GetFirstChild(parentId)
103            while item:
104                if LogInfo['Tree'].GetItemText(item) == treeitem:
105                    parentId = item
106                    break
107                else:
108                    item, cookie = LogInfo['Tree'].GetNextChild(parentId, cookie)
109            else:
110                raise Exception("Tree item not found for "+str(self))
111        # get the inner most data array
112        data = LogInfo['Tree'].GetItemPyData(item)
113        for item in self.indexRefs[:-1]:
114            data = data[item]
115        # set the value
116        data[self.indexRefs[-1]] = self.value
117
118class MenuLogEntry(LogEntry):
119    'object that tracks when a menu command is executed'
120    def __init__(self,menulabellist):
121        self.menulabellist = menulabellist
122        if debug:
123            t = menulabellist[:]
124            t.reverse()
125            l = ''
126            for item in t:
127                if l: l += ' -> '
128                l += item
129            print 'Logging menu command: '+l
130    def __str__(self):
131        return 'Menu press: From '+_l2s(self.menulabellist,'/')
132    def Replay(self):
133        'Perform a Menu item action when read from the log'
134        key = ''
135        for item in self.menulabellist:
136            if key: key += '+'
137            key += item
138        if MenuBindingLookup.get(key):
139            handler,id,menuobj = MenuBindingLookup[key]
140            MyEvent = wx.CommandEvent(wx.EVT_MENU.typeId, id)
141            MyEvent.SetEventObject(menuobj)
142            handler(MyEvent)
143        else:
144            raise Exception('No binding for menu item '+key)       
145           
146class TabLogEntry(LogEntry):
147    'Object to track when tabs are pressed in the DataFrame window'
148    def __init__(self,title,tabname):
149        self.wintitle = title
150        self.tablabel = tabname
151        if debug: print 'Logging tab: "'+tabname+'" on window titled '+title
152    def __str__(self):
153        return 'Tab press: Tab='+_l2s([self.tablabel])+' on window labeled '+str(self.wintitle)
154    def Repaint(self):
155        'Used to redraw a window created in response to a Tab press'
156        if debug: print 'Repaint'
157        saveval = LogInfo['LastPaintAction']
158        self.Replay()
159        LogInfo['LastPaintAction'] = saveval
160    def Replay(self):
161        'Perform a Tab press action when read from the log'
162        wintitle = self.wintitle
163        tabname = self.tablabel
164        LogInfo['LastPaintAction'] = self
165        if LogInfo['Tree'].G2frame.dataFrame.GetTitle() != wintitle:
166            print LogInfo['Tree'].G2frame.dataFrame.GetTitle(),' != ',wintitle
167            raise Exception('tab in wrong window')
168        for PageNum in range(LogInfo['Tree'].G2frame.dataDisplay.GetPageCount()):
169            if tabname == LogInfo['Tree'].G2frame.dataDisplay.GetPageText(PageNum):
170                LogInfo['Tree'].G2frame.dataDisplay.SetSelection(PageNum)
171                return
172        else:
173            print tabname,'not in',[
174                LogInfo['Tree'].G2frame.dataDisplay.GetPageText(PageNum) for
175                PageNum in range(LogInfo['Tree'].G2frame.dataDisplay.GetPageCount())]
176            raise Exception('tab not found')
177def MakeTabLog(title,tabname):
178    'Create a TabLogEntry action log'
179    G2logList.append(TabLogEntry(title,tabname))
180
181class TreeLogEntry(LogEntry):
182    'Object to track when tree items are pressed in the main window'
183    def __init__(self,itemlist):
184        self.treeItemList = itemlist
185        if debug: print 'Logging press on tree: ',itemlist
186    def __str__(self):
187        treeList = self.treeItemList[:]
188        if type(treeList[0]) is tuple:
189            treeList[0] = 'Hist #'+str(treeList[0][1]+1)+' of type '+treeList[0][0]
190        elif len(treeList) > 1 and type(treeList[1]) is int:
191            treeList[1] = 'Phase #'+str(treeList[1]+1)
192        return 'Tree item pressed: '+_l2s(treeList)
193    def Repaint(self):
194        'Used to redraw a window created in response to a click on a data tree item'
195        if debug: print 'Repaint'
196        saveval = LogInfo['LastPaintAction']
197        LogInfo['Tree'].SelectItem(LogInfo['Tree'].root) # need to select something else
198        wx.Yield()
199        self.Replay()
200        LogInfo['LastPaintAction'] = saveval
201    def Replay(self):
202        'Perform a Tree press action when read from the log'
203        LogInfo['LastPaintAction'] = self
204        parent = LogInfo['Tree'].root
205        for i,txt in enumerate(self.treeItemList):
206            if i == 0 and type(txt) is tuple:
207                txt = LogInfo['Tree'].ConvertRelativeHistNum(*txt)
208            elif i == 1 and type(txt) is int and self.treeItemList[0] == "Phases":
209                txt = LogInfo['Tree'].ConvertRelativePhaseNum(txt)
210            item = G2gd.GetPatternTreeItemId(LogInfo['Tree'].G2frame,parent,txt)
211            if not item:
212                print 'Not found',txt
213                return
214            else:
215                parent = item
216        else:
217            LogInfo['Tree'].SelectItem(item)
218def MakeTreeLog(textlist):
219    'Create a TreeLogEntry action log'
220    G2logList.append(TreeLogEntry(textlist))
221   
222class ButtonLogEntry(LogEntry):
223    'Object to track button press'
224    def __init__(self,locationcode,label):
225        self.locationcode = locationcode
226        self.label = label
227        if debug: print 'Logging '+label+' button press in '+locationcode
228    def __str__(self):
229        return 'Press of '+self.label+' button in '+self.locationcode
230    def Replay(self):
231        key = self.locationcode + '+' + self.label
232        if ButtonBindingLookup.get(key):
233            btn = ButtonBindingLookup[key]
234            clickEvent = wx.CommandEvent(wx.EVT_BUTTON.typeId, btn.GetId())
235            clickEvent.SetEventObject(btn)
236            #btn.GetEventHandler().ProcessEvent(clickEvent)
237            btn.handler(clickEvent)
238def MakeButtonLog(locationcode,label):
239    'Create a ButtonLogEntry action log'
240    G2logList.append(ButtonLogEntry(locationcode,label))
241   
242           
243def _wrapper(func):
244    def _wrapped(self, *args, **kwargs):
245        return getattr(self.obj, func)(*args, **kwargs)
246    return _wrapped
247
248class DictMeta(type):
249    def __new__(cls, name, bases, dct):
250        default_attrs = dir(object) + ['__getitem__', '__str__']
251        for attr in dir(dict):
252            if attr not in default_attrs:
253                dct[attr] = _wrapper(attr)
254        return type.__new__(cls, name, bases, dct)
255
256class dictLogged(object):
257    '''A version of a dict object that tracks the source of the
258    object back to the location on the G2 tree.
259    If a list (tuple) or dict are pulled from inside this object
260    the source information is appended to the provinance tracking
261    lists.
262
263    tuples are converted to lists.
264    '''
265    __metaclass__ = DictMeta
266
267    def __init__(self, obj, treeRefs, indexRefs=[]):
268        self.treeRefs = treeRefs
269        self.indexRefs = indexRefs
270        self.obj = obj
271
272    def __getitem__(self,key):
273        val = self.obj.__getitem__(key)   
274        if type(val) is tuple:
275            #if debug: print 'Converting to list',key
276            val = list(val)
277            self.obj[key] = val
278        if type(val) is dict:
279            #print 'dict'
280            return dictLogged(val,self.treeRefs,self.indexRefs+[key])
281        elif type(val) is list:
282            #print 'list'
283            return listLogged(val,self.treeRefs,self.indexRefs+[key])
284        else:
285            #print type(val)
286            return val
287
288    def __str__(self):
289        return self.obj.__str__() + " : " + str(self.treeRefs) + ',' + str(self.indexRefs)
290
291class ListMeta(type):
292    def __new__(cls, name, bases, dct):
293        default_attrs = dir(object) + ['__getitem__', '__str__']
294        for attr in dir(list):
295            if attr not in default_attrs:
296                dct[attr] = _wrapper(attr)
297        return type.__new__(cls, name, bases, dct)
298
299class listLogged(object):
300    '''A version of a list object that tracks the source of the
301    object back to the location on the G2 tree.
302    If a list (tuple) or dict are pulled from inside this object
303    the source information is appended to the provinance tracking
304    lists.
305   
306    tuples are converted to lists.
307    '''
308    __metaclass__ = ListMeta
309
310    def __init__(self, obj, treeRefs, indexRefs=[]):
311        self.treeRefs = treeRefs
312        self.indexRefs = indexRefs
313        self.obj = obj
314
315    def __getitem__(self,key):
316        val = self.obj.__getitem__(key)   
317        if type(val) is tuple:
318            #if debug: print 'Converting to list',key
319            val = list(val)
320            self.obj[key] = val
321        if type(val) is dict:
322            #print 'dict'
323            return dictLogged(val,self.treeRefs,self.indexRefs+[key])
324        elif type(val) is list:
325            #print 'list'
326            return listLogged(val,self.treeRefs,self.indexRefs+[key])
327        else:
328            #print type(val)
329            return val
330
331    def __str__(self):
332        return self.obj.__str__() + " : " + str(self.treeRefs) + ',' + str(self.indexRefs)
333
334#===========================================================================
335# variable tracking
336def LogVarChange(result,key):
337    'Called when a variable is changed to log that action'
338    if not LogInfo['Logging']: return
339    if hasattr(result,'treeRefs'):
340        treevars = result.treeRefs[:]
341        treevars[0] = LogInfo['Tree'].GetRelativeHistNum(treevars[0])
342        if treevars[0] == "Phases" and len(treevars) > 1:
343            treevars[1] = LogInfo['Tree'].GetRelativePhaseNum(treevars[1])
344        lastLog = G2logList[-1]
345        fullrefs = result.indexRefs+[key]
346        if type(lastLog) is VarLogEntry:
347            if lastLog.treeRefs == treevars and lastLog.indexRefs == fullrefs:
348                lastLog.value = result[key]
349                if debug: print 'update last log to ',result[key]
350                return
351        G2logList.append(VarLogEntry(treevars,fullrefs,result[key]))
352    else:
353        print key,'Error: var change has no provenance info'
354
355#===========================================================================
356# menu command tracking
357def _getmenuinfo(id,G2frame,handler):
358    '''Look up the menu/menu-item label tree from a menuitem's Id
359   
360    Note that menubars contain multiple menus which contain multiple menuitems.
361    A menuitem can itself point to a menu and if so that menu can contain
362    multiple menuitems.
363
364    Here we start with the last menuitem and look up the label for that as well
365    as all parents, which will be found in parent menuitems (if any) and the menubar.
366   
367        menuitem    ->  menu ->  menubar
368           |                          |
369           |->itemlabel               |-> menulabel
370           
371    or
372
373        menuitem    ->  (submenu -> menuitem)*(n times) -> menu -> menubar
374           |                            |                             |
375           |->itemlabel                 |-> sublabel(s)               |-> menulabel
376           
377    :returns: a list containing all the labels and the menuitem object
378       or None if the menu object will not be cataloged.
379    '''
380    # don't worry about help menuitems
381    if id == wx.ID_ABOUT: return
382    # get the menu item object by searching through all menubars and then its label
383    for menubar in G2frame.dataMenuBars:
384        menuitem = menubar.FindItemById(id)
385        if menuitem:
386            #print 'getmenuinfo found',id,menuitem
387            break
388    else:
389        print '****** getmenuinfo failed for id=',id,'binding to=',handler
390        #raise Exception('debug: getmenuinfo failed')
391        return
392    menuLabelList = [menuitem.GetItemLabel()]
393   
394    # get the menu where the current item is located
395    menu = menuitem.GetMenu()
396    while menu.GetParent(): # is this menu a submenu of a previous menu?
397        parentmenu = menu.GetParent()
398        # cycle through the parentmenu until we find the menu
399        for i in range(parentmenu.GetMenuItemCount()):
400            if parentmenu.FindItemByPosition(i).GetSubMenu()==menu:
401                menuLabelList += [parentmenu.FindItemByPosition(i).GetItemLabel()]
402                break
403        else:
404            # menu not found in menu, something is wrong
405            print 'error tracing menuitem to parent menu',menuLabelList
406            #raise Exception('debug1: error tracing menuitem')
407            return
408        menu = parentmenu
409
410    menubar = menu.MenuBar
411    for i in range(menubar.GetMenuCount()):
412        if menubar.GetMenu(i) == menu:
413            menuLabelList += [menubar.GetMenuLabel(i)]
414            break
415    else:
416        # menu not found in menubar, something is wrong
417        print 'error tracing menuitem to menubar',menuLabelList
418        #raise Exception('debug2: error tracing menuitem')
419        return
420    return menuLabelList,menuitem
421
422def SaveMenuCommand(id,G2frame,handler):
423    '''Creates a table of menu items and their pseudo-bindings
424    '''
425    menuinfo = _getmenuinfo(id,G2frame,handler)
426    if not menuinfo: return
427    menuLabelList,menuobj = menuinfo
428    key = ''
429    for item in menuLabelList:
430        if key: key += '+'
431        key += item
432    MenuBindingLookup[key] = [handler,id,menuobj]
433    return menuLabelList
434
435def InvokeMenuCommand(id,G2frame,event):
436    '''Called when a menu item is used to log the action as well as call the
437    routine "bind"ed to that menu item
438    '''
439    menuLabelList,menuobj = _getmenuinfo(id,G2frame,None)
440    key = ''
441    if menuLabelList: 
442        for item in menuLabelList:
443            if key: key += '+'
444            key += item
445    if key in MenuBindingLookup:
446        if LogInfo['Logging']: 
447            G2logList.append(MenuLogEntry(menuLabelList))
448        handler = MenuBindingLookup[key][0]
449        handler(event)
450    else:
451        print 'Error no binding for menu command',menuLabelList,'id=',id
452        return
453
454#===========================================================================
455# Misc externally callable routines
456def LogOn():
457    'Turn On logging of actions'
458    if debug: print 'LogOn'
459    LogInfo['Logging'] = True
460
461def LogOff():
462    'Turn Off logging of actions'
463    if debug: print 'LogOff'
464    LogInfo['Logging'] = False
465   
466def ShowLogStatus():
467    'Return the logging status'
468    return LogInfo['Logging']
469
470def OnReplayPress(event):
471    'execute one or more commands when the replay button is pressed'
472    clb = LogInfo['clb']
473    dlg = clb.GetTopLevelParent()
474    sels = sorted(clb.GetSelections())
475    if not sels:
476        dlg1 = wx.MessageDialog(dlg,
477            'Select one or more items in the list box to replay',
478            'No selection actions',
479            wx.OK)
480        dlg1.CenterOnParent()
481        dlg1.ShowModal()
482        dlg1.Destroy()
483        return
484    logstat = ShowLogStatus()
485    if logstat: LogOff()
486    if debug: print 70*'='
487    for i in sels:
488        i += 1
489        item = G2logList[i]
490        if debug: print 'replaying',item
491        item.Replay()
492        wx.Yield()
493    if i >= len(G2logList)-1:
494        dlg.EndModal(wx.ID_OK)
495    else:
496        clb.DeselectAll()
497        clb.SetSelection(i)
498    if debug: print 70*'='
499    # if the last command did not display a window, repaint it in
500    # case something on that window changed.
501    if item != LogInfo['LastPaintAction'] and hasattr(LogInfo['LastPaintAction'],'Repaint'):
502        LogInfo['LastPaintAction'].Repaint()
503    if logstat: LogOn()
504     
505def ReplayLog(event):
506    'replay the logged actions'
507    LogInfo['LastPaintAction'] = None # clear the pointed to the last data window
508    # is this really needed? -- probably not.
509    commandList = []
510    for item in G2logList:
511        if item: # skip over 1st item in list (None)
512            commandList.append(str(item))
513    if not commandList:
514        dlg = wx.MessageDialog(LogInfo['Tree'],
515            'No actions found in log to replay',
516            'Empty Log',
517            wx.OK)
518        dlg.CenterOnParent()
519        dlg.ShowModal()
520        dlg.Destroy()
521        return
522    dlg = wx.Dialog(LogInfo['Tree'],wx.ID_ANY,'Replay actions from log',
523        style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER|wx.CENTRE)
524    mainSizer = wx.BoxSizer(wx.VERTICAL)
525    mainSizer.Add((5,5))
526    clb = wx.ListBox(dlg, wx.ID_ANY, (30,100), wx.DefaultSize, commandList,
527                     style=wx.LB_EXTENDED)
528    LogInfo['clb'] = clb
529    mainSizer.Add(clb,1,wx.EXPAND,1)
530    mainSizer.Add((5,5))
531    btn = wx.Button(dlg, wx.ID_ANY,'Replay selected')
532    btn.Bind(wx.EVT_BUTTON,OnReplayPress)
533    mainSizer.Add(btn,0,wx.ALIGN_CENTER,0)
534    btnsizer = wx.StdDialogButtonSizer()
535    OKbtn = wx.Button(dlg, wx.ID_OK,'Close')
536    #OKbtn = wx.Button(dlg, wx.ID_CLOSE)
537    OKbtn.SetDefault()
538    OKbtn.Bind(wx.EVT_BUTTON,lambda event: dlg.EndModal(wx.ID_OK))
539    btnsizer.AddButton(OKbtn)
540    btnsizer.Realize()
541    mainSizer.Add((-1,5),1,wx.EXPAND,1)
542    mainSizer.Add(btnsizer,0,wx.ALIGN_CENTER,0)
543    mainSizer.Add((-1,5))
544    dlg.SetSizer(mainSizer)
545    dlg.CenterOnParent()
546    clb.SetSelection(0)
547    dlg.ShowModal()
548    dlg.Destroy()
549    LogInfo['Tree'].G2frame.OnMacroRecordStatus(None) # sync the menu checkmark(s)
550    return
551
552if debug: LogOn() # for debug, start with logging enabled
Note: See TracBrowser for help on using the repository browser.