source: trunk/GSASIIlog.py @ 3023

Last change on this file since 3023 was 3023, checked in by odonnell, 4 years ago

remove wx dependency for GSASIIscriptable

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