source: trunk/GSASIIlog.py @ 4818

Last change on this file since 4818 was 4800, checked in by toby, 4 years ago

finish up docs cleanups

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