source: branch/logging/GSASIIlog.py @ 1510

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

logging pretty well ready to roll

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