source: trunk/GSASIIlog.py @ 1521

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

Fix bug on use of str on logged objects

  • Property svn:eol-style set to native
File size: 20.5 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__()
290    def __repr__(self):
291        return self.obj.__str__() + " : " + str(self.treeRefs) + ',' + str(self.indexRefs)
292
293class ListMeta(type):
294    def __new__(cls, name, bases, dct):
295        default_attrs = dir(object) + ['__getitem__', '__str__']
296        for attr in dir(list):
297            if attr not in default_attrs:
298                dct[attr] = _wrapper(attr)
299        return type.__new__(cls, name, bases, dct)
300
301class listLogged(object):
302    '''A version of a list object that tracks the source of the
303    object back to the location on the G2 tree.
304    If a list (tuple) or dict are pulled from inside this object
305    the source information is appended to the provinance tracking
306    lists.
307   
308    tuples are converted to lists.
309    '''
310    __metaclass__ = ListMeta
311
312    def __init__(self, obj, treeRefs, indexRefs=[]):
313        self.treeRefs = treeRefs
314        self.indexRefs = indexRefs
315        self.obj = obj
316
317    def __getitem__(self,key):
318        val = self.obj.__getitem__(key)   
319        if type(val) is tuple:
320            #if debug: print 'Converting to list',key
321            val = list(val)
322            self.obj[key] = val
323        if type(val) is dict:
324            #print 'dict'
325            return dictLogged(val,self.treeRefs,self.indexRefs+[key])
326        elif type(val) is list:
327            #print 'list'
328            return listLogged(val,self.treeRefs,self.indexRefs+[key])
329        else:
330            #print type(val)
331            return val
332
333    def __str__(self):
334        return self.obj.__str__()
335    def __repr__(self):
336        return self.obj.__str__() + " : " + str(self.treeRefs) + ',' + str(self.indexRefs)
337
338#===========================================================================
339# variable tracking
340def LogVarChange(result,key):
341    'Called when a variable is changed to log that action'
342    if not LogInfo['Logging']: return
343    if hasattr(result,'treeRefs'):
344        treevars = result.treeRefs[:]
345        treevars[0] = LogInfo['Tree'].GetRelativeHistNum(treevars[0])
346        if treevars[0] == "Phases" and len(treevars) > 1:
347            treevars[1] = LogInfo['Tree'].GetRelativePhaseNum(treevars[1])
348        lastLog = G2logList[-1]
349        fullrefs = result.indexRefs+[key]
350        if type(lastLog) is VarLogEntry:
351            if lastLog.treeRefs == treevars and lastLog.indexRefs == fullrefs:
352                lastLog.value = result[key]
353                if debug: print 'update last log to ',result[key]
354                return
355        G2logList.append(VarLogEntry(treevars,fullrefs,result[key]))
356    else:
357        print key,'Error: var change has no provenance info'
358
359#===========================================================================
360# menu command tracking
361def _getmenuinfo(id,G2frame,handler):
362    '''Look up the menu/menu-item label tree from a menuitem's Id
363   
364    Note that menubars contain multiple menus which contain multiple menuitems.
365    A menuitem can itself point to a menu and if so that menu can contain
366    multiple menuitems.
367
368    Here we start with the last menuitem and look up the label for that as well
369    as all parents, which will be found in parent menuitems (if any) and the menubar.
370   
371        menuitem    ->  menu ->  menubar
372           |                          |
373           |->itemlabel               |-> menulabel
374           
375    or
376
377        menuitem    ->  (submenu -> menuitem)*(n times) -> menu -> menubar
378           |                            |                             |
379           |->itemlabel                 |-> sublabel(s)               |-> menulabel
380           
381    :returns: a list containing all the labels and the menuitem object
382       or None if the menu object will not be cataloged.
383    '''
384    # don't worry about help menuitems
385    if id == wx.ID_ABOUT: return
386    # get the menu item object by searching through all menubars and then its label
387    for menubar in G2frame.dataMenuBars:
388        menuitem = menubar.FindItemById(id)
389        if menuitem:
390            #print 'getmenuinfo found',id,menuitem
391            break
392    else:
393        print '****** getmenuinfo failed for id=',id,'binding to=',handler
394        #raise Exception('debug: getmenuinfo failed')
395        return
396    menuLabelList = [menuitem.GetItemLabel()]
397   
398    # get the menu where the current item is located
399    menu = menuitem.GetMenu()
400    while menu.GetParent(): # is this menu a submenu of a previous menu?
401        parentmenu = menu.GetParent()
402        # cycle through the parentmenu until we find the menu
403        for i in range(parentmenu.GetMenuItemCount()):
404            if parentmenu.FindItemByPosition(i).GetSubMenu()==menu:
405                menuLabelList += [parentmenu.FindItemByPosition(i).GetItemLabel()]
406                break
407        else:
408            # menu not found in menu, something is wrong
409            print 'error tracing menuitem to parent menu',menuLabelList
410            #raise Exception('debug1: error tracing menuitem')
411            return
412        menu = parentmenu
413
414    menubar = menu.MenuBar
415    for i in range(menubar.GetMenuCount()):
416        if menubar.GetMenu(i) == menu:
417            menuLabelList += [menubar.GetMenuLabel(i)]
418            break
419    else:
420        # menu not found in menubar, something is wrong
421        print 'error tracing menuitem to menubar',menuLabelList
422        #raise Exception('debug2: error tracing menuitem')
423        return
424    return menuLabelList,menuitem
425
426def SaveMenuCommand(id,G2frame,handler):
427    '''Creates a table of menu items and their pseudo-bindings
428    '''
429    menuinfo = _getmenuinfo(id,G2frame,handler)
430    if not menuinfo: return
431    menuLabelList,menuobj = menuinfo
432    key = ''
433    for item in menuLabelList:
434        if key: key += '+'
435        key += item
436    MenuBindingLookup[key] = [handler,id,menuobj]
437    return menuLabelList
438
439def InvokeMenuCommand(id,G2frame,event):
440    '''Called when a menu item is used to log the action as well as call the
441    routine "bind"ed to that menu item
442    '''
443    menuLabelList,menuobj = _getmenuinfo(id,G2frame,None)
444    key = ''
445    if menuLabelList: 
446        for item in menuLabelList:
447            if key: key += '+'
448            key += item
449    if key in MenuBindingLookup:
450        if LogInfo['Logging']: 
451            G2logList.append(MenuLogEntry(menuLabelList))
452        handler = MenuBindingLookup[key][0]
453        handler(event)
454    else:
455        print 'Error no binding for menu command',menuLabelList,'id=',id
456        return
457
458#===========================================================================
459# Misc externally callable routines
460def LogOn():
461    'Turn On logging of actions'
462    if debug: print 'LogOn'
463    LogInfo['Logging'] = True
464
465def LogOff():
466    'Turn Off logging of actions'
467    if debug: print 'LogOff'
468    LogInfo['Logging'] = False
469   
470def ShowLogStatus():
471    'Return the logging status'
472    return LogInfo['Logging']
473
474def OnReplayPress(event):
475    'execute one or more commands when the replay button is pressed'
476    clb = LogInfo['clb']
477    dlg = clb.GetTopLevelParent()
478    sels = sorted(clb.GetSelections())
479    if not sels:
480        dlg1 = wx.MessageDialog(dlg,
481            'Select one or more items in the list box to replay',
482            'No selection actions',
483            wx.OK)
484        dlg1.CenterOnParent()
485        dlg1.ShowModal()
486        dlg1.Destroy()
487        return
488    logstat = ShowLogStatus()
489    if logstat: LogOff()
490    if debug: print 70*'='
491    for i in sels:
492        i += 1
493        item = G2logList[i]
494        if debug: print 'replaying',item
495        item.Replay()
496        wx.Yield()
497    if i >= len(G2logList)-1:
498        dlg.EndModal(wx.ID_OK)
499    else:
500        clb.DeselectAll()
501        clb.SetSelection(i)
502    if debug: print 70*'='
503    # if the last command did not display a window, repaint it in
504    # case something on that window changed.
505    if item != LogInfo['LastPaintAction'] and hasattr(LogInfo['LastPaintAction'],'Repaint'):
506        LogInfo['LastPaintAction'].Repaint()
507    if logstat: LogOn()
508     
509def ReplayLog(event):
510    'replay the logged actions'
511    LogInfo['LastPaintAction'] = None # clear the pointed to the last data window
512    # is this really needed? -- probably not.
513    commandList = []
514    for item in G2logList:
515        if item: # skip over 1st item in list (None)
516            commandList.append(str(item))
517    if not commandList:
518        dlg = wx.MessageDialog(LogInfo['Tree'],
519            'No actions found in log to replay',
520            'Empty Log',
521            wx.OK)
522        dlg.CenterOnParent()
523        dlg.ShowModal()
524        dlg.Destroy()
525        return
526    dlg = wx.Dialog(LogInfo['Tree'],wx.ID_ANY,'Replay actions from log',
527        style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER|wx.CENTRE)
528    mainSizer = wx.BoxSizer(wx.VERTICAL)
529    mainSizer.Add((5,5))
530    clb = wx.ListBox(dlg, wx.ID_ANY, (30,100), wx.DefaultSize, commandList,
531                     style=wx.LB_EXTENDED)
532    LogInfo['clb'] = clb
533    mainSizer.Add(clb,1,wx.EXPAND,1)
534    mainSizer.Add((5,5))
535    btn = wx.Button(dlg, wx.ID_ANY,'Replay selected')
536    btn.Bind(wx.EVT_BUTTON,OnReplayPress)
537    mainSizer.Add(btn,0,wx.ALIGN_CENTER,0)
538    btnsizer = wx.StdDialogButtonSizer()
539    OKbtn = wx.Button(dlg, wx.ID_OK,'Close')
540    #OKbtn = wx.Button(dlg, wx.ID_CLOSE)
541    OKbtn.SetDefault()
542    OKbtn.Bind(wx.EVT_BUTTON,lambda event: dlg.EndModal(wx.ID_OK))
543    btnsizer.AddButton(OKbtn)
544    btnsizer.Realize()
545    mainSizer.Add((-1,5),1,wx.EXPAND,1)
546    mainSizer.Add(btnsizer,0,wx.ALIGN_CENTER,0)
547    mainSizer.Add((-1,5))
548    dlg.SetSizer(mainSizer)
549    dlg.CenterOnParent()
550    clb.SetSelection(0)
551    dlg.ShowModal()
552    dlg.Destroy()
553    LogInfo['Tree'].G2frame.OnMacroRecordStatus(None) # sync the menu checkmark(s)
554    return
555
556if debug: LogOn() # for debug, start with logging enabled
Note: See TracBrowser for help on using the repository browser.