source: trunk/GSASIIlog.py @ 3136

Last change on this file since 3136 was 3136, checked in by vondreele, 4 years ago

make GSAS-II python 3.6 compliant & preserve python 2.7 use;changes:
do from future import division, print_function for all GSAS-II py sources
all menu items revised to be py 2.7/3.6 compliant
all wx.OPEN --> wx.FD_OPEN in file dialogs
all integer divides (typically for image pixel math) made explicit with ; ambiguous ones made floats as appropriate
all print "stuff" --> print (stuff)
all print >> pFile,'stuff' --> pFile.writeCIFtemplate('stuff')
all read file opens made explicit 'r' or 'rb'
all cPickle imports made for py2.7 or 3.6 as cPickle or _pickle; test for '2' platform.version_tuple[0] for py 2.7
define cPickleload to select load(fp) or load(fp,encoding='latin-1') for loading gpx files; provides cross compatibility between py 2.7/3.6 gpx files
make dict.keys() as explicit list(dict.keys()) as needed (NB: possible source of remaining py3.6 bugs)
make zip(a,b) as explicit list(zip(a,b)) as needed (NB: possible source of remaining py3.6 bugs)
select unichr/chr according test for '2' platform.version_tuple[0] for py 2.7 (G2pwdGUI * G2plot) for special characters
select wg.EVT_GRID_CELL_CHANGE (classic) or wg.EVT_GRID_CELL_CHANGED (phoenix) in grid Bind
maxint --> maxsize; used in random number stuff
raise Exception,"stuff" --> raise Exception("stuff")
wx 'classic' sizer.DeleteWindows?() or 'phoenix' sizer.Clear(True)
wx 'classic' SetToolTipString?(text) or 'phoenix' SetToolTip?(wx.ToolTip?(text)); define SetToolTipString?(self,text) to handle the choice in plots
status.SetFields? --> status.SetStatusText?
'classic' AddSimpleTool? or 'phoenix' self.AddTool? for plot toolbar; Bind different as well
define GetItemPydata? as it doesn't exist in wx 'phoenix'
allow python versions 2.7 & 3.6 to run GSAS-II
Bind override commented out - no logging capability (NB: remove all logging code?)
all import ContentsValidator? open filename & test if valid then close; filepointer removed from Reader
binary importers (mostly images) test for 'byte' type & convert as needed to satisfy py 3.6 str/byte rules

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