source: trunk/GSASIIlog.py @ 1512

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

implement logging and config storage

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