source: trunk/GSASIIlog.py @ 1526

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

address menu trace problem with wx 2.9+

  • Property svn:eol-style set to native
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: $
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    i,j= wx.__version__.split('.')[0:2]
415    if int(i)+int(j)/10. > 2.8 and 'wxOSX' in wx.PlatformInfo:
416        # on mac, with wx 2.9+ the menubar has a menu and this is found above, so
417        # we are now done.
418        return menuLabelList,menuitem
419   
420    menubar = menu.MenuBar
421    for i in range(menubar.GetMenuCount()):
422        if menubar.GetMenu(i) == menu:
423            menuLabelList += [menubar.GetMenuLabel(i)]
424            return menuLabelList,menuitem
425
426    # menu not found in menubar, something is wrong
427    print 'error tracing menuitem to menubar',menuLabelList
428    #raise Exception('debug2: error tracing menuitem')
429    return
430
431def SaveMenuCommand(id,G2frame,handler):
432    '''Creates a table of menu items and their pseudo-bindings
433    '''
434    menuinfo = _getmenuinfo(id,G2frame,handler)
435    if not menuinfo: return
436    menuLabelList,menuobj = menuinfo
437    key = ''
438    for item in menuLabelList:
439        if key: key += '+'
440        key += item
441    MenuBindingLookup[key] = [handler,id,menuobj]
442    return menuLabelList
443
444def InvokeMenuCommand(id,G2frame,event):
445    '''Called when a menu item is used to log the action as well as call the
446    routine "bind"ed to that menu item
447    '''
448    menuLabelList,menuobj = _getmenuinfo(id,G2frame,None)
449    key = ''
450    if menuLabelList: 
451        for item in menuLabelList:
452            if key: key += '+'
453            key += item
454    if key in MenuBindingLookup:
455        if LogInfo['Logging']: 
456            G2logList.append(MenuLogEntry(menuLabelList))
457        handler = MenuBindingLookup[key][0]
458        handler(event)
459    else:
460        print 'Error no binding for menu command',menuLabelList,'id=',id
461        return
462
463#===========================================================================
464# Misc externally callable routines
465def LogOn():
466    'Turn On logging of actions'
467    if debug: print 'LogOn'
468    LogInfo['Logging'] = True
469
470def LogOff():
471    'Turn Off logging of actions'
472    if debug: print 'LogOff'
473    LogInfo['Logging'] = False
474   
475def ShowLogStatus():
476    'Return the logging status'
477    return LogInfo['Logging']
478
479def OnReplayPress(event):
480    'execute one or more commands when the replay button is pressed'
481    clb = LogInfo['clb']
482    dlg = clb.GetTopLevelParent()
483    sels = sorted(clb.GetSelections())
484    if not sels:
485        dlg1 = wx.MessageDialog(dlg,
486            'Select one or more items in the list box to replay',
487            'No selection actions',
488            wx.OK)
489        dlg1.CenterOnParent()
490        dlg1.ShowModal()
491        dlg1.Destroy()
492        return
493    logstat = ShowLogStatus()
494    if logstat: LogOff()
495    if debug: print 70*'='
496    for i in sels:
497        i += 1
498        item = G2logList[i]
499        if debug: print 'replaying',item
500        item.Replay()
501        wx.Yield()
502    if i >= len(G2logList)-1:
503        dlg.EndModal(wx.ID_OK)
504    else:
505        clb.DeselectAll()
506        clb.SetSelection(i)
507    if debug: print 70*'='
508    # if the last command did not display a window, repaint it in
509    # case something on that window changed.
510    if item != LogInfo['LastPaintAction'] and hasattr(LogInfo['LastPaintAction'],'Repaint'):
511        LogInfo['LastPaintAction'].Repaint()
512    if logstat: LogOn()
513     
514def ReplayLog(event):
515    'replay the logged actions'
516    LogInfo['LastPaintAction'] = None # clear the pointed to the last data window
517    # is this really needed? -- probably not.
518    commandList = []
519    for item in G2logList:
520        if item: # skip over 1st item in list (None)
521            commandList.append(str(item))
522    if not commandList:
523        dlg = wx.MessageDialog(LogInfo['Tree'],
524            'No actions found in log to replay',
525            'Empty Log',
526            wx.OK)
527        dlg.CenterOnParent()
528        dlg.ShowModal()
529        dlg.Destroy()
530        return
531    dlg = wx.Dialog(LogInfo['Tree'],wx.ID_ANY,'Replay actions from log',
532        style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER|wx.CENTRE)
533    mainSizer = wx.BoxSizer(wx.VERTICAL)
534    mainSizer.Add((5,5))
535    clb = wx.ListBox(dlg, wx.ID_ANY, (30,100), wx.DefaultSize, commandList,
536                     style=wx.LB_EXTENDED)
537    LogInfo['clb'] = clb
538    mainSizer.Add(clb,1,wx.EXPAND,1)
539    mainSizer.Add((5,5))
540    btn = wx.Button(dlg, wx.ID_ANY,'Replay selected')
541    btn.Bind(wx.EVT_BUTTON,OnReplayPress)
542    mainSizer.Add(btn,0,wx.ALIGN_CENTER,0)
543    btnsizer = wx.StdDialogButtonSizer()
544    OKbtn = wx.Button(dlg, wx.ID_OK,'Close')
545    #OKbtn = wx.Button(dlg, wx.ID_CLOSE)
546    OKbtn.SetDefault()
547    OKbtn.Bind(wx.EVT_BUTTON,lambda event: dlg.EndModal(wx.ID_OK))
548    btnsizer.AddButton(OKbtn)
549    btnsizer.Realize()
550    mainSizer.Add((-1,5),1,wx.EXPAND,1)
551    mainSizer.Add(btnsizer,0,wx.ALIGN_CENTER,0)
552    mainSizer.Add((-1,5))
553    dlg.SetSizer(mainSizer)
554    dlg.CenterOnParent()
555    clb.SetSelection(0)
556    dlg.ShowModal()
557    dlg.Destroy()
558    LogInfo['Tree'].G2frame.OnMacroRecordStatus(None) # sync the menu checkmark(s)
559    return
560
561if debug: LogOn() # for debug, start with logging enabled
Note: See TracBrowser for help on using the repository browser.