source: trunk/GSASIIctrlGUI.py @ 3167

Last change on this file since 3167 was 3167, checked in by toby, 5 years ago

Misc Py3 fixes

  • Property svn:eol-style set to native
  • Property svn:keywords set to Date Author Revision URL Id
File size: 205.6 KB
Line 
1# -*- coding: utf-8 -*-
2#GSASIIctrlGUI - Custom GSAS-II GUI controls
3########### SVN repository information ###################
4# $Date: 2017-11-30 04:42:44 +0000 (Thu, 30 Nov 2017) $
5# $Author: toby $
6# $Revision: 3167 $
7# $URL: trunk/GSASIIctrlGUI.py $
8# $Id: GSASIIctrlGUI.py 3167 2017-11-30 04:42:44Z toby $
9########### SVN repository information ###################
10'''
11*GSASIIctrlGUI: Custom GUI controls*
12---------------------------------------------
13
14A library of GUI controls for reuse throughout GSAS-II
15
16'''
17from __future__ import division, print_function
18import os
19import sys
20try:
21    import wx
22    import wx.grid as wg
23    # import wx.wizard as wz
24    import wx.aui
25    import wx.lib.scrolledpanel as wxscroll
26    import wx.html        # could postpone this for quicker startup
27    import matplotlib as mpl
28except ImportError:
29    raise
30    # Dumm 'wx' so this file can be imported
31    vals = ('TreeCtrl TextCtrl PyValidator Button ComboBox Choice CheckBox'.split() +
32            'Dialog ID_ANY OPEN'.split())
33    class Placeholder(object):
34        def __init__(self, vals):
35            for val in vals:
36                setattr(self, val, object)
37        def __getattr__(self, value):
38            if value[0].isupper():
39                return object
40            return Placeholder([])
41    wx = Placeholder(vals)
42    wxscroll = Placeholder(['ScrolledPanel'])
43    if 'phoenix' in wx.version():
44        wg = Placeholder('Grid GridTableBase GridCellEditor'.split())
45    else:
46        wg = Placeholder('Grid PyGridTableBase PyGridCellEditor'.split())
47import time
48import copy
49import webbrowser     # could postpone this for quicker startup
50
51import GSASIIpath
52GSASIIpath.SetVersionNumber("$Revision: 3167 $")
53import GSASIIdataGUI as G2gd
54import GSASIIpwdGUI as G2pdG
55import GSASIIpy3 as G2py3
56import GSASIIlog as log
57import GSASIIobj as G2obj
58
59# Define a short names for convenience
60WHITE = (255,255,255)
61DULL_YELLOW = (230,230,190)
62# Don't depend on wx, for scriptable
63try:
64    VERY_LIGHT_GREY = wx.Colour(235,235,235)
65    WACV = wx.ALIGN_CENTER_VERTICAL
66except:
67    # Don't depend on GUI
68    pass
69
70################################################################################
71#### Fixed definitions for wx Ids
72################################################################################
73def Define_wxId(*args):
74    '''routine to create unique global wx Id symbols in this module.
75    '''
76    for arg in args:
77        if GSASIIpath.GetConfigValue('debug') and not arg.startswith('wxID_'):
78            print ('Problem in name'+arg)
79        if arg in globals():
80            if GSASIIpath.GetConfigValue('debug'): print (arg+'already defined')
81            continue
82        exec('global '+arg+';'+arg+' = wx.NewId()')
83
84################################################################################
85#### Tree Control
86################################################################################
87class G2TreeCtrl(wx.TreeCtrl):
88    '''Create a wrapper around the standard TreeCtrl so we can "wrap"
89    various events.
90   
91    This logs when a tree item is selected (in :meth:`onSelectionChanged`)
92
93    This also wraps lists and dicts pulled out of the tree to track where
94    they were retrieved from.
95    '''
96    #def SelectItem(self,event):
97    #    print 'Select Item'
98    #    import GSASIIobj as G2obj
99    #    G2obj.HowDidIgetHere()
100    #    wx.TreeCtrl.SelectItem(self,event)
101       
102    def __init__(self,parent=None,*args,**kwargs):
103        super(self.__class__,self).__init__(parent=parent,*args,**kwargs)
104        self.G2frame = parent.GetTopLevelParent()
105        self.root = self.AddRoot('Loaded Data: ')
106        self.SelectionChanged = None
107        self.textlist = None
108        log.LogInfo['Tree'] = self
109
110    def _getTreeItemsList(self,item):
111        '''Get the full tree hierarchy from a reference to a tree item.
112        Note that this effectively hard-codes phase and histogram names in the
113        returned list. We may want to make these names relative in the future.
114        '''
115        textlist = [self.GetItemText(item)]
116        parent = self.GetItemParent(item)
117        while parent:
118            if parent == self.root: break
119            textlist.insert(0,self.GetItemText(parent))
120            parent = self.GetItemParent(parent)
121        return textlist
122   
123    def GetItemPyData(self,id):
124        if 'phoenix' in wx.version():
125            return wx.TreeCtrl.GetItemData(self,id)
126        else:
127            return wx.TreeCtrl.GetItemPyData(self,id)
128
129    def SetItemPyData(self,id,data):
130        if 'phoenix' in wx.version():
131            return wx.TreeCtrl.SetItemData(self,id,data)
132        else:
133            return wx.TreeCtrl.SetItemPyData(self,id,data)
134
135    def onSelectionChanged(self,event):
136        '''Log each press on a tree item here.
137        '''
138        if not self.G2frame.treePanel:
139            return
140        if self.SelectionChanged:
141            textlist = self._getTreeItemsList(event.GetItem())
142            if log.LogInfo['Logging'] and event.GetItem() != self.root:
143                textlist[0] = self.GetRelativeHistNum(textlist[0])
144                if textlist[0] == "Phases" and len(textlist) > 1:
145                    textlist[1] = self.GetRelativePhaseNum(textlist[1])
146                log.MakeTreeLog(textlist)
147            if textlist == self.textlist:
148                return      #same as last time - don't get it again
149            self.textlist = textlist
150            self.SelectionChanged(event)
151
152    def Bind(self,eventtype,handler,*args,**kwargs):
153        '''Override the Bind() function so that page change events can be trapped
154        '''
155        if eventtype == wx.EVT_TREE_SEL_CHANGED:
156            self.SelectionChanged = handler
157            wx.TreeCtrl.Bind(self,eventtype,self.onSelectionChanged)
158            return
159        wx.TreeCtrl.Bind(self,eventtype,handler,*args,**kwargs)
160
161    # commented out, disables Logging
162    # def GetItemPyData(self,*args,**kwargs):
163    #    '''Override the standard method to wrap the contents
164    #    so that the source can be logged when changed
165    #    '''
166    #    data = super(self.__class__,self).GetItemPyData(*args,**kwargs)
167    #    textlist = self._getTreeItemsList(args[0])
168    #    if type(data) is dict:
169    #        return log.dictLogged(data,textlist)
170    #    if type(data) is list:
171    #        return log.listLogged(data,textlist)
172    #    if type(data) is tuple: #N.B. tuples get converted to lists
173    #        return log.listLogged(list(data),textlist)
174    #    return data
175
176    def GetRelativeHistNum(self,histname):
177        '''Returns list with a histogram type and a relative number for that
178        histogram, or the original string if not a histogram
179        '''
180        histtype = histname.split()[0]
181        if histtype != histtype.upper(): # histograms (only) have a keyword all in caps
182            return histname
183        item, cookie = self.GetFirstChild(self.root)
184        i = 0
185        while item:
186            itemtext = self.GetItemText(item)
187            if itemtext == histname:
188                return histtype,i
189            elif itemtext.split()[0] == histtype:
190                i += 1
191            item, cookie = self.GetNextChild(self.root, cookie)
192        else:
193            raise Exception("Histogram not found: "+histname)
194
195    def ConvertRelativeHistNum(self,histtype,histnum):
196        '''Converts a histogram type and relative histogram number to a
197        histogram name in the current project
198        '''
199        item, cookie = self.GetFirstChild(self.root)
200        i = 0
201        while item:
202            itemtext = self.GetItemText(item)
203            if itemtext.split()[0] == histtype:
204                if i == histnum: return itemtext
205                i += 1
206            item, cookie = self.GetNextChild(self.root, cookie)
207        else:
208            raise Exception("Histogram #'+str(histnum)+' of type "+histtype+' not found')
209       
210    def GetRelativePhaseNum(self,phasename):
211        '''Returns a phase number if the string matches a phase name
212        or else returns the original string
213        '''
214        item, cookie = self.GetFirstChild(self.root)
215        while item:
216            itemtext = self.GetItemText(item)
217            if itemtext == "Phases":
218                parent = item
219                item, cookie = self.GetFirstChild(parent)
220                i = 0
221                while item:
222                    itemtext = self.GetItemText(item)
223                    if itemtext == phasename:
224                        return i
225                    item, cookie = self.GetNextChild(parent, cookie)
226                    i += 1
227                else:
228                    return phasename # not a phase name
229            item, cookie = self.GetNextChild(self.root, cookie)
230        else:
231            raise Exception("No phases found ")
232
233    def ConvertRelativePhaseNum(self,phasenum):
234        '''Converts relative phase number to a phase name in
235        the current project
236        '''
237        item, cookie = self.GetFirstChild(self.root)
238        while item:
239            itemtext = self.GetItemText(item)
240            if itemtext == "Phases":
241                parent = item
242                item, cookie = self.GetFirstChild(parent)
243                i = 0
244                while item:
245                    if i == phasenum:
246                        return self.GetItemText(item)
247                    item, cookie = self.GetNextChild(parent, cookie)
248                    i += 1
249                else:
250                    raise Exception("Phase "+str(phasenum)+" not found")
251            item, cookie = self.GetNextChild(self.root, cookie)
252        else:
253            raise Exception("No phases found ")
254
255    def GetImageLoc(self,TreeId):
256        '''Get Image data from the Tree. Handles cases where the
257        image name is specified, as well as where the image file name is
258        a tuple containing the image file and an image number
259        '''
260       
261        size,imagefile = self.GetItemPyData(TreeId)
262        if type(imagefile) is tuple or type(imagefile) is list:
263            return size,imagefile[0],imagefile[1]
264        else:
265            return size,imagefile,None
266
267    def UpdateImageLoc(self,TreeId,imagefile):
268        '''Saves a new imagefile name in the Tree. Handles cases where the
269        image name is specified, as well as where the image file name is
270        a tuple containing the image file and an image number
271        '''
272       
273        idata = self.GetItemPyData(TreeId)
274        if type(idata[1]) is tuple or type(idata[1]) is list:
275            idata[1] = list(idata[1])
276            idata[1][0] = [imagefile,idata[1][1]]
277        else:
278            idata[1]  = imagefile
279       
280    def SaveExposedItems(self):
281        '''Traverse the top level tree items and save names of exposed (expanded) tree items.
282        Done before a refinement.
283        '''
284        self.ExposedItems = []
285        item, cookie = self.GetFirstChild(self.root)
286        while item:
287            name = self.GetItemText(item)
288            if self.IsExpanded(item): self.ExposedItems.append(name)
289            item, cookie = self.GetNextChild(self.root, cookie)
290#        print 'exposed:',self.ExposedItems
291
292    def RestoreExposedItems(self):
293        '''Traverse the top level tree items and restore exposed (expanded) tree items
294        back to their previous state (done after a reload of the tree after a refinement)
295        '''
296        item, cookie = self.GetFirstChild(self.root)
297        while item:
298            name = self.GetItemText(item)
299            if name in self.ExposedItems: self.Expand(item)
300            item, cookie = self.GetNextChild(self.root, cookie)
301
302################################################################################
303#### TextCtrl that stores input as entered with optional validation
304################################################################################
305class ValidatedTxtCtrl(wx.TextCtrl):
306    '''Create a TextCtrl widget that uses a validator to prevent the
307    entry of inappropriate characters and changes color to highlight
308    when invalid input is supplied. As valid values are typed,
309    they are placed into the dict or list where the initial value
310    came from. The type of the initial value must be int,
311    float or str or None (see :obj:`key` and :obj:`typeHint`);
312    this type (or the one in :obj:`typeHint`) is preserved.
313
314    Float values can be entered in the TextCtrl as numbers or also
315    as algebraic expressions using operators + - / \* () and \*\*,
316    in addition pi, sind(), cosd(), tand(), and sqrt() can be used,
317    as well as appreviations s, sin, c, cos, t, tan and sq.
318
319    :param wx.Panel parent: name of panel or frame that will be
320      the parent to the TextCtrl. Can be None.
321
322    :param dict/list loc: the dict or list with the initial value to be
323      placed in the TextCtrl.
324
325    :param int/str key: the dict key or the list index for the value to be
326      edited by the TextCtrl. The ``loc[key]`` element must exist, but may
327      have value None. If None, the type for the element is taken from
328      :obj:`typeHint` and the value for the control is set initially
329      blank (and thus invalid.) This is a way to specify a field without a
330      default value: a user must set a valid value.
331       
332      If the value is not None, it must have a base
333      type of int, float, str or unicode; the TextCrtl will be initialized
334      from this value.
335     
336    :param list nDig: number of digits, places and optionally the format
337       ([nDig,nPlc,fmt]) after decimal to use for display of float. The format
338       is either 'f' (default) or 'g'. Alternately, None can be specified which
339       causes numbers to be displayed with approximately 5 significant figures
340       (Default=None).
341
342    :param bool notBlank: if True (default) blank values are invalid
343      for str inputs.
344     
345    :param number min: minimum allowed valid value. If None (default) the
346      lower limit is unbounded.
347      NB: test in NumberValidator is val >= min not val > min
348
349    :param number max: maximum allowed valid value. If None (default) the
350      upper limit is unbounded
351      NB: test in NumberValidator is val <= max not val < max
352     
353    :param list exclLim: if True exclude min/max value ([exclMin,exclMax]);
354      (Default=[False,False])
355
356    :param function OKcontrol: specifies a function or method that will be
357      called when the input is validated. The called function is supplied
358      with one argument which is False if the TextCtrl contains an invalid
359      value and True if the value is valid.
360      Note that this function should check all values
361      in the dialog when True, since other entries might be invalid.
362      The default for this is None, which indicates no function should
363      be called.
364
365    :param function OnLeave: specifies a function or method that will be
366      called when the focus for the control is lost.
367      The called function is supplied with (at present) three keyword arguments:
368
369      * invalid: (*bool*) True if the value for the TextCtrl is invalid
370      * value:   (*int/float/str*)  the value contained in the TextCtrl
371      * tc:      (*wx.TextCtrl*)  the TextCtrl object
372
373      The number of keyword arguments may be increased in the future should needs arise,
374      so it is best to code these functions with a \*\*kwargs argument so they will
375      continue to run without errors
376
377      The default for OnLeave is None, which indicates no function should
378      be called.
379
380    :param type typeHint: the value of typeHint is overrides the initial value
381      for the dict/list element ``loc[key]``, if set to
382      int or float, which specifies the type for input to the TextCtrl.
383      Defaults as None, which is ignored.
384
385    :param bool CIFinput: for str input, indicates that only printable
386      ASCII characters may be entered into the TextCtrl. Forces output
387      to be ASCII rather than Unicode. For float and int input, allows
388      use of a single '?' or '.' character as valid input.
389
390    :param dict OnLeaveArgs: a dict with keyword args that are passed to
391      the :attr:`OnLeave` function. Defaults to ``{}``
392
393    :param bool ASCIIonly: if set as True will remove unicode characters from
394      strings
395
396    :param (other): other optional keyword parameters for the
397      wx.TextCtrl widget such as size or style may be specified.
398    '''
399    def __init__(self,parent,loc,key,nDig=None,notBlank=True,min=None,max=None,
400        OKcontrol=None,OnLeave=None,typeHint=None,CIFinput=False,exclLim=[False,False],
401        OnLeaveArgs={}, ASCIIonly=False, **kw):
402        # save passed values needed outside __init__
403        self.result = loc
404        self.key = key
405        self.nDig = nDig
406        self.OKcontrol=OKcontrol
407        self.OnLeave = OnLeave
408        self.OnLeaveArgs = OnLeaveArgs
409        self.CIFinput = CIFinput
410        self.notBlank = notBlank
411        self.ASCIIonly = ASCIIonly
412        self.type = str
413        # initialization
414        self.invalid = False   # indicates if the control has invalid contents
415        self.evaluated = False # set to True when the validator recognizes an expression
416        val = loc[key]
417        if 'style' in kw: # add a "Process Enter" to style
418            kw['style'] += kw['style'] | wx.TE_PROCESS_ENTER
419        else:
420            kw['style'] = wx.TE_PROCESS_ENTER
421        if 'int' in str(type(val)) or typeHint is int:
422            self.type = int
423            wx.TextCtrl.__init__(self,parent,wx.ID_ANY,
424                validator=NumberValidator(int,result=loc,key=key,min=min,max=max,
425                    exclLim=exclLim,OKcontrol=OKcontrol,CIFinput=CIFinput),**kw)
426            if val is not None:
427                self._setValue(val)
428            else: # no default is invalid for a number
429                self.invalid = True
430                self._IndicateValidity()
431
432        elif 'float' in str(type(val)) or typeHint is float:
433            self.type = float
434            wx.TextCtrl.__init__(self,parent,wx.ID_ANY,
435                validator=NumberValidator(float,result=loc,key=key,min=min,max=max,
436                    exclLim=exclLim,OKcontrol=OKcontrol,CIFinput=CIFinput),**kw)
437            if val is not None:
438                self._setValue(val)
439            else:
440                self.invalid = True
441                self._IndicateValidity()
442
443        elif isinstance(val,str) or isinstance(val,unicode) or typeHint is str:
444            if self.CIFinput:
445                wx.TextCtrl.__init__(
446                    self,parent,wx.ID_ANY,
447                    validator=ASCIIValidator(result=loc,key=key),
448                    **kw)
449            else:
450                wx.TextCtrl.__init__(self,parent,wx.ID_ANY,**kw)
451            if val is not None:
452                self.SetValue(val)
453            if notBlank:
454                self.Bind(wx.EVT_CHAR,self._onStringKey)
455                self.ShowStringValidity() # test if valid input
456            else:
457                self.invalid = False
458                self.Bind(wx.EVT_CHAR,self._GetStringValue)
459        elif val is None:
460            raise Exception("ValidatedTxtCtrl error: value of "+str(key)+
461                             " element is None and typeHint not defined as int or float")
462        else:
463            raise Exception("ValidatedTxtCtrl error: Unknown element ("+str(key)+
464                             ") type: "+str(type(val)))
465        # When the mouse is moved away or the widget loses focus,
466        # display the last saved value, if an expression
467        self.Bind(wx.EVT_LEAVE_WINDOW, self._onLeaveWindow)
468        self.Bind(wx.EVT_TEXT_ENTER, self._onLoseFocus)
469        self.Bind(wx.EVT_KILL_FOCUS, self._onLoseFocus)
470        # patch for wx 2.9 on Mac
471        i,j= wx.__version__.split('.')[0:2]
472        if int(i)+int(j)/10. > 2.8 and 'wxOSX' in wx.PlatformInfo:
473            self.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)
474
475    def SetValue(self,val):
476        if self.result is not None: # note that this bypasses formatting
477            self.result[self.key] = val
478            log.LogVarChange(self.result,self.key)
479        self._setValue(val)
480
481    def _setValue(self,val,show=True):
482        '''Check the validity of an int or float value and convert to a str.
483        Possibly format it. If show is True, display the formatted value in
484        the Text widget.
485        '''
486        self.invalid = False
487        if self.type is int:
488            try:
489                if int(val) != val:
490                    self.invalid = True
491                else:
492                    val = int(val)
493            except:
494                if self.CIFinput and (val == '?' or val == '.'):
495                    pass
496                else:
497                    self.invalid = True
498            if show: wx.TextCtrl.SetValue(self,str(val))
499        elif self.type is float:
500            try:
501                val = float(val) # convert strings, if needed
502            except:
503                if self.CIFinput and (val == '?' or val == '.'):
504                    pass
505                else:
506                    self.invalid = True
507            if self.nDig and show:
508                wx.TextCtrl.SetValue(self,str(G2py3.FormatValue(val,self.nDig)))
509            elif show:
510                wx.TextCtrl.SetValue(self,str(G2py3.FormatSigFigs(val)).rstrip('0'))
511        else:
512            if self.ASCIIonly:
513                s = ''
514                for c in val:
515                    if ord(c) < 128: s += c
516                if val != s:
517                    val = s
518                    show = True
519            if show:
520                try:
521                    wx.TextCtrl.SetValue(self,str(val))
522                except:
523                    wx.TextCtrl.SetValue(self,val)
524            self.ShowStringValidity() # test if valid input
525            return
526       
527        self._IndicateValidity()
528        if self.OKcontrol:
529            self.OKcontrol(not self.invalid)
530
531    def OnKeyDown(self,event):
532        'Special callback for wx 2.9+ on Mac where backspace is not processed by validator'
533        key = event.GetKeyCode()
534        if key in [wx.WXK_BACK, wx.WXK_DELETE]:
535            if self.Validator: wx.CallAfter(self.Validator.TestValid,self)
536        if key == wx.WXK_RETURN or key == wx.WXK_NUMPAD_ENTER:
537            self._onLoseFocus(None)
538        if event: event.Skip()
539                   
540    def _onStringKey(self,event):
541        if event: event.Skip()
542        if self.invalid: # check for validity after processing the keystroke
543            wx.CallAfter(self.ShowStringValidity,True) # was invalid
544        else:
545            wx.CallAfter(self.ShowStringValidity,False) # was valid
546
547    def _IndicateValidity(self):
548        'Set the control colors to show invalid input'
549        if self.invalid:
550            ins = self.GetInsertionPoint()
551            self.SetForegroundColour("red")
552            self.SetBackgroundColour("yellow")
553            self.SetFocus()
554            self.Refresh() # this selects text on some Linuxes
555            self.SetSelection(0,0)   # unselect
556            self.SetInsertionPoint(ins) # put insertion point back
557        else: # valid input
558            self.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW))
559            self.SetForegroundColour("black")
560            self.Refresh()
561
562    def ShowStringValidity(self,previousInvalid=True):
563        '''Check if input is valid. Anytime the input is
564        invalid, call self.OKcontrol (if defined) because it is fast.
565        If valid, check for any other invalid entries only when
566        changing from invalid to valid, since that is slower.
567       
568        :param bool previousInvalid: True if the TextCtrl contents were
569          invalid prior to the current change.
570         
571        '''
572        val = self.GetValue().strip()
573        if self.notBlank:
574            self.invalid = not val
575        else:
576            self.invalid = False
577        self._IndicateValidity()
578        if self.invalid:
579            if self.OKcontrol:
580                self.OKcontrol(False)
581        elif self.OKcontrol and previousInvalid:
582            self.OKcontrol(True)
583        # always store the result
584        if self.CIFinput: # for CIF make results ASCII
585            self.result[self.key] = val.encode('ascii','replace') 
586        else:
587            self.result[self.key] = val
588        log.LogVarChange(self.result,self.key)
589
590    def _GetStringValue(self,event):
591        '''Get string input and store.
592        '''
593        if event: event.Skip() # process keystroke
594        wx.CallAfter(self._SaveStringValue)
595       
596    def _SaveStringValue(self):
597        val = self.GetValue().strip()
598        # always store the result
599        if self.CIFinput: # for CIF make results ASCII
600            self.result[self.key] = val.encode('ascii','replace') 
601        else:
602            self.result[self.key] = val
603        log.LogVarChange(self.result,self.key)
604
605    def _onLeaveWindow(self,event):
606        '''If the mouse leaves the text box, save the result, if valid,
607        but (unlike _onLoseFocus) don't update the textbox contents.
608        '''
609        if not self.IsModified():   #ignore mouse crusing
610            return
611        if self.evaluated and not self.invalid: # deal with computed expressions
612            self.evaluated = False # expression has been recast as value, reset flag
613        if self.invalid: # don't update an invalid expression
614            if event: event.Skip()
615            return
616        self._setValue(self.result[self.key],show=False) # save value quietly
617        if self.OnLeave:
618            self.event = event
619            self.OnLeave(invalid=self.invalid,value=self.result[self.key],
620                tc=self,**self.OnLeaveArgs)
621        if event: event.Skip()
622           
623    def _onLoseFocus(self,event):
624        '''Enter has been pressed or focus transferred to another control,
625        Evaluate and update the current control contents
626        '''
627        if event: event.Skip()
628        if not self.IsModified():   #ignore mouse crusing
629            return
630        if self.evaluated: # deal with computed expressions
631            if self.invalid: # don't substitute for an invalid expression
632                return 
633            self.evaluated = False # expression has been recast as value, reset flag
634            self._setValue(self.result[self.key])
635        elif self.result is not None: # show formatted result, as Bob wants
636            if not self.invalid: # don't update an invalid expression
637                self._setValue(self.result[self.key])
638        if self.OnLeave:
639            self.event = event
640            self.OnLeave(invalid=self.invalid,value=self.result[self.key],
641                tc=self,**self.OnLeaveArgs)
642
643################################################################################
644class NumberValidator(wx.PyValidator):
645    '''A validator to be used with a TextCtrl to prevent
646    entering characters other than digits, signs, and for float
647    input, a period and exponents.
648   
649    The value is checked for validity after every keystroke
650      If an invalid number is entered, the box is highlighted.
651      If the number is valid, it is saved in result[key]
652
653    :param type typ: the base data type. Must be int or float.
654
655    :param bool positiveonly: If True, negative integers are not allowed
656      (default False). This prevents the + or - keys from being pressed.
657      Used with typ=int; ignored for typ=float.
658
659    :param number min: Minimum allowed value. If None (default) the
660      lower limit is unbounded
661
662    :param number max: Maximum allowed value. If None (default) the
663      upper limit is unbounded
664     
665    :param list exclLim: if True exclude min/max value ([exclMin,exclMax]);
666     (Default=[False,False])
667
668    :param dict/list result: List or dict where value should be placed when valid
669
670    :param any key: key to use for result (int for list)
671
672    :param function OKcontrol: function or class method to control
673      an OK button for a window.
674      Ignored if None (default)
675
676    :param bool CIFinput: allows use of a single '?' or '.' character
677      as valid input.
678     
679    '''
680    def __init__(self, typ, positiveonly=False, min=None, max=None,exclLim=[False,False],
681        result=None, key=None, OKcontrol=None, CIFinput=False):
682        'Create the validator'
683        if 'phoenix' in wx.version():
684            wx.Validator.__init__(self)
685        else:
686            wx.PyValidator.__init__(self)
687        # save passed parameters
688        self.typ = typ
689        self.positiveonly = positiveonly
690        self.min = min
691        self.max = max
692        self.exclLim = exclLim
693        self.result = result
694        self.key = key
695        self.OKcontrol = OKcontrol
696        self.CIFinput = CIFinput
697        # set allowed keys by data type
698        self.Bind(wx.EVT_CHAR, self.OnChar)
699        if self.typ == int and self.positiveonly:
700            self.validchars = '0123456789'
701        elif self.typ == int:
702            self.validchars = '0123456789+-'
703        elif self.typ == float:
704            # allow for above and sind, cosd, sqrt, tand, pi, and abbreviations
705            # also addition, subtraction, division, multiplication, exponentiation
706            self.validchars = '0123456789.-+eE/cosindcqrtap()*'
707        else:
708            self.validchars = None
709            return
710        if self.CIFinput:
711            self.validchars += '?.'
712    def Clone(self):
713        'Create a copy of the validator, a strange, but required component'
714        return NumberValidator(typ=self.typ, 
715                               positiveonly=self.positiveonly,
716                               min=self.min, max=self.max,
717                               result=self.result, key=self.key,
718                               OKcontrol=self.OKcontrol,
719                               CIFinput=self.CIFinput)
720    def TransferToWindow(self):
721        'Needed by validator, strange, but required component'
722        return True # Prevent wxDialog from complaining.
723    def TransferFromWindow(self):
724        'Needed by validator, strange, but required component'
725        return True # Prevent wxDialog from complaining.
726    def TestValid(self,tc):
727        '''Check if the value is valid by casting the input string
728        into the current type.
729
730        Set the invalid variable in the TextCtrl object accordingly.
731
732        If the value is valid, save it in the dict/list where
733        the initial value was stored, if appropriate.
734
735        :param wx.TextCtrl tc: A reference to the TextCtrl that the validator
736          is associated with.
737        '''
738        tc.invalid = False # assume valid
739        if self.CIFinput:
740            val = tc.GetValue().strip()
741            if val == '?' or val == '.':
742                self.result[self.key] = val
743                log.LogVarChange(self.result,self.key)
744                return
745        try:
746            val = self.typ(tc.GetValue())
747        except (ValueError, SyntaxError):
748            if self.typ is float: # for float values, see if an expression can be evaluated
749                val = G2py3.FormulaEval(tc.GetValue())
750                if val is None:
751                    tc.invalid = True
752                    return
753                else:
754                    tc.evaluated = True
755            else: 
756                tc.invalid = True
757                return
758        if self.max != None:
759            if val >= self.max and self.exclLim[1]:
760                tc.invalid = True
761            elif val > self.max:
762                tc.invalid = True
763        if self.min != None:
764            if val <= self.min and self.exclLim[0]:
765                tc.invalid = True
766            elif val < self.min:
767                tc.invalid = True  # invalid
768        if self.key is not None and self.result is not None and not tc.invalid:
769            self.result[self.key] = val
770            log.LogVarChange(self.result,self.key)
771
772    def ShowValidity(self,tc):
773        '''Set the control colors to show invalid input
774
775        :param wx.TextCtrl tc: A reference to the TextCtrl that the validator
776          is associated with.
777
778        '''
779        if tc.invalid:
780            ins = tc.GetInsertionPoint()
781            tc.SetForegroundColour("red")
782            tc.SetBackgroundColour("yellow")
783            tc.SetFocus()
784            tc.Refresh() # this selects text on some Linuxes
785            tc.SetSelection(0,0)   # unselect
786            tc.SetInsertionPoint(ins) # put insertion point back
787            return False
788        else: # valid input
789            tc.SetBackgroundColour(wx.SystemSettings.GetColour(wx.SYS_COLOUR_WINDOW))
790            tc.SetForegroundColour("black")
791            tc.Refresh()
792            return True
793
794    def CheckInput(self,previousInvalid):
795        '''called to test every change to the TextCtrl for validity and
796        to change the appearance of the TextCtrl
797
798        Anytime the input is invalid, call self.OKcontrol
799        (if defined) because it is fast.
800        If valid, check for any other invalid entries only when
801        changing from invalid to valid, since that is slower.
802
803        :param bool previousInvalid: True if the TextCtrl contents were
804          invalid prior to the current change.
805        '''
806        tc = self.GetWindow()
807        self.TestValid(tc)
808        self.ShowValidity(tc)
809        # if invalid
810        if tc.invalid and self.OKcontrol:
811            self.OKcontrol(False)
812        if not tc.invalid and self.OKcontrol and previousInvalid:
813            self.OKcontrol(True)
814
815    def OnChar(self, event):
816        '''Called each type a key is pressed
817        ignores keys that are not allowed for int and float types
818        '''
819        key = event.GetKeyCode()
820        tc = self.GetWindow()
821        if key == wx.WXK_RETURN or key == wx.WXK_NUMPAD_ENTER:
822            if tc.invalid:
823                self.CheckInput(True) 
824            else:
825                self.CheckInput(False) 
826            if event: event.Skip()
827            return
828        if key < wx.WXK_SPACE or key == wx.WXK_DELETE or key > 255: # control characters get processed
829            if event: event.Skip()
830            if tc.invalid:
831                wx.CallAfter(self.CheckInput,True) 
832            else:
833                wx.CallAfter(self.CheckInput,False) 
834            return
835        elif chr(key) in self.validchars: # valid char pressed?
836            if event: event.Skip()
837            if tc.invalid:
838                wx.CallAfter(self.CheckInput,True) 
839            else:
840                wx.CallAfter(self.CheckInput,False) 
841            return
842        if not wx.Validator_IsSilent(): wx.Bell()
843        return  # Returning without calling event.Skip, which eats the keystroke
844
845################################################################################
846class ASCIIValidator(wx.PyValidator):
847    '''A validator to be used with a TextCtrl to prevent
848    entering characters other than ASCII characters.
849   
850    The value is checked for validity after every keystroke
851      If an invalid number is entered, the box is highlighted.
852      If the number is valid, it is saved in result[key]
853
854    :param dict/list result: List or dict where value should be placed when valid
855
856    :param any key: key to use for result (int for list)
857
858    '''
859    def __init__(self, result=None, key=None):
860        'Create the validator'
861        import string
862        wx.PyValidator.__init__(self)
863        # save passed parameters
864        self.result = result
865        self.key = key
866        self.validchars = string.ascii_letters + string.digits + string.punctuation + string.whitespace
867        self.Bind(wx.EVT_CHAR, self.OnChar)
868    def Clone(self):
869        'Create a copy of the validator, a strange, but required component'
870        return ASCIIValidator(result=self.result, key=self.key)
871        tc = self.GetWindow()
872        tc.invalid = False # make sure the validity flag is defined in parent
873    def TransferToWindow(self):
874        'Needed by validator, strange, but required component'
875        return True # Prevent wxDialog from complaining.
876    def TransferFromWindow(self):
877        'Needed by validator, strange, but required component'
878        return True # Prevent wxDialog from complaining.
879    def TestValid(self,tc):
880        '''Check if the value is valid by casting the input string
881        into ASCII.
882
883        Save it in the dict/list where the initial value was stored
884
885        :param wx.TextCtrl tc: A reference to the TextCtrl that the validator
886          is associated with.
887        '''
888        self.result[self.key] = tc.GetValue().encode('ascii','replace')
889        log.LogVarChange(self.result,self.key)
890
891    def OnChar(self, event):
892        '''Called each type a key is pressed
893        ignores keys that are not allowed for int and float types
894        '''
895        key = event.GetKeyCode()
896        tc = self.GetWindow()
897        if key == wx.WXK_RETURN or key == wx.WXK_NUMPAD_ENTER:
898            self.TestValid(tc)
899            if event: event.Skip()
900            return
901        if key < wx.WXK_SPACE or key == wx.WXK_DELETE or key > 255: # control characters get processed
902            if event: event.Skip()
903            self.TestValid(tc)
904            return
905        elif chr(key) in self.validchars: # valid char pressed?
906            if event: event.Skip()
907            self.TestValid(tc)
908            return
909        if not wx.Validator_IsSilent():
910            wx.Bell()
911        return  # Returning without calling event.Skip, which eats the keystroke
912
913################################################################################
914def HorizontalLine(sizer,parent):
915    '''Draws a horizontal line as wide as the window.
916    This shows up on the Mac as a very thin line, no matter what I do
917    '''
918    line = wx.StaticLine(parent, size=(-1,3), style=wx.LI_HORIZONTAL)
919    sizer.Add(line, 0, wx.EXPAND|wx.ALIGN_CENTER|wx.ALL, 5)
920
921################################################################################
922class G2LoggedButton(wx.Button):
923    '''A version of wx.Button that creates logging events. Bindings are saved
924    in the object, and are looked up rather than directly set with a bind.
925    An index to these buttons is saved as log.ButtonBindingLookup
926    :param wx.Panel parent: parent widget
927    :param int id: Id for button
928    :param str label: label for button
929    :param str locationcode: a label used internally to uniquely indentify the button
930    :param function handler: a routine to call when the button is pressed
931    '''
932    def __init__(self,parent,id=wx.ID_ANY,label='',locationcode='',
933                 handler=None,*args,**kwargs):
934        super(self.__class__,self).__init__(parent,id,label,*args,**kwargs)
935        self.label = label
936        self.handler = handler
937        self.locationcode = locationcode
938        key = locationcode + '+' + label # hash code to find button
939        self.Bind(wx.EVT_BUTTON,self.onPress)
940        log.ButtonBindingLookup[key] = self
941    def onPress(self,event):
942        'create log event and call handler'
943        log.MakeButtonLog(self.locationcode,self.label)
944        self.handler(event)
945       
946################################################################################
947class EnumSelector(wx.ComboBox):
948    '''A customized :class:`wxpython.ComboBox` that selects items from a list
949    of choices, but sets a dict (list) entry to the corresponding
950    entry from the input list of values.
951
952    :param wx.Panel parent: the parent to the :class:`~wxpython.ComboBox` (usually a
953      frame or panel)
954    :param dict dct: a dict (or list) to contain the value set
955      for the :class:`~wxpython.ComboBox`.
956    :param item: the dict key (or list index) where ``dct[item]`` will
957      be set to the value selected in the :class:`~wxpython.ComboBox`. Also, dct[item]
958      contains the starting value shown in the widget. If the value
959      does not match an entry in :data:`values`, the first value
960      in :data:`choices` is used as the default, but ``dct[item]`` is
961      not changed.   
962    :param list choices: a list of choices to be displayed to the
963      user such as
964      ::
965     
966      ["default","option 1","option 2",]
967
968      Note that these options will correspond to the entries in
969      :data:`values` (if specified) item by item.
970    :param list values: a list of values that correspond to
971      the options in :data:`choices`, such as
972      ::
973     
974      [0,1,2]
975     
976      The default for :data:`values` is to use the same list as
977      specified for :data:`choices`.
978    :param (other): additional keyword arguments accepted by
979      :class:`~wxpython.ComboBox` can be specified.
980    '''
981    def __init__(self,parent,dct,item,choices,values=None,**kw):
982        if values is None:
983            values = choices
984        if dct[item] in values:
985            i = values.index(dct[item])
986        else:
987            i = 0
988        startval = choices[i]
989        wx.ComboBox.__init__(self,parent,wx.ID_ANY,startval,
990                             choices = choices,
991                             style=wx.CB_DROPDOWN|wx.CB_READONLY,
992                             **kw)
993        self.choices = choices
994        self.values = values
995        self.dct = dct
996        self.item = item
997        self.Bind(wx.EVT_COMBOBOX, self.onSelection)
998    def onSelection(self,event):
999        # respond to a selection by setting the enum value in the CIF dictionary
1000        if self.GetValue() in self.choices: # should always be true!
1001            self.dct[self.item] = self.values[self.choices.index(self.GetValue())]
1002        else:
1003            self.dct[self.item] = self.values[0] # unknown
1004
1005################################################################################
1006class G2ChoiceButton(wx.Choice):
1007    '''A customized version of a wx.Choice that automatically initializes
1008    the control to match a supplied value and saves the choice directly
1009    into an array or list. Optionally a function can be called each time a
1010    choice is selected. The widget can be used with an array item that is set to
1011    to the choice by number (``indLoc[indKey]``) or by string value
1012    (``strLoc[strKey]``) or both. The initial value is taken from ``indLoc[indKey]``
1013    if not None or ``strLoc[strKey]`` if not None.
1014
1015    :param wx.Panel parent: name of panel or frame that will be
1016      the parent to the widget. Can be None.
1017    :param list choiceList: a list or tuple of choices to offer the user.
1018    :param dict/list indLoc: a dict or list with the initial value to be
1019      placed in the Choice button. If this is None, this is ignored.
1020    :param int/str indKey: the dict key or the list index for the value to be
1021      edited by the Choice button. If indLoc is not None then this
1022      must be specified and the ``indLoc[indKey]`` will be set. If the value
1023      for ``indLoc[indKey]`` is not None, it should be an integer in
1024      range(len(choiceList)). The Choice button will be initialized to the
1025      choice corresponding to the value in this element if not None.
1026    :param dict/list strLoc: a dict or list with the string value corresponding to
1027      indLoc/indKey. Default (None) means that this is not used.
1028    :param int/str strKey: the dict key or the list index for the string value
1029      The ``strLoc[strKey]`` element must exist or strLoc must be None (default).
1030    :param function onChoice: name of a function to call when the choice is made.
1031    '''
1032    def __init__(self,parent,choiceList,indLoc=None,indKey=None,strLoc=None,strKey=None,
1033                 onChoice=None,**kwargs):
1034        wx.Choice.__init__(self,parent,choices=choiceList,id=wx.ID_ANY,**kwargs)
1035        self.choiceList = choiceList
1036        self.indLoc = indLoc
1037        self.indKey = indKey
1038        self.strLoc = strLoc
1039        self.strKey = strKey
1040        self.onChoice = None
1041        self.SetSelection(wx.NOT_FOUND)
1042        if self.indLoc is not None and self.indLoc.get(self.indKey) is not None:
1043            self.SetSelection(self.indLoc[self.indKey])
1044            if self.strLoc is not None:
1045                self.strLoc[self.strKey] = self.GetStringSelection()
1046                log.LogVarChange(self.strLoc,self.strKey)
1047        elif self.strLoc is not None and self.strLoc.get(self.strKey) is not None:
1048            try:
1049                self.SetSelection(choiceList.index(self.strLoc[self.strKey]))
1050                if self.indLoc is not None:
1051                    self.indLoc[self.indKey] = self.GetSelection()
1052                    log.LogVarChange(self.indLoc,self.indKey)
1053            except ValueError:
1054                pass
1055        self.Bind(wx.EVT_CHOICE, self._OnChoice)
1056        #if self.strLoc is not None: # make sure strLoc gets initialized
1057        #    self._OnChoice(None) # note that onChoice will not be called
1058        self.onChoice = onChoice
1059    def _OnChoice(self,event):
1060        if self.indLoc is not None:
1061            self.indLoc[self.indKey] = self.GetSelection()
1062            log.LogVarChange(self.indLoc,self.indKey)
1063        if self.strLoc is not None:
1064            self.strLoc[self.strKey] = self.GetStringSelection()
1065            log.LogVarChange(self.strLoc,self.strKey)
1066        if self.onChoice:
1067            self.onChoice()
1068
1069############################################################### Custom checkbox that saves values into dict/list as used
1070class G2CheckBox(wx.CheckBox):
1071    '''A customized version of a CheckBox that automatically initializes
1072    the control to a supplied list or dict entry and updates that
1073    entry as the widget is used.
1074
1075    :param wx.Panel parent: name of panel or frame that will be
1076      the parent to the widget. Can be None.
1077    :param str label: text to put on check button
1078    :param dict/list loc: the dict or list with the initial value to be
1079      placed in the CheckBox.
1080    :param int/str key: the dict key or the list index for the value to be
1081      edited by the CheckBox. The ``loc[key]`` element must exist.
1082      The CheckBox will be initialized from this value.
1083      If the value is anything other that True (or 1), it will be taken as
1084      False.
1085    :param function OnChange: specifies a function or method that will be
1086      called when the CheckBox is changed (Default is None).
1087      The called function is supplied with one argument, the calling event.
1088    '''
1089    def __init__(self,parent,label,loc,key,OnChange=None):
1090        wx.CheckBox.__init__(self,parent,id=wx.ID_ANY,label=label)
1091        self.loc = loc
1092        self.key = key
1093        self.OnChange = OnChange
1094        self.SetValue(self.loc[self.key]==True)
1095        self.Bind(wx.EVT_CHECKBOX, self._OnCheckBox)
1096    def _OnCheckBox(self,event):
1097        self.loc[self.key] = self.GetValue()
1098        log.LogVarChange(self.loc,self.key)
1099        if self.OnChange: self.OnChange(event)
1100                   
1101################################################################################
1102#### Commonly used dialogs
1103################################################################################
1104def CallScrolledMultiEditor(parent,dictlst,elemlst,prelbl=[],postlbl=[],
1105                 title='Edit items',header='',size=(300,250),
1106                             CopyButton=False, ASCIIonly=False, **kw):
1107    '''Shell routine to call a ScrolledMultiEditor dialog. See
1108    :class:`ScrolledMultiEditor` for parameter definitions.
1109
1110    :returns: True if the OK button is pressed; False if the window is closed
1111      with the system menu or the Cancel button.
1112
1113    '''
1114    dlg = ScrolledMultiEditor(parent,dictlst,elemlst,prelbl,postlbl,
1115                              title,header,size,
1116                              CopyButton, ASCIIonly, **kw)
1117    if dlg.ShowModal() == wx.ID_OK:
1118        dlg.Destroy()
1119        return True
1120    else:
1121        dlg.Destroy()
1122        return False
1123
1124################################################################################
1125class ScrolledMultiEditor(wx.Dialog):
1126    '''Define a window for editing a potentially large number of dict- or
1127    list-contained values with validation for each item. Edited values are
1128    automatically placed in their source location. If invalid entries
1129    are provided, the TextCtrl is turned yellow and the OK button is disabled.
1130
1131    The type for each TextCtrl validation is determined by the
1132    initial value of the entry (int, float or string).
1133    Float values can be entered in the TextCtrl as numbers or also
1134    as algebraic expressions using operators + - / \* () and \*\*,
1135    in addition pi, sind(), cosd(), tand(), and sqrt() can be used,
1136    as well as appreviations s(), sin(), c(), cos(), t(), tan() and sq().
1137
1138    :param wx.Frame parent: name of parent window, or may be None
1139
1140    :param tuple dictlst: a list of dicts or lists containing values to edit
1141
1142    :param tuple elemlst: a list of keys for each item in a dictlst. Must have the
1143      same length as dictlst.
1144
1145    :param wx.Frame parent: name of parent window, or may be None
1146   
1147    :param tuple prelbl: a list of labels placed before the TextCtrl for each
1148      item (optional)
1149   
1150    :param tuple postlbl: a list of labels placed after the TextCtrl for each
1151      item (optional)
1152
1153    :param str title: a title to place in the frame of the dialog
1154
1155    :param str header: text to place at the top of the window. May contain
1156      new line characters.
1157
1158    :param wx.Size size: a size parameter that dictates the
1159      size for the scrolled region of the dialog. The default is
1160      (300,250).
1161
1162    :param bool CopyButton: if True adds a small button that copies the
1163      value for the current row to all fields below (default is False)
1164     
1165    :param bool ASCIIonly: if set as True will remove unicode characters from
1166      strings
1167     
1168    :param list minvals: optional list of minimum values for validation
1169      of float or int values. Ignored if value is None.
1170    :param list maxvals: optional list of maximum values for validation
1171      of float or int values. Ignored if value is None.
1172    :param list sizevals: optional list of wx.Size values for each input
1173      widget. Ignored if value is None.
1174     
1175    :param tuple checkdictlst: an optional list of dicts or lists containing bool
1176      values (similar to dictlst).
1177    :param tuple checkelemlst: an optional list of dicts or lists containing bool
1178      key values (similar to elemlst). Must be used with checkdictlst.
1179    :param string checklabel: a string to use for each checkbutton
1180     
1181    :returns: the wx.Dialog created here. Use method .ShowModal() to display it.
1182   
1183    *Example for use of ScrolledMultiEditor:*
1184
1185    ::
1186
1187        dlg = <pkg>.ScrolledMultiEditor(frame,dictlst,elemlst,prelbl,postlbl,
1188                                        header=header)
1189        if dlg.ShowModal() == wx.ID_OK:
1190             for d,k in zip(dictlst,elemlst):
1191                 print d[k]
1192
1193    *Example definitions for dictlst and elemlst:*
1194
1195    ::
1196     
1197          dictlst = (dict1,list1,dict1,list1)
1198          elemlst = ('a', 1, 2, 3)
1199
1200      This causes items dict1['a'], list1[1], dict1[2] and list1[3] to be edited.
1201   
1202    Note that these items must have int, float or str values assigned to
1203    them. The dialog will force these types to be retained. String values
1204    that are blank are marked as invalid.
1205    '''
1206   
1207    def __init__(self,parent,dictlst,elemlst,prelbl=[],postlbl=[],
1208                 title='Edit items',header='',size=(300,250),
1209                 CopyButton=False, ASCIIonly=False,
1210                 minvals=[],maxvals=[],sizevals=[],
1211                 checkdictlst=[], checkelemlst=[], checklabel=""):
1212        if len(dictlst) != len(elemlst):
1213            raise Exception("ScrolledMultiEditor error: len(dictlst) != len(elemlst) "+str(len(dictlst))+" != "+str(len(elemlst)))
1214        if len(checkdictlst) != len(checkelemlst):
1215            raise Exception("ScrolledMultiEditor error: len(checkdictlst) != len(checkelemlst) "+str(len(checkdictlst))+" != "+str(len(checkelemlst)))
1216        wx.Dialog.__init__( # create dialog & sizer
1217            self,parent,wx.ID_ANY,title,
1218            style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)
1219        mainSizer = wx.BoxSizer(wx.VERTICAL)
1220        self.orig = []
1221        self.dictlst = dictlst
1222        self.elemlst = elemlst
1223        self.checkdictlst = checkdictlst
1224        self.checkelemlst = checkelemlst
1225        self.StartCheckValues = [checkdictlst[i][checkelemlst[i]] for i in range(len(checkdictlst))]
1226        self.ButtonIndex = {}
1227        for d,i in zip(dictlst,elemlst):
1228            self.orig.append(d[i])
1229        # add a header if supplied
1230        if header:
1231            subSizer = wx.BoxSizer(wx.HORIZONTAL)
1232            subSizer.Add((-1,-1),1,wx.EXPAND)
1233            subSizer.Add(wx.StaticText(self,wx.ID_ANY,header))
1234            subSizer.Add((-1,-1),1,wx.EXPAND)
1235            mainSizer.Add(subSizer,0,wx.EXPAND,0)
1236        # make OK button now, because we will need it for validation
1237        self.OKbtn = wx.Button(self, wx.ID_OK)
1238        self.OKbtn.SetDefault()
1239        # create scrolled panel and sizer
1240        panel = wxscroll.ScrolledPanel(self, wx.ID_ANY,size=size,
1241            style = wx.TAB_TRAVERSAL|wx.SUNKEN_BORDER)
1242        cols = 4
1243        if CopyButton: cols += 1
1244        subSizer = wx.FlexGridSizer(cols=cols,hgap=2,vgap=2)
1245        self.ValidatedControlsList = [] # make list of TextCtrls
1246        self.CheckControlsList = [] # make list of CheckBoxes
1247        for i,(d,k) in enumerate(zip(dictlst,elemlst)):
1248            if i >= len(prelbl): # label before TextCtrl, or put in a blank
1249                subSizer.Add((-1,-1)) 
1250            else:
1251                subSizer.Add(wx.StaticText(panel,wx.ID_ANY,str(prelbl[i])))
1252            kargs = {}
1253            if i < len(minvals):
1254                if minvals[i] is not None: kargs['min']=minvals[i]
1255            if i < len(maxvals):
1256                if maxvals[i] is not None: kargs['max']=maxvals[i]
1257            if i < len(sizevals):
1258                if sizevals[i]: kargs['size']=sizevals[i]
1259            if CopyButton:
1260                import wx.lib.colourselect as wscs  # is there a way to test?
1261                but = wscs.ColourSelect(label='v', # would like to use u'\u2193' or u'\u25BC' but not in WinXP
1262                    parent=panel,colour=(255,255,200),size=wx.Size(30,23),
1263                    style=wx.RAISED_BORDER)
1264                but.Bind(wx.EVT_BUTTON, self._OnCopyButton)
1265                but.SetToolTipString('Press to copy adjacent value to all rows below')
1266                self.ButtonIndex[but] = i
1267                subSizer.Add(but)
1268            # create the validated TextCrtl, store it and add it to the sizer
1269            ctrl = ValidatedTxtCtrl(panel,d,k,OKcontrol=self.ControlOKButton,ASCIIonly=ASCIIonly,
1270                                    **kargs)
1271            self.ValidatedControlsList.append(ctrl)
1272            subSizer.Add(ctrl)
1273            if i < len(postlbl): # label after TextCtrl, or put in a blank
1274                subSizer.Add(wx.StaticText(panel,wx.ID_ANY,str(postlbl[i])))
1275            else:
1276                subSizer.Add((-1,-1))
1277            if i < len(checkdictlst):
1278                ch = G2CheckBox(panel,checklabel,checkdictlst[i],checkelemlst[i])
1279                self.CheckControlsList.append(ch)
1280                subSizer.Add(ch)                   
1281            else:
1282                subSizer.Add((-1,-1))
1283        # finish up ScrolledPanel
1284        panel.SetSizer(subSizer)
1285        panel.SetAutoLayout(1)
1286        panel.SetupScrolling()
1287        # patch for wx 2.9 on Mac
1288        i,j= wx.__version__.split('.')[0:2]
1289        if int(i)+int(j)/10. > 2.8 and 'wxOSX' in wx.PlatformInfo:
1290            panel.SetMinSize((subSizer.GetSize()[0]+30,panel.GetSize()[1]))       
1291        mainSizer.Add(panel,1, wx.ALL|wx.EXPAND,1)
1292
1293        # Sizer for OK/Close buttons. N.B. on Close changes are discarded
1294        # by restoring the initial values
1295        btnsizer = wx.BoxSizer(wx.HORIZONTAL)
1296        btnsizer.Add(self.OKbtn)
1297        btn = wx.Button(self, wx.ID_CLOSE,"Cancel") 
1298        btn.Bind(wx.EVT_BUTTON,self._onClose)
1299        btnsizer.Add(btn)
1300        mainSizer.Add(btnsizer, 0, wx.ALIGN_CENTER|wx.ALL, 5)
1301        # size out the window. Set it to be enlarged but not made smaller
1302        self.SetSizer(mainSizer)
1303        mainSizer.Fit(self)
1304        self.SetMinSize(self.GetSize())
1305
1306    def _OnCopyButton(self,event):
1307        'Implements the copy down functionality'
1308        but = event.GetEventObject()
1309        n = self.ButtonIndex.get(but)
1310        if n is None: return
1311        for i,(d,k,ctrl) in enumerate(zip(self.dictlst,self.elemlst,self.ValidatedControlsList)):
1312            if i < n: continue
1313            if i == n:
1314                val = d[k]
1315                continue
1316            d[k] = val
1317            ctrl.SetValue(val)
1318        for i in range(len(self.checkdictlst)):
1319            if i < n: continue
1320            self.checkdictlst[i][self.checkelemlst[i]] = self.checkdictlst[n][self.checkelemlst[n]]
1321            self.CheckControlsList[i].SetValue(self.checkdictlst[i][self.checkelemlst[i]])
1322    def _onClose(self,event):
1323        'Used on Cancel: Restore original values & close the window'
1324        for d,i,v in zip(self.dictlst,self.elemlst,self.orig):
1325            d[i] = v
1326        for i in range(len(self.checkdictlst)):
1327            self.checkdictlst[i][self.checkelemlst[i]] = self.StartCheckValues[i]
1328        self.EndModal(wx.ID_CANCEL)
1329       
1330    def ControlOKButton(self,setvalue):
1331        '''Enable or Disable the OK button for the dialog. Note that this is
1332        passed into the ValidatedTxtCtrl for use by validators.
1333
1334        :param bool setvalue: if True, all entries in the dialog are
1335          checked for validity. if False then the OK button is disabled.
1336
1337        '''
1338        if setvalue: # turn button on, do only if all controls show as valid
1339            for ctrl in self.ValidatedControlsList:
1340                if ctrl.invalid:
1341                    self.OKbtn.Disable()
1342                    return
1343            else:
1344                self.OKbtn.Enable()
1345        else:
1346            self.OKbtn.Disable()
1347
1348###############################################  Multichoice Dialog with set all, toggle & filter options
1349class G2MultiChoiceDialog(wx.Dialog):
1350    '''A dialog similar to MultiChoiceDialog except that buttons are
1351    added to set all choices and to toggle all choices.
1352
1353    :param wx.Frame ParentFrame: reference to parent frame
1354    :param str title: heading above list of choices
1355    :param str header: Title to place on window frame
1356    :param list ChoiceList: a list of choices where one more will be selected
1357    :param bool toggle: If True (default) the toggle and select all buttons
1358      are displayed
1359    :param bool monoFont: If False (default), use a variable-spaced font;
1360      if True use a equally-spaced font.
1361    :param bool filterBox: If True (default) an input widget is placed on
1362      the window and only entries matching the entered text are shown.
1363    :param dict extraOpts: a dict containing a entries of form label_i and value_i with extra
1364      options to present to the user, where value_i is the default value.
1365      Options are listed ordered by the value_i values.
1366    :param kw: optional keyword parameters for the wx.Dialog may
1367      be included such as size [which defaults to `(320,310)`] and
1368      style (which defaults to `wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER|wx.CENTRE| wx.OK | wx.CANCEL`);
1369      note that `wx.OK` and `wx.CANCEL` style items control
1370      the presence of the eponymous buttons in the dialog.
1371    :returns: the name of the created dialog 
1372    '''
1373    def __init__(self,parent, title, header, ChoiceList, toggle=True,
1374                 monoFont=False, filterBox=True, extraOpts={}, **kw):
1375        # process keyword parameters, notably style
1376        options = {'size':(320,310), # default Frame keywords
1377                   'style':wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER|wx.CENTRE| wx.OK | wx.CANCEL,
1378                   }
1379        options.update(kw)
1380        self.ChoiceList = ['%4d) %s'%(i,item) for i,item in enumerate(ChoiceList)] # numbered list of choices (list of str values)
1381        self.Selections = len(self.ChoiceList) * [False,] # selection status for each choice (list of bools)
1382        self.filterlist = range(len(self.ChoiceList)) # list of the choice numbers that have been filtered (list of int indices)
1383        self.Stride = 1
1384        if options['style'] & wx.OK:
1385            useOK = True
1386            options['style'] ^= wx.OK
1387        else:
1388            useOK = False
1389        if options['style'] & wx.CANCEL:
1390            useCANCEL = True
1391            options['style'] ^= wx.CANCEL
1392        else:
1393            useCANCEL = False       
1394        # create the dialog frame
1395        wx.Dialog.__init__(self,parent,wx.ID_ANY,header,**options)
1396        # fill the dialog
1397        Sizer = wx.BoxSizer(wx.VERTICAL)
1398        topSizer = wx.BoxSizer(wx.HORIZONTAL)
1399        topSizer.Add(wx.StaticText(self,wx.ID_ANY,title,size=(-1,35)),
1400            1,wx.ALL|wx.EXPAND|WACV,1)
1401        if filterBox:
1402            self.timer = wx.Timer()
1403            self.timer.Bind(wx.EVT_TIMER,self.Filter)
1404            topSizer.Add(wx.StaticText(self,wx.ID_ANY,'Name \nFilter: '),0,wx.ALL|WACV,1)
1405            self.filterBox = wx.TextCtrl(self, wx.ID_ANY, size=(80,-1),style=wx.TE_PROCESS_ENTER)
1406            self.filterBox.Bind(wx.EVT_TEXT,self.onChar)
1407            self.filterBox.Bind(wx.EVT_TEXT_ENTER,self.Filter)
1408            topSizer.Add(self.filterBox,0,wx.ALL|WACV,0)
1409        Sizer.Add(topSizer,0,wx.ALL|wx.EXPAND,8)
1410        self.settingRange = False
1411        self.rangeFirst = None
1412        self.clb = wx.CheckListBox(self, wx.ID_ANY, (30,30), wx.DefaultSize, self.ChoiceList)
1413        self.clb.Bind(wx.EVT_CHECKLISTBOX,self.OnCheck)
1414        if monoFont:
1415            font1 = wx.Font(self.clb.GetFont().GetPointSize(),
1416                            wx.MODERN, wx.NORMAL, wx.NORMAL, False)
1417            self.clb.SetFont(font1)
1418        Sizer.Add(self.clb,1,wx.LEFT|wx.RIGHT|wx.EXPAND,10)
1419        Sizer.Add((-1,10))
1420        # set/toggle buttons
1421        if toggle:
1422            tSizer = wx.FlexGridSizer(cols=2,hgap=5,vgap=5)
1423            tSizer.Add(wx.StaticText(self,label=' Apply stride:'),0,WACV)
1424            numbs = [str(i+1) for i in range(9)]+[str(2*i+10) for i in range(6)]
1425            self.stride = wx.ComboBox(self,value='1',choices=numbs,style=wx.CB_READONLY|wx.CB_DROPDOWN)
1426            self.stride.Bind(wx.EVT_COMBOBOX,self.OnStride)
1427            tSizer.Add(self.stride,0,WACV)
1428            setBut = wx.Button(self,wx.ID_ANY,'Set All')
1429            setBut.Bind(wx.EVT_BUTTON,self._SetAll)
1430            tSizer.Add(setBut)
1431            togBut = wx.Button(self,wx.ID_ANY,'Toggle All')
1432            togBut.Bind(wx.EVT_BUTTON,self._ToggleAll)
1433            tSizer.Add(togBut)
1434            self.rangeBut = wx.ToggleButton(self,wx.ID_ANY,'Set Range')
1435            self.rangeBut.Bind(wx.EVT_TOGGLEBUTTON,self.SetRange)
1436            tSizer.Add(self.rangeBut)
1437            self.rangeCapt = wx.StaticText(self,wx.ID_ANY,'')
1438            tSizer.Add(self.rangeCapt)
1439            Sizer.Add(tSizer,0,wx.LEFT,12)
1440        # Extra widgets
1441        Sizer.Add((-1,5),0,wx.LEFT,0)
1442        bSizer = wx.BoxSizer(wx.VERTICAL)
1443        for lbl in sorted(extraOpts.keys()):
1444            if not lbl.startswith('label'): continue
1445            key = lbl.replace('label','value')
1446            if key not in extraOpts: continue
1447            eSizer = wx.BoxSizer(wx.HORIZONTAL)
1448            if type(extraOpts[key]) is bool:
1449                eSizer.Add(G2CheckBox(self,extraOpts[lbl],extraOpts,key))
1450            else:
1451                eSizer.Add(wx.StaticText(self,wx.ID_ANY,extraOpts[lbl]))
1452                eSizer.Add(ValidatedTxtCtrl(self,extraOpts,key))
1453            bSizer.Add(eSizer,0,wx.LEFT,0)
1454        Sizer.Add(bSizer,0,wx.CENTER,0)
1455        Sizer.Add((-1,5),0,wx.LEFT,0)
1456        # OK/Cancel buttons
1457        btnsizer = wx.StdDialogButtonSizer()
1458        if useOK:
1459            self.OKbtn = wx.Button(self, wx.ID_OK)
1460            self.OKbtn.SetDefault()
1461            btnsizer.AddButton(self.OKbtn)
1462            self.OKbtn.Bind(wx.EVT_BUTTON,self.onOk)
1463        if useCANCEL:
1464            btn = wx.Button(self, wx.ID_CANCEL)
1465            btn.Bind(wx.EVT_BUTTON,self.onCancel)
1466            btnsizer.AddButton(btn)
1467        btnsizer.Realize()
1468        Sizer.Add((-1,5))
1469        Sizer.Add(btnsizer,0,wx.ALIGN_RIGHT,50)
1470        Sizer.Add((-1,20))
1471        # OK done, let's get outa here
1472        self.SetSizer(Sizer)
1473        Sizer.Fit(self)
1474        self.CenterOnParent()
1475       
1476    def onOk(self,event):
1477        parent = self.GetParent()
1478        parent.Raise()
1479        self.EndModal(wx.ID_OK)             
1480       
1481    def onCancel(self,event):
1482        parent = self.GetParent()
1483        parent.Raise()
1484        self.EndModal(wx.ID_CANCEL)
1485       
1486    def OnStride(self,event):
1487        self.Stride = int(self.stride.GetValue())
1488
1489    def SetRange(self,event):
1490        '''Respond to a press of the Set Range button. Set the range flag and
1491        the caption next to the button
1492        '''
1493        self.settingRange = self.rangeBut.GetValue()
1494        if self.settingRange:
1495            self.rangeCapt.SetLabel('Select range start')
1496        else:
1497            self.rangeCapt.SetLabel('')           
1498        self.rangeFirst = None
1499       
1500    def GetSelections(self):
1501        'Returns a list of the indices for the selected choices'
1502        # update self.Selections with settings for displayed items
1503        for i in range(len(self.filterlist)):
1504            self.Selections[self.filterlist[i]] = self.clb.IsChecked(i)
1505        # return all selections, shown or hidden
1506        return [i for i in range(len(self.Selections)) if self.Selections[i]]
1507       
1508    def SetSelections(self,selList):
1509        '''Sets the selection indices in selList as selected. Resets any previous
1510        selections for compatibility with wx.MultiChoiceDialog. Note that
1511        the state for only the filtered items is shown.
1512
1513        :param list selList: indices of items to be selected. These indices
1514          are referenced to the order in self.ChoiceList
1515        '''
1516        self.Selections = len(self.ChoiceList) * [False,] # reset selections
1517        for sel in selList:
1518            self.Selections[sel] = True
1519        self._ShowSelections()
1520
1521    def _ShowSelections(self):
1522        'Show the selection state for displayed items'
1523        self.clb.SetChecked(
1524            [i for i in range(len(self.filterlist)) if self.Selections[self.filterlist[i]]]
1525            ) # Note anything previously checked will be cleared.
1526           
1527    def _SetAll(self,event):
1528        'Set all viewed choices on'
1529        self.clb.SetChecked(range(0,len(self.filterlist),self.Stride))
1530        self.stride.SetValue('1')
1531        self.Stride = 1
1532       
1533    def _ToggleAll(self,event):
1534        'flip the state of all viewed choices'
1535        for i in range(len(self.filterlist)):
1536            self.clb.Check(i,not self.clb.IsChecked(i))
1537           
1538    def onChar(self,event):
1539        'Respond to keyboard events in the Filter box'
1540        self.OKbtn.Enable(False)
1541        if self.timer.IsRunning():
1542            self.timer.Stop()
1543        self.timer.Start(1000,oneShot=True)
1544        if event: event.Skip()
1545       
1546    def OnCheck(self,event):
1547        '''for CheckListBox events; if Set Range is in use, this sets/clears all
1548        entries in range between start and end according to the value in start.
1549        Repeated clicks on the start change the checkbox state, but do not trigger
1550        the range copy.
1551        The caption next to the button is updated on the first button press.
1552        '''
1553        if self.settingRange:
1554            id = event.GetInt()
1555            if self.rangeFirst is None:
1556                name = self.clb.GetString(id)
1557                self.rangeCapt.SetLabel(name+' to...')
1558                self.rangeFirst = id
1559            elif self.rangeFirst == id:
1560                pass
1561            else:
1562                for i in range(min(self.rangeFirst,id), max(self.rangeFirst,id)+1,self.Stride):
1563                    self.clb.Check(i,self.clb.IsChecked(self.rangeFirst))
1564                self.rangeBut.SetValue(False)
1565                self.rangeCapt.SetLabel('')
1566            return
1567       
1568    def Filter(self,event):
1569        '''Read text from filter control and select entries that match. Called by
1570        Timer after a delay with no input or if Enter is pressed.
1571        '''
1572        if self.timer.IsRunning():
1573            self.timer.Stop()
1574        self.GetSelections() # record current selections
1575        txt = self.filterBox.GetValue()
1576        self.clb.Clear()
1577       
1578        self.Update()
1579        self.filterlist = []
1580        if txt:
1581            txt = txt.lower()
1582            ChoiceList = []
1583            for i,item in enumerate(self.ChoiceList):
1584                if item.lower().find(txt) != -1:
1585                    ChoiceList.append(item)
1586                    self.filterlist.append(i)
1587        else:
1588            self.filterlist = range(len(self.ChoiceList))
1589            ChoiceList = self.ChoiceList
1590        self.clb.AppendItems(ChoiceList)
1591        self._ShowSelections()
1592        self.OKbtn.Enable(True)
1593
1594def SelectEdit1Var(G2frame,array,labelLst,elemKeysLst,dspLst,refFlgElem):
1595    '''Select a variable from a list, then edit it and select histograms
1596    to copy it to.
1597
1598    :param wx.Frame G2frame: main GSAS-II frame
1599    :param dict array: the array (dict or list) where values to be edited are kept
1600    :param list labelLst: labels for each data item
1601    :param list elemKeysLst: a list of lists of keys needed to be applied (see below)
1602      to obtain the value of each parameter
1603    :param list dspLst: list list of digits to be displayed (10,4) is 10 digits
1604      with 4 decimal places. Can be None.
1605    :param list refFlgElem: a list of lists of keys needed to be applied (see below)
1606      to obtain the refine flag for each parameter or None if the parameter
1607      does not have refine flag.
1608
1609    Example::
1610      array = data
1611      labelLst = ['v1','v2']
1612      elemKeysLst = [['v1'], ['v2',0]]
1613      refFlgElem = [None, ['v2',1]]
1614
1615     * The value for v1 will be in data['v1'] and this cannot be refined while,
1616     * The value for v2 will be in data['v2'][0] and its refinement flag is data['v2'][1]
1617    '''
1618    def unkey(dct,keylist):
1619        '''dive into a nested set of dicts/lists applying keys in keylist
1620        consecutively
1621        '''
1622        d = dct
1623        for k in keylist:
1624            d = d[k]
1625        return d
1626
1627    def OnChoice(event):
1628        'Respond when a parameter is selected in the Choice box'
1629        if 'phoenix' in wx.version():
1630            valSizer.Clear(True)
1631        else:
1632            valSizer.DeleteWindows()
1633        lbl = event.GetString()
1634        copyopts['currentsel'] = lbl
1635        i = labelLst.index(lbl)
1636        OKbtn.Enable(True)
1637        ch.SetLabel(lbl)
1638        args = {}
1639        if dspLst[i]:
1640            args = {'nDig':dspLst[i]}
1641        Val = ValidatedTxtCtrl(
1642            dlg,
1643            unkey(array,elemKeysLst[i][:-1]),
1644            elemKeysLst[i][-1],
1645            **args)
1646        copyopts['startvalue'] = unkey(array,elemKeysLst[i])
1647        #unkey(array,elemKeysLst[i][:-1])[elemKeysLst[i][-1]] =
1648        valSizer.Add(Val,0,wx.LEFT,5)
1649        dlg.SendSizeEvent()
1650       
1651    # SelectEdit1Var execution begins here
1652    saveArray = copy.deepcopy(array) # keep original values
1653    TreeItemType = G2frame.GPXtree.GetItemText(G2frame.PickId)
1654    copyopts = {'InTable':False,"startvalue":None,'currentsel':None}       
1655    hst = G2frame.GPXtree.GetItemText(G2frame.PatternId)
1656    histList = G2pdG.GetHistsLikeSelected(G2frame)
1657    if not histList:
1658        G2frame.ErrorDialog('No match','No histograms match '+hst,G2frame)
1659        return
1660    dlg = wx.Dialog(G2frame,wx.ID_ANY,'Set a parameter value',
1661        style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)
1662    mainSizer = wx.BoxSizer(wx.VERTICAL)
1663    mainSizer.Add((5,5))
1664    subSizer = wx.BoxSizer(wx.HORIZONTAL)
1665    subSizer.Add((-1,-1),1,wx.EXPAND)
1666    subSizer.Add(wx.StaticText(dlg,wx.ID_ANY,'Select a parameter and set a new value'))
1667    subSizer.Add((-1,-1),1,wx.EXPAND)
1668    mainSizer.Add(subSizer,0,wx.EXPAND,0)
1669    mainSizer.Add((0,10))
1670
1671    subSizer = wx.FlexGridSizer(0,2,5,0)
1672    subSizer.Add(wx.StaticText(dlg,wx.ID_ANY,'Parameter: '))
1673    ch = wx.Choice(dlg, wx.ID_ANY, choices = sorted(labelLst))
1674    ch.SetSelection(-1)
1675    ch.Bind(wx.EVT_CHOICE, OnChoice)
1676    subSizer.Add(ch)
1677    subSizer.Add(wx.StaticText(dlg,wx.ID_ANY,'Value: '))
1678    valSizer = wx.BoxSizer(wx.HORIZONTAL)
1679    subSizer.Add(valSizer)
1680    mainSizer.Add(subSizer)
1681
1682    mainSizer.Add((-1,20))
1683    subSizer = wx.BoxSizer(wx.HORIZONTAL)
1684    subSizer.Add(G2CheckBox(dlg, 'Edit in table ', copyopts, 'InTable'))
1685    mainSizer.Add(subSizer)
1686
1687    btnsizer = wx.StdDialogButtonSizer()
1688    OKbtn = wx.Button(dlg, wx.ID_OK,'Continue')
1689    OKbtn.Enable(False)
1690    OKbtn.SetDefault()
1691    OKbtn.Bind(wx.EVT_BUTTON,lambda event: dlg.EndModal(wx.ID_OK))
1692    btnsizer.AddButton(OKbtn)
1693    btn = wx.Button(dlg, wx.ID_CANCEL)
1694    btnsizer.AddButton(btn)
1695    btnsizer.Realize()
1696    mainSizer.Add((-1,5),1,wx.EXPAND,1)
1697    mainSizer.Add(btnsizer,0,wx.ALIGN_CENTER,0)
1698    mainSizer.Add((-1,10))
1699
1700    dlg.SetSizer(mainSizer)
1701    dlg.CenterOnParent()
1702    if dlg.ShowModal() != wx.ID_OK:
1703        array.update(saveArray)
1704        dlg.Destroy()
1705        return
1706    dlg.Destroy()
1707
1708    copyList = []
1709    lbl = copyopts['currentsel']
1710    dlg = G2MultiChoiceDialog(G2frame,'Copy parameter '+lbl+' from\n'+hst,
1711        'Copy parameters', histList)
1712    dlg.CenterOnParent()
1713    try:
1714        if dlg.ShowModal() == wx.ID_OK:
1715            for i in dlg.GetSelections(): 
1716                copyList.append(histList[i])
1717        else:
1718            # reset the parameter since cancel was pressed
1719            array.update(saveArray)
1720            return
1721    finally:
1722        dlg.Destroy()
1723
1724    prelbl = [hst]
1725    i = labelLst.index(lbl)
1726    keyLst = elemKeysLst[i]
1727    refkeys = refFlgElem[i]
1728    dictlst = [unkey(array,keyLst[:-1])]
1729    if refkeys is not None:
1730        refdictlst = [unkey(array,refkeys[:-1])]
1731    else:
1732        refdictlst = None
1733    Id = G2gd.GetGPXtreeItemId(G2frame,G2frame.root,hst)
1734    hstData = G2frame.GPXtree.GetItemPyData(G2gd.GetGPXtreeItemId(G2frame,Id,'Instrument Parameters'))[0]
1735    for h in copyList:
1736        Id = G2gd.GetGPXtreeItemId(G2frame,G2frame.root,h)
1737        instData = G2frame.GPXtree.GetItemPyData(G2gd.GetGPXtreeItemId(G2frame,Id,'Instrument Parameters'))[0]
1738        if len(hstData) != len(instData) or hstData['Type'][0] != instData['Type'][0]:  #don't mix data types or lam & lam1/lam2 parms!
1739            print (h+' not copied - instrument parameters not commensurate')
1740            continue
1741        hData = G2frame.GPXtree.GetItemPyData(G2gd.GetGPXtreeItemId(G2frame,Id,TreeItemType))
1742        if TreeItemType == 'Instrument Parameters':
1743            hData = hData[0]
1744        #copy the value if it is changed or we will not edit in a table
1745        valNow = unkey(array,keyLst)
1746        if copyopts['startvalue'] != valNow or not copyopts['InTable']:
1747            unkey(hData,keyLst[:-1])[keyLst[-1]] = valNow
1748        prelbl += [h]
1749        dictlst += [unkey(hData,keyLst[:-1])]
1750        if refdictlst is not None:
1751            refdictlst += [unkey(hData,refkeys[:-1])]
1752    if refdictlst is None:
1753        args = {}
1754    else:
1755        args = {'checkdictlst':refdictlst,
1756                'checkelemlst':len(dictlst)*[refkeys[-1]],
1757                'checklabel':'Refine?'}
1758    if copyopts['InTable']:
1759        dlg = ScrolledMultiEditor(
1760            G2frame,dictlst,
1761            len(dictlst)*[keyLst[-1]],prelbl,
1762            header='Editing parameter '+lbl,
1763            CopyButton=True,**args)
1764        dlg.CenterOnParent()
1765        if dlg.ShowModal() != wx.ID_OK:
1766            array.update(saveArray)
1767        dlg.Destroy()
1768
1769################################################################        Single choice Dialog with filter options
1770class G2SingleChoiceDialog(wx.Dialog):
1771    '''A dialog similar to wx.SingleChoiceDialog except that a filter can be
1772    added.
1773
1774    :param wx.Frame ParentFrame: reference to parent frame
1775    :param str title: heading above list of choices
1776    :param str header: Title to place on window frame
1777    :param list ChoiceList: a list of choices where one will be selected
1778    :param bool monoFont: If False (default), use a variable-spaced font;
1779      if True use a equally-spaced font.
1780    :param bool filterBox: If True (default) an input widget is placed on
1781      the window and only entries matching the entered text are shown.
1782    :param kw: optional keyword parameters for the wx.Dialog may
1783      be included such as size [which defaults to `(320,310)`] and
1784      style (which defaults to ``wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER | wx.CENTRE | wx.OK | wx.CANCEL``);
1785      note that ``wx.OK`` and ``wx.CANCEL`` controls
1786      the presence of the eponymous buttons in the dialog.
1787    :returns: the name of the created dialog
1788    '''
1789    def __init__(self,parent, title, header, ChoiceList, 
1790                 monoFont=False, filterBox=True, **kw):
1791        # process keyword parameters, notably style
1792        options = {'size':(320,310), # default Frame keywords
1793                   'style':wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER|wx.CENTRE| wx.OK | wx.CANCEL,
1794                   }
1795        options.update(kw)
1796        self.ChoiceList = ChoiceList
1797        self.filterlist = range(len(self.ChoiceList))
1798        if options['style'] & wx.OK:
1799            useOK = True
1800            options['style'] ^= wx.OK
1801        else:
1802            useOK = False
1803        if options['style'] & wx.CANCEL:
1804            useCANCEL = True
1805            options['style'] ^= wx.CANCEL
1806        else:
1807            useCANCEL = False       
1808        # create the dialog frame
1809        wx.Dialog.__init__(self,parent,wx.ID_ANY,header,**options)
1810        # fill the dialog
1811        Sizer = wx.BoxSizer(wx.VERTICAL)
1812        topSizer = wx.BoxSizer(wx.HORIZONTAL)
1813        topSizer.Add(
1814            wx.StaticText(self,wx.ID_ANY,title,size=(-1,35)),
1815            1,wx.ALL|wx.EXPAND|WACV,1)
1816        if filterBox:
1817            self.timer = wx.Timer()
1818            self.timer.Bind(wx.EVT_TIMER,self.Filter)
1819            topSizer.Add(wx.StaticText(self,wx.ID_ANY,'Filter: '),0,wx.ALL,1)
1820            self.filterBox = wx.TextCtrl(self, wx.ID_ANY, size=(80,-1),
1821                                         style=wx.TE_PROCESS_ENTER)
1822            self.filterBox.Bind(wx.EVT_CHAR,self.onChar)
1823            self.filterBox.Bind(wx.EVT_TEXT_ENTER,self.Filter)
1824        topSizer.Add(self.filterBox,0,wx.ALL,0)
1825        Sizer.Add(topSizer,0,wx.ALL|wx.EXPAND,8)
1826        self.clb = wx.ListBox(self, wx.ID_ANY, (30,30), wx.DefaultSize, ChoiceList)
1827        self.clb.Bind(wx.EVT_LEFT_DCLICK,self.onDoubleClick)
1828        if monoFont:
1829            font1 = wx.Font(self.clb.GetFont().GetPointSize(),
1830                            wx.MODERN, wx.NORMAL, wx.NORMAL, False)
1831            self.clb.SetFont(font1)
1832        Sizer.Add(self.clb,1,wx.LEFT|wx.RIGHT|wx.EXPAND,10)
1833        Sizer.Add((-1,10))
1834        # OK/Cancel buttons
1835        btnsizer = wx.StdDialogButtonSizer()
1836        if useOK:
1837            self.OKbtn = wx.Button(self, wx.ID_OK)
1838            self.OKbtn.SetDefault()
1839            btnsizer.AddButton(self.OKbtn)
1840        if useCANCEL:
1841            btn = wx.Button(self, wx.ID_CANCEL)
1842            btnsizer.AddButton(btn)
1843        btnsizer.Realize()
1844        Sizer.Add((-1,5))
1845        Sizer.Add(btnsizer,0,wx.ALIGN_RIGHT,50)
1846        Sizer.Add((-1,20))
1847        # OK done, let's get outa here
1848        self.SetSizer(Sizer)
1849    def GetSelection(self):
1850        'Returns the index of the selected choice'
1851        i = self.clb.GetSelection()
1852        if i < 0 or i >= len(self.filterlist):
1853            return wx.NOT_FOUND
1854        return self.filterlist[i]
1855    def onChar(self,event):
1856        self.OKbtn.Enable(False)
1857        if self.timer.IsRunning():
1858            self.timer.Stop()
1859        self.timer.Start(1000,oneShot=True)
1860        if event: event.Skip()
1861    def Filter(self,event):
1862        if self.timer.IsRunning():
1863            self.timer.Stop()
1864        txt = self.filterBox.GetValue()
1865        self.clb.Clear()
1866        self.Update()
1867        self.filterlist = []
1868        if txt:
1869            txt = txt.lower()
1870            ChoiceList = []
1871            for i,item in enumerate(self.ChoiceList):
1872                if item.lower().find(txt) != -1:
1873                    ChoiceList.append(item)
1874                    self.filterlist.append(i)
1875        else:
1876            self.filterlist = range(len(self.ChoiceList))
1877            ChoiceList = self.ChoiceList
1878        self.clb.AppendItems(ChoiceList)
1879        self.OKbtn.Enable(True)
1880    def onDoubleClick(self,event):
1881        self.EndModal(wx.ID_OK)
1882       
1883################################################################################
1884class FlagSetDialog(wx.Dialog):
1885    ''' Creates popup with table of variables to be checked for e.g. refinement flags
1886    '''
1887    def __init__(self,parent,title,colnames,rownames,flags):
1888        wx.Dialog.__init__(self,parent,-1,title,
1889            pos=wx.DefaultPosition,style=wx.DEFAULT_DIALOG_STYLE)
1890        self.panel = None
1891        self.colnames = colnames
1892        self.rownames = rownames
1893        self.flags = flags
1894        self.newflags = copy.copy(flags)
1895        self.Draw()
1896       
1897    def Draw(self):
1898        Indx = {}
1899       
1900        def OnSelection(event):
1901            Obj = event.GetEventObject()
1902            [name,ia] = Indx[Obj.GetId()]
1903            self.newflags[name][ia] = Obj.GetValue()
1904           
1905        if self.panel:
1906            self.panel.DestroyChildren()  #safe: wx.Panel
1907            self.panel.Destroy()
1908        self.panel = wx.Panel(self)
1909        mainSizer = wx.BoxSizer(wx.VERTICAL)
1910        flagSizer = wx.FlexGridSizer(0,len(self.colnames),5,5)
1911        for item in self.colnames:
1912            flagSizer.Add(wx.StaticText(self.panel,label=item),0,WACV)
1913        for ia,atm in enumerate(self.rownames):
1914            flagSizer.Add(wx.StaticText(self.panel,label=atm),0,WACV)
1915            for name in self.colnames[1:]:
1916                if self.flags[name][ia]:
1917                    self.newflags[name][ia] = False     #default is off
1918                    flg = wx.CheckBox(self.panel,-1,label='')
1919                    flg.Bind(wx.EVT_CHECKBOX,OnSelection)
1920                    Indx[flg.GetId()] = [name,ia]
1921                    flagSizer.Add(flg,0,WACV)
1922                else:
1923                    flagSizer.Add(wx.StaticText(self.panel,label='na'),0,WACV)
1924           
1925        mainSizer.Add(flagSizer,0)
1926        OkBtn = wx.Button(self.panel,-1,"Ok")
1927        OkBtn.Bind(wx.EVT_BUTTON, self.OnOk)
1928        CancelBtn = wx.Button(self.panel,-1,'Cancel')
1929        CancelBtn.Bind(wx.EVT_BUTTON, self.OnCancel)
1930        btnSizer = wx.BoxSizer(wx.HORIZONTAL)
1931        btnSizer.Add((20,20),1)
1932        btnSizer.Add(OkBtn)
1933        btnSizer.Add(CancelBtn)
1934        btnSizer.Add((20,20),1)
1935        mainSizer.Add(btnSizer,0,wx.EXPAND|wx.BOTTOM|wx.TOP, 10)
1936        self.panel.SetSizer(mainSizer)
1937        self.panel.Fit()
1938        self.Fit()
1939       
1940    def GetSelection(self):
1941        return self.newflags
1942
1943    def OnOk(self,event):
1944        parent = self.GetParent()
1945        parent.Raise()
1946        self.EndModal(wx.ID_OK)             
1947       
1948    def OnCancel(self,event):
1949        parent = self.GetParent()
1950        parent.Raise()
1951        self.EndModal(wx.ID_CANCEL)
1952
1953###################################################################,#############
1954def G2MessageBox(parent,msg,title='Error'):
1955    '''Simple code to display a error or warning message
1956    '''
1957    dlg = wx.MessageDialog(parent,StripIndents(msg), title, wx.OK)
1958    dlg.ShowModal()
1959    dlg.Destroy()
1960   
1961################################################################################
1962class PickTwoDialog(wx.Dialog):
1963    '''This does not seem to be in use
1964    '''
1965    def __init__(self,parent,title,prompt,names,choices):
1966        wx.Dialog.__init__(self,parent,-1,title, 
1967            pos=wx.DefaultPosition,style=wx.DEFAULT_DIALOG_STYLE)
1968        self.panel = None
1969        self.prompt = prompt
1970        self.choices = choices
1971        self.names = names
1972        self.Draw()
1973
1974    def Draw(self):
1975        Indx = {}
1976       
1977        def OnSelection(event):
1978            Obj = event.GetEventObject()
1979            id = Indx[Obj.GetId()]
1980            self.choices[id] = Obj.GetValue().encode()  #to avoid Unicode versions
1981            self.Draw()
1982           
1983        if self.panel:
1984            self.panel.DestroyChildren()  #safe: wx.Panel
1985            self.panel.Destroy()
1986        self.panel = wx.Panel(self)
1987        mainSizer = wx.BoxSizer(wx.VERTICAL)
1988        mainSizer.Add(wx.StaticText(self.panel,-1,self.prompt),0,wx.ALIGN_CENTER)
1989        for isel,name in enumerate(self.choices):
1990            lineSizer = wx.BoxSizer(wx.HORIZONTAL)
1991            lineSizer.Add(wx.StaticText(self.panel,-1,'Reference atom '+str(isel+1)),0,wx.ALIGN_CENTER)
1992            nameList = self.names[:]
1993            if isel:
1994                if self.choices[0] in nameList:
1995                    nameList.remove(self.choices[0])
1996            choice = wx.ComboBox(self.panel,-1,value=name,choices=nameList,
1997                style=wx.CB_READONLY|wx.CB_DROPDOWN)
1998            Indx[choice.GetId()] = isel
1999            choice.Bind(wx.EVT_COMBOBOX, OnSelection)
2000            lineSizer.Add(choice,0,WACV)
2001            mainSizer.Add(lineSizer)
2002        OkBtn = wx.Button(self.panel,-1,"Ok")
2003        OkBtn.Bind(wx.EVT_BUTTON, self.OnOk)
2004        CancelBtn = wx.Button(self.panel,-1,'Cancel')
2005        CancelBtn.Bind(wx.EVT_BUTTON, self.OnCancel)
2006        btnSizer = wx.BoxSizer(wx.HORIZONTAL)
2007        btnSizer.Add((20,20),1)
2008        btnSizer.Add(OkBtn)
2009        btnSizer.Add(CancelBtn)
2010        btnSizer.Add((20,20),1)
2011        mainSizer.Add(btnSizer,0,wx.EXPAND|wx.BOTTOM|wx.TOP, 10)
2012        self.panel.SetSizer(mainSizer)
2013        self.panel.Fit()
2014        self.Fit()
2015       
2016    def GetSelection(self):
2017        return self.choices
2018
2019    def OnOk(self,event):
2020        parent = self.GetParent()
2021        parent.Raise()
2022        self.EndModal(wx.ID_OK)             
2023       
2024    def OnCancel(self,event):
2025        parent = self.GetParent()
2026        parent.Raise()
2027        self.EndModal(wx.ID_CANCEL)
2028
2029################################################################################
2030class SingleFloatDialog(wx.Dialog):
2031    '''Dialog to obtain a single float value from user
2032
2033    Typical usage::
2034
2035            limits = (0,1)
2036            dlg = G2G.SingleFloatDialog(G2frame,'New value','Enter new value for...',default,limits)
2037            if dlg.ShowModal() == wx.ID_OK:
2038                parm = dlg.GetValue()
2039            dlg.Destroy()   
2040
2041    '''
2042    def __init__(self,parent,title,prompt,value,limits=[0.,1.],format='%.5g'):
2043        wx.Dialog.__init__(self,parent,-1,title, 
2044            pos=wx.DefaultPosition,style=wx.DEFAULT_DIALOG_STYLE)
2045        self.panel = None
2046        self.limits = limits
2047        self.value = value
2048        self.prompt = prompt
2049        self.format = format
2050        self.Draw()
2051       
2052    def Draw(self):
2053       
2054        def OnValItem(event):
2055            if event: event.Skip()
2056            try:
2057                val = float(valItem.GetValue())
2058                if val < self.limits[0] or val > self.limits[1]:
2059                    raise ValueError
2060            except ValueError:
2061                val = self.value
2062            self.value = val
2063            valItem.SetValue(self.format%(self.value))
2064           
2065        if self.panel: self.panel.Destroy()
2066        self.panel = wx.Panel(self)
2067        mainSizer = wx.BoxSizer(wx.VERTICAL)
2068        mainSizer.Add(wx.StaticText(self.panel,-1,self.prompt),0,wx.ALIGN_CENTER)
2069        valItem = wx.TextCtrl(self.panel,-1,value=self.format%(self.value),style=wx.TE_PROCESS_ENTER)
2070        mainSizer.Add(valItem,0,wx.ALIGN_CENTER)
2071        valItem.Bind(wx.EVT_TEXT_ENTER,OnValItem)
2072        valItem.Bind(wx.EVT_KILL_FOCUS,OnValItem)
2073        OkBtn = wx.Button(self.panel,-1,"Ok")
2074        OkBtn.Bind(wx.EVT_BUTTON, self.OnOk)
2075        CancelBtn = wx.Button(self.panel,-1,'Cancel')
2076        CancelBtn.Bind(wx.EVT_BUTTON, self.OnCancel)
2077        btnSizer = wx.BoxSizer(wx.HORIZONTAL)
2078        btnSizer.Add((20,20),1)
2079        btnSizer.Add(OkBtn)
2080        btnSizer.Add(CancelBtn)
2081        btnSizer.Add((20,20),1)
2082        mainSizer.Add(btnSizer,0,wx.EXPAND|wx.BOTTOM|wx.TOP, 10)
2083        self.panel.SetSizer(mainSizer)
2084        self.panel.Fit()
2085        self.Fit()
2086
2087    def GetValue(self):
2088        return self.value
2089       
2090    def OnOk(self,event):
2091        parent = self.GetParent()
2092        parent.Raise()
2093        self.EndModal(wx.ID_OK)             
2094       
2095    def OnCancel(self,event):
2096        parent = self.GetParent()
2097        parent.Raise()
2098        self.EndModal(wx.ID_CANCEL)
2099
2100################################################################################
2101class MultiFloatDialog(wx.Dialog):
2102    'Dialog to obtain a multi float value from user'
2103    def __init__(self,parent,title,prompts,values,limits=[[0.,1.],],formats=['%.5g',]):
2104        wx.Dialog.__init__(self,parent,-1,title, 
2105            pos=wx.DefaultPosition,style=wx.DEFAULT_DIALOG_STYLE)
2106        self.panel = None
2107        self.limits = limits
2108        self.values = values
2109        self.prompts = prompts
2110        self.formats = formats
2111        self.Draw()
2112       
2113    def Draw(self):
2114       
2115        def OnValItem(event):
2116            if event: event.Skip()
2117            Obj = event.GetEventObject()
2118            id,limits,format = Indx[Obj]
2119            try:
2120                val = float(Obj.GetValue())
2121                if val < limits[0] or val > limits[1]:
2122                    raise ValueError
2123            except ValueError:
2124                val = self.values[id]
2125            self.values[id] = val
2126            Obj.SetValue(format%(val))
2127           
2128        Indx = {}
2129        if self.panel: self.panel.Destroy()
2130        self.panel = wx.Panel(self)
2131        mainSizer = wx.BoxSizer(wx.VERTICAL)
2132        lineSizer = wx.FlexGridSizer(0,2,5,5)
2133        for id,[prompt,value,limits,format] in enumerate(zip(self.prompts,self.values,self.limits,self.formats)):
2134            lineSizer.Add(wx.StaticText(self.panel,label=prompt),0,wx.ALIGN_CENTER)
2135            valItem = wx.TextCtrl(self.panel,value=format%(value),style=wx.TE_PROCESS_ENTER)
2136            Indx[valItem] = [id,limits,format]
2137            lineSizer.Add(valItem,0,wx.ALIGN_CENTER)
2138            valItem.Bind(wx.EVT_TEXT_ENTER,OnValItem)
2139            valItem.Bind(wx.EVT_KILL_FOCUS,OnValItem)
2140        mainSizer.Add(lineSizer)
2141        OkBtn = wx.Button(self.panel,-1,"Ok")
2142        OkBtn.Bind(wx.EVT_BUTTON, self.OnOk)
2143        CancelBtn = wx.Button(self.panel,-1,'Cancel')
2144        CancelBtn.Bind(wx.EVT_BUTTON, self.OnCancel)
2145        btnSizer = wx.BoxSizer(wx.HORIZONTAL)
2146        btnSizer.Add((20,20),1)
2147        btnSizer.Add(OkBtn)
2148        btnSizer.Add(CancelBtn)
2149        btnSizer.Add((20,20),1)
2150        mainSizer.Add(btnSizer,0,wx.EXPAND|wx.BOTTOM|wx.TOP, 10)
2151        self.panel.SetSizer(mainSizer)
2152        self.panel.Fit()
2153        self.Fit()
2154
2155    def GetValues(self):
2156        return self.values
2157       
2158    def OnOk(self,event):
2159        parent = self.GetParent()
2160        parent.Raise()
2161        self.EndModal(wx.ID_OK)             
2162       
2163    def OnCancel(self,event):
2164        parent = self.GetParent()
2165        parent.Raise()
2166        self.EndModal(wx.ID_CANCEL)
2167
2168################################################################################
2169class SingleStringDialog(wx.Dialog):
2170    '''Dialog to obtain a single string value from user
2171   
2172    :param wx.Frame parent: name of parent frame
2173    :param str title: title string for dialog
2174    :param str prompt: string to tell use what they are inputting
2175    :param str value: default input value, if any
2176    '''
2177    def __init__(self,parent,title,prompt,value='',size=(200,-1),help=''):
2178        wx.Dialog.__init__(self,parent,wx.ID_ANY,title, 
2179                           pos=wx.DefaultPosition,
2180                           style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)
2181        self.value = value
2182        self.prompt = prompt
2183        self.CenterOnParent()
2184        self.panel = wx.Panel(self)
2185        mainSizer = wx.BoxSizer(wx.VERTICAL)
2186        if help:
2187            sizer1 = wx.BoxSizer(wx.HORIZONTAL)   
2188            sizer1.Add((-1,-1),1, wx.EXPAND, 0)
2189            sizer1.Add(HelpButton(self.panel,help),0,wx.ALIGN_RIGHT|wx.ALL)
2190            mainSizer.Add(sizer1,0,wx.ALIGN_RIGHT|wx.EXPAND)
2191        mainSizer.Add(wx.StaticText(self.panel,-1,self.prompt),0,wx.ALIGN_CENTER)
2192        self.valItem = wx.TextCtrl(self.panel,-1,value=self.value,size=size)
2193        mainSizer.Add(self.valItem,0,wx.ALIGN_CENTER)
2194        btnsizer = wx.StdDialogButtonSizer()
2195        OKbtn = wx.Button(self.panel, wx.ID_OK)
2196        OKbtn.SetDefault()
2197        btnsizer.AddButton(OKbtn)
2198        btn = wx.Button(self.panel, wx.ID_CANCEL)
2199        btnsizer.AddButton(btn)
2200        btnsizer.Realize()
2201        mainSizer.Add(btnsizer,0,wx.ALIGN_CENTER)
2202        self.panel.SetSizer(mainSizer)
2203        self.panel.Fit()
2204        self.Fit()
2205
2206    def Show(self):
2207        '''Use this method after creating the dialog to post it
2208        :returns: True if the user pressed OK; False if the User pressed Cancel
2209        '''
2210        if self.ShowModal() == wx.ID_OK:
2211            self.value = self.valItem.GetValue()
2212            return True
2213        else:
2214            return False
2215
2216    def GetValue(self):
2217        '''Use this method to get the value entered by the user
2218        :returns: string entered by user
2219        '''
2220        return self.value
2221
2222################################################################################
2223class MultiStringDialog(wx.Dialog):
2224    '''Dialog to obtain a multi string values from user
2225   
2226    :param wx.Frame parent: name of parent frame
2227    :param str title: title string for dialog
2228    :param str prompts: strings to tell use what they are inputting
2229    :param str values: default input values, if any
2230    :param int size: length of the input box in pixels
2231    '''
2232    def __init__(self,parent,title,prompts,values=[],size=-1):
2233       
2234        wx.Dialog.__init__(self,parent,wx.ID_ANY,title, 
2235                           pos=wx.DefaultPosition,
2236                           style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)
2237        self.values = values
2238        self.prompts = prompts
2239        self.CenterOnParent()
2240        mainSizer = wx.BoxSizer(wx.VERTICAL)
2241        promptSizer = wx.FlexGridSizer(0,2,5,5)
2242        self.Indx = {}
2243        for prompt,value in zip(prompts,values):
2244            promptSizer.Add(wx.StaticText(self,-1,prompt),0,WACV)
2245            valItem = wx.TextCtrl(self,-1,value=value,style=wx.TE_PROCESS_ENTER,size=(size,-1))
2246            self.Indx[valItem.GetId()] = prompt
2247            valItem.Bind(wx.EVT_TEXT,self.newValue)
2248            promptSizer.Add(valItem,1,WACV|wx.EXPAND,1)
2249        mainSizer.Add(promptSizer,1,wx.ALL|wx.EXPAND,1)
2250        btnsizer = wx.StdDialogButtonSizer()
2251        OKbtn = wx.Button(self, wx.ID_OK)
2252        OKbtn.SetDefault()
2253        btnsizer.AddButton(OKbtn)
2254        btn = wx.Button(self, wx.ID_CANCEL)
2255        btnsizer.AddButton(btn)
2256        btnsizer.Realize()
2257        mainSizer.Add(btnsizer,0,wx.ALIGN_CENTER)
2258        self.SetSizer(mainSizer)
2259        self.Fit()
2260       
2261    def newValue(self,event):
2262        Obj = event.GetEventObject()
2263        item = self.Indx[Obj.GetId()]
2264        id = self.prompts.index(item)
2265        self.values[id] = Obj.GetValue()
2266
2267    def Show(self):
2268        '''Use this method after creating the dialog to post it
2269        :returns: True if the user pressed OK; False if the User pressed Cancel
2270        '''
2271        if self.ShowModal() == wx.ID_OK:
2272            return True
2273        else:
2274            return False
2275
2276    def GetValues(self):
2277        '''Use this method to get the value entered by the user
2278        :returns: string entered by user
2279        '''
2280        return self.values
2281
2282################################################################################
2283class G2ColumnIDDialog(wx.Dialog):
2284    '''A dialog for matching column data to desired items; some columns may be ignored.
2285   
2286    :param wx.Frame ParentFrame: reference to parent frame
2287    :param str title: heading above list of choices
2288    :param str header: Title to place on window frame
2289    :param list ChoiceList: a list of possible choices for the columns
2290    :param list ColumnData: lists of column data to be matched with ChoiceList
2291    :param bool monoFont: If False (default), use a variable-spaced font;
2292      if True use a equally-spaced font.
2293    :param kw: optional keyword parameters for the wx.Dialog may
2294      be included such as size [which defaults to `(320,310)`] and
2295      style (which defaults to ``wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER | wx.CENTRE | wx.OK | wx.CANCEL``);
2296      note that ``wx.OK`` and ``wx.CANCEL`` controls
2297      the presence of the eponymous buttons in the dialog.
2298    :returns: the name of the created dialog
2299   
2300    '''
2301
2302    def __init__(self,parent, title, header,Comments,ChoiceList, ColumnData,
2303                 monoFont=False, **kw):
2304
2305        def OnOk(sevent):
2306            OK = True
2307            selCols = []
2308            for col in self.sel:
2309                item = col.GetValue()
2310                if item != ' ' and item in selCols:
2311                    OK = False
2312                    break
2313                else:
2314                    selCols.append(item)
2315            parent = self.GetParent()
2316            if not OK:
2317                parent.ErrorDialog('Duplicate',item+' selected more than once')
2318                return
2319            parent.Raise()
2320            self.EndModal(wx.ID_OK)
2321           
2322        def OnModify(event):
2323            if event: event.Skip()
2324            Obj = event.GetEventObject()
2325            icol,colData = Indx[Obj.GetId()]
2326            modify = Obj.GetValue()
2327            if not modify:
2328                return
2329            #print 'Modify column',icol,' by', modify
2330            for i,item in enumerate(self.ColumnData[icol]):
2331                self.ColumnData[icol][i] = str(eval(item+modify))
2332            colData.SetValue('\n'.join(self.ColumnData[icol]))
2333            Obj.SetValue('')
2334           
2335        # process keyword parameters, notably style
2336        options = {'size':(600,310), # default Frame keywords
2337                   'style':wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER|wx.CENTRE| wx.OK | wx.CANCEL,
2338                   }
2339        options.update(kw)
2340        self.Comments = ''.join(Comments)
2341        self.ChoiceList = ChoiceList
2342        self.ColumnData = ColumnData
2343        nCol = len(ColumnData)
2344        if options['style'] & wx.OK:
2345            useOK = True
2346            options['style'] ^= wx.OK
2347        else:
2348            useOK = False
2349        if options['style'] & wx.CANCEL:
2350            useCANCEL = True
2351            options['style'] ^= wx.CANCEL
2352        else:
2353            useCANCEL = False       
2354        # create the dialog frame
2355        wx.Dialog.__init__(self,parent,wx.ID_ANY,header,**options)
2356        panel = wxscroll.ScrolledPanel(self)
2357        # fill the dialog
2358        Sizer = wx.BoxSizer(wx.VERTICAL)
2359        Sizer.Add((-1,5))
2360        Sizer.Add(wx.StaticText(panel,label=title),0,WACV)
2361        if self.Comments:
2362            Sizer.Add(wx.StaticText(panel,label=' Header lines:'),0,WACV)
2363            Sizer.Add(wx.TextCtrl(panel,value=self.Comments,size=(200,-1),
2364                style=wx.TE_MULTILINE|wx.TE_READONLY|wx.TE_DONTWRAP),0,wx.ALL|wx.EXPAND|WACV,8)
2365        columnsSizer = wx.FlexGridSizer(0,nCol,5,10)
2366        self.sel = []
2367        self.mod = []
2368        Indx = {}
2369        for icol,col in enumerate(self.ColumnData):
2370            colSizer = wx.BoxSizer(wx.VERTICAL)
2371            colSizer.Add(wx.StaticText(panel,label=' Column #%d Select:'%(icol)),0,WACV)
2372            self.sel.append(wx.ComboBox(panel,value=' ',choices=self.ChoiceList,style=wx.CB_READONLY|wx.CB_DROPDOWN))
2373            colSizer.Add(self.sel[-1])
2374            colData = wx.TextCtrl(panel,value='\n'.join(self.ColumnData[icol]),size=(120,-1),
2375                style=wx.TE_MULTILINE|wx.TE_READONLY|wx.TE_DONTWRAP)
2376            colSizer.Add(colData,0,WACV)
2377            colSizer.Add(wx.StaticText(panel,label=' Modify by:'),0,WACV)
2378            mod = wx.TextCtrl(panel,size=(120,-1),value='',style=wx.TE_PROCESS_ENTER)
2379            mod.Bind(wx.EVT_TEXT_ENTER,OnModify)
2380            mod.Bind(wx.EVT_KILL_FOCUS,OnModify)
2381            Indx[mod.GetId()] = [icol,colData]
2382            colSizer.Add(mod,0,WACV)
2383            columnsSizer.Add(colSizer)
2384        Sizer.Add(columnsSizer)
2385        Sizer.Add(wx.StaticText(panel,label=' For modify by, enter arithmetic string eg. "-12345.67". "+","-","*","/","**" all allowed'),0,WACV) 
2386        Sizer.Add((-1,10))
2387        # OK/Cancel buttons
2388        btnsizer = wx.StdDialogButtonSizer()
2389        if useOK:
2390            self.OKbtn = wx.Button(panel, wx.ID_OK)
2391            self.OKbtn.SetDefault()
2392            btnsizer.AddButton(self.OKbtn)
2393            self.OKbtn.Bind(wx.EVT_BUTTON, OnOk)
2394        if useCANCEL:
2395            btn = wx.Button(panel, wx.ID_CANCEL)
2396            btnsizer.AddButton(btn)
2397        btnsizer.Realize()
2398        Sizer.Add((-1,5))
2399        Sizer.Add(btnsizer,0,wx.ALIGN_LEFT,20)
2400        Sizer.Add((-1,5))
2401        # OK done, let's get outa here
2402        panel.SetSizer(Sizer)
2403        panel.SetAutoLayout(1)
2404        panel.SetupScrolling()
2405        Size = [450,375]
2406        panel.SetSize(Size)
2407        Size[0] += 25; Size[1]+= 25
2408        self.SetSize(Size)
2409       
2410    def GetSelection(self):
2411        'Returns the selected sample parm for each column'
2412        selCols = []
2413        for item in self.sel:
2414            selCols.append(item.GetValue())
2415        return selCols,self.ColumnData
2416   
2417################################################################################
2418class G2HistoDataDialog(wx.Dialog):
2419    '''A dialog for editing histogram data globally.
2420   
2421    :param wx.Frame ParentFrame: reference to parent frame
2422    :param str title: heading above list of choices
2423    :param str header: Title to place on window frame
2424    :param list ParmList: a list of names for the columns
2425    :param list ParmFmt: a list of formatting strings for the columns
2426    :param list: HistoList: a list of histogram names
2427    :param list ParmData: a list of lists of data matched to ParmList; one for each item in HistoList
2428    :param bool monoFont: If False (default), use a variable-spaced font;
2429      if True use a equally-spaced font.
2430    :param kw: optional keyword parameters for the wx.Dialog may
2431      be included such as size [which defaults to `(320,310)`] and
2432      style (which defaults to
2433      ``wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER | wx.CENTRE | wx.OK | wx.CANCEL``);
2434      note that ``wx.OK`` and ``wx.CANCEL`` controls the presence of the eponymous buttons in the dialog.
2435    :returns: the modified ParmData
2436   
2437    '''
2438
2439    def __init__(self,parent, title, header,ParmList,ParmFmt,HistoList,ParmData,
2440                 monoFont=False, **kw):
2441
2442        def OnOk(sevent):
2443            parent.Raise()
2444            self.EndModal(wx.ID_OK)
2445           
2446        def OnModify(event):
2447            Obj = event.GetEventObject()
2448            irow,it = Indx[Obj.GetId()]
2449            try:
2450                val = float(Obj.GetValue())
2451            except ValueError:
2452                val = self.ParmData[irow][it]
2453            self.ParmData[irow][it] = val
2454            Obj.SetValue(self.ParmFmt[it]%val)
2455                       
2456        # process keyword parameters, notably style
2457        options = {'size':(600,310), # default Frame keywords
2458                   'style':wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER|wx.CENTRE| wx.OK | wx.CANCEL,
2459                   }
2460        options.update(kw)
2461        self.ParmList = ParmList
2462        self.ParmFmt = ParmFmt
2463        self.HistoList = HistoList
2464        self.ParmData = ParmData
2465        nCol = len(ParmList)
2466        if options['style'] & wx.OK:
2467            useOK = True
2468            options['style'] ^= wx.OK
2469        else:
2470            useOK = False
2471        if options['style'] & wx.CANCEL:
2472            useCANCEL = True
2473            options['style'] ^= wx.CANCEL
2474        else:
2475            useCANCEL = False       
2476        # create the dialog frame
2477        wx.Dialog.__init__(self,parent,wx.ID_ANY,header,**options)
2478        panel = wxscroll.ScrolledPanel(self)
2479        # fill the dialog
2480        Sizer = wx.BoxSizer(wx.VERTICAL)
2481        Sizer.Add((-1,5))
2482        Sizer.Add(wx.StaticText(panel,label=title),0,WACV)
2483        dataSizer = wx.FlexGridSizer(0,nCol+1,0,0)
2484        self.sel = []
2485        self.mod = []
2486        Indx = {}
2487        for item in ['Histogram',]+self.ParmList:
2488            dataSizer.Add(wx.StaticText(panel,-1,label=' %10s '%(item)),0,WACV)
2489        for irow,name in enumerate(self.HistoList):
2490            dataSizer.Add(wx.StaticText(panel,label=name),0,WACV|wx.LEFT|wx.RIGHT,10)
2491            for it,item in enumerate(self.ParmData[irow]):
2492                dat = wx.TextCtrl(panel,-1,value=self.ParmFmt[it]%(item),style=wx.TE_PROCESS_ENTER)
2493                dataSizer.Add(dat,0,WACV)
2494                dat.Bind(wx.EVT_TEXT_ENTER,OnModify)
2495                dat.Bind(wx.EVT_KILL_FOCUS,OnModify)
2496                Indx[dat.GetId()] = [irow,it]
2497        Sizer.Add(dataSizer)
2498        Sizer.Add((-1,10))
2499        # OK/Cancel buttons
2500        btnsizer = wx.StdDialogButtonSizer()
2501        if useOK:
2502            self.OKbtn = wx.Button(panel, wx.ID_OK)
2503            self.OKbtn.SetDefault()
2504            btnsizer.AddButton(self.OKbtn)
2505            self.OKbtn.Bind(wx.EVT_BUTTON, OnOk)
2506        if useCANCEL:
2507            btn = wx.Button(panel, wx.ID_CANCEL)
2508            btnsizer.AddButton(btn)
2509        btnsizer.Realize()
2510        Sizer.Add((-1,5))
2511        Sizer.Add(btnsizer,0,wx.ALIGN_LEFT,20)
2512        Sizer.Add((-1,5))
2513        # OK done, let's get outa here
2514        panel.SetSizer(Sizer)
2515        panel.SetAutoLayout(1)
2516        panel.SetupScrolling()
2517        Size = [450,375]
2518        panel.SetSize(Size)
2519        Size[0] += 25; Size[1]+= 25
2520        self.SetSize(Size)
2521       
2522    def GetData(self):
2523        'Returns the modified ParmData'
2524        return self.ParmData
2525   
2526################################################################################
2527def ItemSelector(ChoiceList, ParentFrame=None,
2528                 title='Select an item',
2529                 size=None, header='Item Selector',
2530                 useCancel=True,multiple=False):
2531    ''' Provide a wx dialog to select a single item or multiple items from list of choices
2532
2533    :param list ChoiceList: a list of choices where one will be selected
2534    :param wx.Frame ParentFrame: Name of parent frame (default None)
2535    :param str title: heading above list of choices (default 'Select an item')
2536    :param wx.Size size: Size for dialog to be created (default None -- size as needed)
2537    :param str header: Title to place on window frame (default 'Item Selector')
2538    :param bool useCancel: If True (default) both the OK and Cancel buttons are offered
2539    :param bool multiple: If True then multiple items can be selected (default False)
2540   
2541    :returns: the selection index or None or a selection list if multiple is true
2542
2543    Called by GSASIIdataGUI.OnReOrgSelSeq() Which is not fully implemented.
2544    '''
2545    if multiple:
2546        if useCancel:
2547            dlg = G2MultiChoiceDialog(
2548                ParentFrame,title, header, ChoiceList)
2549        else:
2550            dlg = G2MultiChoiceDialog(
2551                ParentFrame,title, header, ChoiceList,
2552                style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER|wx.OK|wx.CENTRE)
2553    else:
2554        if useCancel:
2555            dlg = wx.SingleChoiceDialog(
2556                ParentFrame,title, header, ChoiceList)
2557        else:
2558            dlg = wx.SingleChoiceDialog(
2559                ParentFrame,title, header,ChoiceList,
2560                style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER|wx.OK|wx.CENTRE)
2561    if size: dlg.SetSize(size)
2562    if dlg.ShowModal() == wx.ID_OK:
2563        if multiple:
2564            dlg.Destroy()
2565            return dlg.GetSelections()
2566        else:
2567            dlg.Destroy()
2568            return dlg.GetSelection()
2569    else:
2570        dlg.Destroy()
2571        return None
2572    dlg.Destroy()
2573
2574######################################################### Column-order selection dialog
2575def GetItemOrder(parent,keylist,vallookup,posdict):
2576    '''Creates a panel where items can be ordered into columns
2577   
2578    :param list keylist: is a list of keys for column assignments
2579    :param dict vallookup: is a dict keyed by names in keylist where each item is a dict.
2580       Each inner dict contains variable names as keys and their associated values
2581    :param dict posdict: is a dict keyed by names in keylist where each item is a dict.
2582       Each inner dict contains column numbers as keys and their associated
2583       variable name as a value. This is used for both input and output.
2584       
2585    '''
2586    dlg = wx.Dialog(parent,style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)
2587    sizer = wx.BoxSizer(wx.VERTICAL)
2588    spanel = OrderBox(dlg,keylist,vallookup,posdict)
2589    spanel.Fit()
2590    sizer.Add(spanel,1,wx.EXPAND)
2591    btnsizer = wx.StdDialogButtonSizer()
2592    btn = wx.Button(dlg, wx.ID_OK)
2593    btn.SetDefault()
2594    btnsizer.AddButton(btn)
2595    #btn = wx.Button(dlg, wx.ID_CANCEL)
2596    #btnsizer.AddButton(btn)
2597    btnsizer.Realize()
2598    sizer.Add(btnsizer, 0, wx.ALIGN_CENTER_VERTICAL|wx.EXPAND|wx.ALL, 5)
2599    dlg.SetSizer(sizer)
2600    sizer.Fit(dlg)
2601    dlg.ShowModal()
2602
2603################################################################################
2604class MultiIntegerDialog(wx.Dialog):
2605    '''Input a series of integers based on prompts
2606    '''
2607    def __init__(self,parent,title,prompts,values):
2608        wx.Dialog.__init__(self,parent,-1,title, 
2609            pos=wx.DefaultPosition,style=wx.DEFAULT_DIALOG_STYLE)
2610        self.panel = wx.Panel(self)         #just a dummy - gets destroyed in Draw!
2611        self.values = values
2612        self.prompts = prompts
2613        self.Draw()
2614       
2615    def Draw(self):
2616       
2617        def OnValItem(event):
2618            event.Skip()
2619            Obj = event.GetEventObject()
2620            ind = Indx[Obj.GetId()]
2621            try:
2622                val = int(Obj.GetValue())
2623                if val <= 0:
2624                    raise ValueError
2625            except ValueError:
2626                val = self.values[ind]
2627            self.values[ind] = val
2628            Obj.SetValue('%d'%(val))
2629           
2630        self.panel.Destroy()
2631        self.panel = wx.Panel(self)
2632        mainSizer = wx.BoxSizer(wx.VERTICAL)
2633        Indx = {}
2634        for ival,[prompt,value] in enumerate(zip(self.prompts,self.values)):
2635            mainSizer.Add(wx.StaticText(self.panel,-1,prompt),0,wx.ALIGN_CENTER)
2636            valItem = wx.TextCtrl(self.panel,-1,value='%d'%(value),style=wx.TE_PROCESS_ENTER)
2637            mainSizer.Add(valItem,0,wx.ALIGN_CENTER)
2638            Indx[valItem.GetId()] = ival
2639            valItem.Bind(wx.EVT_TEXT_ENTER,OnValItem)
2640            valItem.Bind(wx.EVT_KILL_FOCUS,OnValItem)
2641        OkBtn = wx.Button(self.panel,-1,"Ok")
2642        OkBtn.Bind(wx.EVT_BUTTON, self.OnOk)
2643        CancelBtn = wx.Button(self.panel,-1,'Cancel')
2644        CancelBtn.Bind(wx.EVT_BUTTON, self.OnCancel)
2645        btnSizer = wx.BoxSizer(wx.HORIZONTAL)
2646        btnSizer.Add((20,20),1)
2647        btnSizer.Add(OkBtn)
2648        btnSizer.Add(CancelBtn)
2649        btnSizer.Add((20,20),1)
2650        mainSizer.Add(btnSizer,0,wx.EXPAND|wx.BOTTOM|wx.TOP, 10)
2651        self.panel.SetSizer(mainSizer)
2652        self.panel.Fit()
2653        self.Fit()
2654
2655    def GetValues(self):
2656        return self.values
2657       
2658    def OnOk(self,event):
2659        parent = self.GetParent()
2660        parent.Raise()
2661        self.EndModal(wx.ID_OK)             
2662       
2663    def OnCancel(self,event):
2664        parent = self.GetParent()
2665        parent.Raise()
2666        self.EndModal(wx.ID_CANCEL)
2667
2668################################################################################
2669class OrderBox(wxscroll.ScrolledPanel):
2670    '''Creates a panel with scrollbars where items can be ordered into columns
2671   
2672    :param list keylist: is a list of keys for column assignments
2673    :param dict vallookup: is a dict keyed by names in keylist where each item is a dict.
2674      Each inner dict contains variable names as keys and their associated values
2675    :param dict posdict: is a dict keyed by names in keylist where each item is a dict.
2676      Each inner dict contains column numbers as keys and their associated
2677      variable name as a value. This is used for both input and output.
2678     
2679    '''
2680    def __init__(self,parent,keylist,vallookup,posdict,*arg,**kw):
2681        self.keylist = keylist
2682        self.vallookup = vallookup
2683        self.posdict = posdict
2684        self.maxcol = 0
2685        for nam in keylist:
2686            posdict = self.posdict[nam]
2687            if posdict.keys():
2688                self.maxcol = max(self.maxcol, max(posdict))
2689        wxscroll.ScrolledPanel.__init__(self,parent,wx.ID_ANY,*arg,**kw)
2690        self.GBsizer = wx.GridBagSizer(4,4)
2691        self.SetBackgroundColour(WHITE)
2692        self.SetSizer(self.GBsizer)
2693        colList = [str(i) for i in range(self.maxcol+2)]
2694        for i in range(self.maxcol+1):
2695            wid = wx.StaticText(self,wx.ID_ANY,str(i),style=wx.ALIGN_CENTER)
2696            wid.SetBackgroundColour(DULL_YELLOW)
2697            wid.SetMinSize((50,-1))
2698            self.GBsizer.Add(wid,(0,i),flag=wx.ALIGN_CENTER|wx.EXPAND)
2699        self.chceDict = {}
2700        for row,nam in enumerate(self.keylist):
2701            posdict = self.posdict[nam]
2702            for col in posdict:
2703                lbl = posdict[col]
2704                pnl = wx.Panel(self,wx.ID_ANY)
2705                pnl.SetBackgroundColour(VERY_LIGHT_GREY)
2706                insize = wx.BoxSizer(wx.VERTICAL)
2707                wid = wx.Choice(pnl,wx.ID_ANY,choices=colList)
2708                insize.Add(wid,0,wx.EXPAND|wx.BOTTOM,3)
2709                wid.SetSelection(col)
2710                self.chceDict[wid] = (row,col)
2711                wid.Bind(wx.EVT_CHOICE,self.OnChoice)
2712                wid = wx.StaticText(pnl,wx.ID_ANY,lbl)
2713                insize.Add(wid,0,flag=wx.EXPAND)
2714                try:
2715                    val = G2py3.FormatSigFigs(self.vallookup[nam][lbl],maxdigits=8)
2716                except KeyError:
2717                    val = '?'
2718                wid = wx.StaticText(pnl,wx.ID_ANY,'('+val+')')
2719                insize.Add(wid,0,flag=wx.EXPAND)
2720                pnl.SetSizer(insize)
2721                self.GBsizer.Add(pnl,(row+1,col),flag=wx.EXPAND)
2722        self.SetAutoLayout(1)
2723        self.SetupScrolling()
2724        self.SetMinSize((
2725            min(700,self.GBsizer.GetSize()[0]),
2726            self.GBsizer.GetSize()[1]+20))
2727    def OnChoice(self,event):
2728        '''Called when a column is assigned to a variable
2729        '''
2730        row,col = self.chceDict[event.EventObject] # which variable was this?
2731        newcol = event.Selection # where will it be moved?
2732        if newcol == col:
2733            return # no change: nothing to do!
2734        prevmaxcol = self.maxcol # save current table size
2735        key = self.keylist[row] # get the key for the current row
2736        lbl = self.posdict[key][col] # selected variable name
2737        lbl1 = self.posdict[key].get(col+1,'') # next variable name, if any
2738        # if a posXXX variable is selected, and the next variable is posXXX, move them together
2739        repeat = 1
2740        if lbl[:3] == 'pos' and lbl1[:3] == 'int' and lbl[3:] == lbl1[3:]:
2741            repeat = 2
2742        for i in range(repeat): # process the posXXX and then the intXXX (or a single variable)
2743            col += i
2744            newcol += i
2745            if newcol in self.posdict[key]:
2746                # find first non-blank after newcol
2747                for mtcol in range(newcol+1,self.maxcol+2):
2748                    if mtcol not in self.posdict[key]: break
2749                l1 = range(mtcol,newcol,-1)+[newcol]
2750                l = range(mtcol-1,newcol-1,-1)+[col]
2751            else:
2752                l1 = [newcol]
2753                l = [col]
2754            # move all of the items, starting from the last column
2755            for newcol,col in zip(l1,l):
2756                #print 'moving',col,'to',newcol
2757                self.posdict[key][newcol] = self.posdict[key][col]
2758                del self.posdict[key][col]
2759                self.maxcol = max(self.maxcol,newcol)
2760                obj = self.GBsizer.FindItemAtPosition((row+1,col))
2761                self.GBsizer.SetItemPosition(obj.GetWindow(),(row+1,newcol))
2762                for wid in obj.GetWindow().Children:
2763                    if wid in self.chceDict:
2764                        self.chceDict[wid] = (row,newcol)
2765                        wid.SetSelection(self.chceDict[wid][1])
2766        # has the table gotten larger? If so we need new column heading(s)
2767        if prevmaxcol != self.maxcol:
2768            for i in range(prevmaxcol+1,self.maxcol+1):
2769                wid = wx.StaticText(self,wx.ID_ANY,str(i),style=wx.ALIGN_CENTER)
2770                wid.SetBackgroundColour(DULL_YELLOW)
2771                wid.SetMinSize((50,-1))
2772                self.GBsizer.Add(wid,(0,i),flag=wx.ALIGN_CENTER|wx.EXPAND)
2773            colList = [str(i) for i in range(self.maxcol+2)]
2774            for wid in self.chceDict:
2775                wid.SetItems(colList)
2776                wid.SetSelection(self.chceDict[wid][1])
2777        self.GBsizer.Layout()
2778        self.FitInside()
2779       
2780################################################################################
2781def GetImportFile(G2frame, message, defaultDir="", defaultFile="", style=wx.FD_OPEN,
2782                  *args, **kwargs):
2783    '''Uses a customized dialog that gets files from the appropriate import directory.
2784    Arguments are used the same as in :func:`wx.FileDialog`. Selection of
2785    multiple files is allowed if argument style includes wx.FD_MULTIPLE.
2786
2787    The default initial directory (unless overridden with argument defaultDir)
2788    is found in G2frame.TutorialImportDir, config setting Import_directory or
2789    G2frame.LastImportDir, see :func:`GetImportPath`.
2790
2791    The path of the first file entered is used to set G2frame.LastImportDir
2792    and optionally config setting Import_directory.
2793
2794    :returns: a list of files or an empty list
2795    '''
2796    dlg = wx.FileDialog(G2frame, message, defaultDir, defaultFile, *args,
2797                        style=style, **kwargs)
2798    pth = GetImportPath(G2frame)
2799    if not defaultDir and pth: dlg.SetDirectory(pth)
2800    try:
2801        if dlg.ShowModal() == wx.ID_OK:
2802            if style & wx.FD_MULTIPLE:
2803                filelist = dlg.GetPaths()
2804                if len(filelist) == 0: return []
2805            else:
2806                filelist = [dlg.GetPath(),]
2807            # not sure if we want to do this (why use wx.CHANGE_DIR?)
2808            if style & wx.FD_CHANGE_DIR: # to get Mac/Linux to change directory like windows!
2809                os.chdir(dlg.GetDirectory())
2810        else: # cancel was pressed
2811            return []
2812    finally:
2813        dlg.Destroy()
2814    # save the path of the first file and reset the TutorialImportDir variable
2815    pth = os.path.split(os.path.abspath(filelist[0]))[0]
2816    if GSASIIpath.GetConfigValue('Save_paths'): SaveImportDirectory(pth)
2817    G2frame.LastImportDir = pth
2818    G2frame.TutorialImportDir = None
2819    return filelist
2820
2821def GetImportPath(G2frame):
2822    '''Determines the default location to use for importing files. Tries sequentially
2823    G2frame.TutorialImportDir, config var Import_directory and G2frame.LastImportDir.
2824   
2825    :returns: a string containing the path to be used when reading files or None
2826      if none of the above are specified.
2827    '''
2828    if G2frame.TutorialImportDir:
2829        if os.path.exists(G2frame.TutorialImportDir):
2830            return G2frame.TutorialImportDir
2831        elif GSASIIpath.GetConfigValue('debug'):
2832            print('Tutorial location (TutorialImportDir) not found: '+G2frame.TutorialImportDir)
2833    pth = GSASIIpath.GetConfigValue('Import_directory')
2834    if pth:
2835        pth = os.path.expanduser(pth)
2836        if os.path.exists(pth):
2837            return pth
2838        elif GSASIIpath.GetConfigValue('debug'):
2839            print('Ignoring Config Import_directory value: '+
2840                      GSASIIpath.GetConfigValue('Import_directory'))
2841    if G2frame.LastImportDir:
2842        if os.path.exists(G2frame.LastImportDir):
2843            return G2frame.LastImportDir
2844        elif GSASIIpath.GetConfigValue('debug'):
2845            print('Warning: G2frame.LastImportDir not found = '+G2frame.LastImportDir)
2846    return None
2847
2848def GetExportPath(G2frame):
2849    '''Determines the default location to use for writing files. Tries sequentially
2850    G2frame.LastExportDir and G2frame.LastGPXdir.
2851   
2852    :returns: a string containing the path to be used when writing files or '.'
2853      if none of the above are specified.
2854    '''
2855    if G2frame.LastExportDir:
2856        return G2frame.LastExportDir
2857    elif G2frame.LastGPXdir:
2858        return G2frame.LastGPXdir
2859    else:
2860        return '.'
2861
2862################################################################################
2863class SGMessageBox(wx.Dialog):
2864    ''' Special version of MessageBox that displays space group & super space group text
2865    in two blocks
2866    '''
2867    def __init__(self,parent,title,text,table,):
2868        wx.Dialog.__init__(self,parent,wx.ID_ANY,title,pos=wx.DefaultPosition,
2869            style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)
2870        self.text = text
2871        self.table = table
2872        self.panel = wx.Panel(self)
2873        mainSizer = wx.BoxSizer(wx.VERTICAL)
2874        mainSizer.Add((0,10))
2875        for line in text:
2876            mainSizer.Add(wx.StaticText(self.panel,label='     %s     '%(line)),0,WACV)
2877        ncol = self.table[0].count(',')+1
2878        tableSizer = wx.FlexGridSizer(0,2*ncol+3,0,0)
2879        for j,item in enumerate(self.table):
2880            num,flds = item.split(')')
2881            tableSizer.Add(wx.StaticText(self.panel,label='     %s  '%(num+')')),0,WACV|wx.ALIGN_LEFT)           
2882            flds = flds.replace(' ','').split(',')
2883            for i,fld in enumerate(flds):
2884                if i < ncol-1:
2885                    tableSizer.Add(wx.StaticText(self.panel,label='%s, '%(fld)),0,WACV|wx.ALIGN_RIGHT)
2886                else:
2887                    tableSizer.Add(wx.StaticText(self.panel,label='%s'%(fld)),0,WACV|wx.ALIGN_RIGHT)
2888            if not j%2:
2889                tableSizer.Add((20,0))
2890        mainSizer.Add(tableSizer,0,wx.ALIGN_LEFT)
2891        btnsizer = wx.StdDialogButtonSizer()
2892        OKbtn = wx.Button(self.panel, wx.ID_OK)
2893        OKbtn.Bind(wx.EVT_BUTTON, self.OnOk)
2894        OKbtn.SetDefault()
2895        btnsizer.AddButton(OKbtn)
2896        btnsizer.Realize()
2897        mainSizer.Add((0,10))
2898        mainSizer.Add(btnsizer,0,wx.ALIGN_CENTER)
2899        self.panel.SetSizer(mainSizer)
2900        self.panel.Fit()
2901        self.Fit()
2902        size = self.GetSize()
2903        self.SetSize([size[0]+20,size[1]])
2904
2905    def Show(self):
2906        '''Use this method after creating the dialog to post it
2907        '''
2908        self.ShowModal()
2909        return
2910
2911    def OnOk(self,event):
2912        parent = self.GetParent()
2913        parent.Raise()
2914        self.EndModal(wx.ID_OK)
2915
2916################################################################################
2917class DisAglDialog(wx.Dialog):
2918    '''Distance/Angle Controls input dialog. After
2919    :meth:`ShowModal` returns, the results are found in
2920    dict :attr:`self.data`, which is accessed using :meth:`GetData`.
2921
2922    :param wx.Frame parent: reference to parent frame (or None)
2923    :param dict data: a dict containing the current
2924      search ranges or an empty dict, which causes default values
2925      to be used.
2926      Will be used to set element `DisAglCtls` in
2927      :ref:`Phase Tree Item <Phase_table>`
2928    :param dict default:  A dict containing the default
2929      search ranges for each element.
2930    :param bool Reset: if True (default), show Reset button
2931    :param bool Angle: if True (default), show angle radii
2932    '''
2933    def __init__(self,parent,data,default,Reset=True,Angle=True):
2934        text = 'Distance Angle Controls'
2935        if not Angle:
2936            text = 'Distance Controls'
2937        wx.Dialog.__init__(self,parent,wx.ID_ANY,text, 
2938            pos=wx.DefaultPosition,style=wx.DEFAULT_DIALOG_STYLE)
2939        self.default = default
2940        self.Reset = Reset
2941        self.Angle = Angle
2942        self.panel = None
2943        self._default(data,self.default)
2944        self.Draw(self.data)
2945               
2946    def _default(self,data,default):
2947        '''Set starting values for the search values, either from
2948        the input array or from defaults, if input is null
2949        '''
2950        if data:
2951            self.data = copy.deepcopy(data) # don't mess with originals
2952        else:
2953            self.data = {}
2954            self.data['Name'] = default['Name']
2955            self.data['Factors'] = [0.85,0.85]
2956            self.data['AtomTypes'] = default['AtomTypes']
2957            self.data['BondRadii'] = default['BondRadii'][:]
2958            self.data['AngleRadii'] = default['AngleRadii'][:]
2959
2960    def Draw(self,data):
2961        '''Creates the contents of the dialog. Normally called
2962        by :meth:`__init__`.
2963        '''
2964        if self.panel: self.panel.Destroy()
2965        self.panel = wx.Panel(self)
2966        mainSizer = wx.BoxSizer(wx.VERTICAL)
2967        mainSizer.Add(wx.StaticText(self.panel,-1,'Controls for phase '+data['Name']),
2968            0,WACV|wx.LEFT,10)
2969        mainSizer.Add((10,10),1)
2970       
2971        ncol = 3
2972        if not self.Angle:
2973            ncol=2
2974        radiiSizer = wx.FlexGridSizer(0,ncol,5,5)
2975        radiiSizer.Add(wx.StaticText(self.panel,-1,' Type'),0,WACV)
2976        radiiSizer.Add(wx.StaticText(self.panel,-1,'Bond radii'),0,WACV)
2977        if self.Angle:
2978            radiiSizer.Add(wx.StaticText(self.panel,-1,'Angle radii'),0,WACV)
2979        self.objList = {}
2980        for id,item in enumerate(self.data['AtomTypes']):
2981            radiiSizer.Add(wx.StaticText(self.panel,-1,' '+item),0,WACV)
2982            bRadii = ValidatedTxtCtrl(self.panel,data['BondRadii'],id,nDig=(10,3))
2983            radiiSizer.Add(bRadii,0,WACV)
2984            if self.Angle:
2985                aRadii = ValidatedTxtCtrl(self.panel,data['AngleRadii'],id,nDig=(10,3))
2986                radiiSizer.Add(aRadii,0,WACV)
2987        mainSizer.Add(radiiSizer,0,wx.EXPAND)
2988        if self.Angle:
2989            factorSizer = wx.FlexGridSizer(0,2,5,5)
2990            Names = ['Bond','Angle']
2991            for i,name in enumerate(Names):
2992                factorSizer.Add(wx.StaticText(self.panel,-1,name+' search factor'),0,WACV)
2993                bondFact = ValidatedTxtCtrl(self.panel,data['Factors'],i,nDig=(10,3))
2994                factorSizer.Add(bondFact)
2995            mainSizer.Add(factorSizer,0,wx.EXPAND)
2996       
2997        OkBtn = wx.Button(self.panel,-1,"Ok")
2998        OkBtn.Bind(wx.EVT_BUTTON, self.OnOk)
2999        btnSizer = wx.BoxSizer(wx.HORIZONTAL)
3000        btnSizer.Add((20,20),1)
3001        btnSizer.Add(OkBtn)
3002        if self.Reset:
3003            ResetBtn = wx.Button(self.panel,-1,'Reset')
3004            ResetBtn.Bind(wx.EVT_BUTTON, self.OnReset)
3005            btnSizer.Add(ResetBtn)
3006        btnSizer.Add((20,20),1)
3007        mainSizer.Add(btnSizer,0,wx.EXPAND|wx.BOTTOM|wx.TOP, 10)
3008        self.panel.SetSizer(mainSizer)
3009        self.panel.Fit()
3010        self.Fit()
3011   
3012    def GetData(self):
3013        'Returns the values from the dialog'
3014        return self.data
3015       
3016    def OnOk(self,event):
3017        'Called when the OK button is pressed'
3018        parent = self.GetParent()
3019        parent.Raise()
3020        self.EndModal(wx.ID_OK)             
3021       
3022    def OnReset(self,event):
3023        'Called when the Reset button is pressed'
3024        data = {}
3025        self._default(data,self.default)
3026        wx.CallAfter(self.Draw,self.data)
3027               
3028################################################################################
3029class ShowLSParms(wx.Dialog):
3030    '''Create frame to show least-squares parameters
3031    '''
3032    def __init__(self,parent,title,parmDict,varyList,fullVaryList,
3033                 size=(375,430)):
3034       
3035        wx.Dialog.__init__(self,parent,wx.ID_ANY,title,size=size,
3036                           style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)
3037        self.panel = wxscroll.ScrolledPanel(self)
3038        self.parmChoice = 'Phase'
3039        self.parmDict = parmDict
3040        self.varyList = varyList
3041        self.fullVaryList = fullVaryList
3042
3043        self.parmNames = list(parmDict.keys())
3044        self.parmNames.sort()
3045        splitNames = [item.split(':') for item in self.parmNames if len(item) > 3 and not isinstance(self.parmDict[item],basestring)]
3046        self.globNames = [':'.join(item) for item in splitNames if not item[0] and not item[1]]
3047        self.globVars = list(set([' ',]+[item[2] for item in splitNames if not item[0] and not item[1]]))
3048        self.globVars.sort()
3049        self.hisNames = [':'.join(item) for item in splitNames if not item[0] and item[1]]
3050        self.hisNums = list(set([int(item.split(':')[1]) for item in self.hisNames]))
3051        self.hisNums.sort()
3052        self.hisNums = [' ',]+[str(item) for item in self.hisNums]
3053        self.hisVars = list(set([' ',]+[item[2] for item in splitNames if not item[0]]))
3054        self.hisVars.sort()
3055        self.phasNames = [':'.join(item) for item in splitNames if not item[1] and 'is' not in item[2]]
3056        self.phasNums = [' ',]+list(set([item.split(':')[0] for item in self.phasNames]))
3057        if '' in self.phasNums: self.phasNums.remove('')
3058        self.phasVars = list(set([' ',]+[item[2] for item in splitNames if not item[1] and 'is' not in item[2]]))
3059        self.phasVars.sort()
3060        self.phasNums.sort()
3061        self.hapNames = [':'.join(item) for item in splitNames if item[0] and item[1]]
3062        self.hapVars = list(set([' ',]+[item[2] for item in splitNames if item[0] and item[1]]))
3063        self.hapVars.sort()
3064        self.hisNum = ' '
3065        self.phasNum = ' '
3066        self.varName = ' '
3067        self.listSel = 'Refined'
3068        self.DrawPanel()
3069       
3070           
3071    def DrawPanel(self):
3072           
3073        def _OnParmSel(event):
3074            self.parmChoice = parmSel.GetStringSelection()
3075            self.varName = ' '
3076            wx.CallLater(100,self.DrawPanel)
3077           
3078        def OnPhasSel(event):
3079            event.Skip()
3080            self.phasNum = phasSel.GetValue()
3081            self.varName = ' '
3082            wx.CallLater(100,self.DrawPanel)
3083
3084        def OnHistSel(event):
3085            event.Skip()
3086            self.hisNum = histSel.GetValue()
3087            self.varName = ' '
3088            wx.CallLater(100,self.DrawPanel)
3089           
3090        def OnVarSel(event):
3091            self.varName = varSel.GetValue()
3092            self.phasNum = ' '
3093            self.hisNum = ' '
3094            wx.CallLater(100,self.DrawPanel)
3095           
3096        def OnListSel(event):
3097            self.listSel = listSel.GetStringSelection()
3098            wx.CallLater(100,self.DrawPanel)
3099
3100        if self.panel.GetSizer(): self.panel.GetSizer().Clear(True)
3101        mainSizer = wx.BoxSizer(wx.VERTICAL)
3102        num = len(self.varyList)
3103        mainSizer.Add(wx.StaticText(self.panel,label=' Number of refined variables: '+str(num)),0)
3104        if len(self.varyList) != len(self.fullVaryList):
3105            num = len(self.fullVaryList) - len(self.varyList)
3106            mainSizer.Add(wx.StaticText(self.panel,label=' + '+str(num)+' parameters are varied via constraints'))
3107        choiceDict = {'Global':self.globNames,'Phase':self.phasNames,'Phase/Histo':self.hapNames,'Histogram':self.hisNames}
3108        choice = ['Phase','Phase/Histo','Histogram']
3109        if len(self.globNames):
3110            choice += ['Global',]
3111        parmSizer = wx.FlexGridSizer(0,3,5,5)
3112        parmSel = wx.RadioBox(self.panel,wx.ID_ANY,'Parameter type:',choices=choice,
3113            majorDimension=1,style=wx.RA_SPECIFY_COLS)
3114        parmSel.Bind(wx.EVT_RADIOBOX,_OnParmSel)
3115        parmSel.SetStringSelection(self.parmChoice)
3116        parmSizer.Add(parmSel,0)
3117        numSizer = wx.BoxSizer(wx.VERTICAL)
3118        numSizer.Add((5,25),0)
3119        if self.parmChoice in ['Phase','Phase/Histo'] and len(self.phasNums) > 1:
3120            numSizer.Add(wx.StaticText(self.panel,label='Phase'),0)
3121            phasSel = wx.ComboBox(self.panel,choices=self.phasNums,value=self.phasNum,
3122                style=wx.CB_READONLY|wx.CB_DROPDOWN)
3123            phasSel.Bind(wx.EVT_COMBOBOX,OnPhasSel)
3124            numSizer.Add(phasSel,0)
3125        if self.parmChoice in ['Histogram','Phase/Histo'] and len(self.hisNums) > 1:
3126            numSizer.Add(wx.StaticText(self.panel,label='Histogram'),0)
3127            histSel = wx.ComboBox(self.panel,choices=self.hisNums,value=self.hisNum,
3128                style=wx.CB_READONLY|wx.CB_DROPDOWN)
3129            histSel.Bind(wx.EVT_COMBOBOX,OnHistSel)
3130#            histSel = wx.TextCtrl(self.panel,size=(50,25),value='0',style=wx.TE_PROCESS_ENTER)
3131#            histSel.Bind(wx.EVT_TEXT_ENTER,OnHistSel)
3132#            histSel.Bind(wx.EVT_KILL_FOCUS,OnHistSel)
3133            numSizer.Add(histSel,0)
3134        parmSizer.Add(numSizer)
3135        varSizer = wx.BoxSizer(wx.VERTICAL)
3136        if self.parmChoice in ['Phase',]:
3137            varSel = wx.ComboBox(self.panel,choices=self.phasVars,value=self.varName,
3138                style=wx.CB_READONLY|wx.CB_DROPDOWN)
3139            varSel.Bind(wx.EVT_COMBOBOX,OnVarSel)
3140        elif self.parmChoice in ['Histogram',]:
3141            varSel = wx.ComboBox(self.panel,choices=self.hisVars,value=self.varName,
3142                style=wx.CB_READONLY|wx.CB_DROPDOWN)
3143            varSel.Bind(wx.EVT_COMBOBOX,OnVarSel)
3144        elif self.parmChoice in ['Phase/Histo',]:
3145            varSel = wx.ComboBox(self.panel,choices=self.hapVars,value=self.varName,
3146                style=wx.CB_READONLY|wx.CB_DROPDOWN)
3147            varSel.Bind(wx.EVT_COMBOBOX,OnVarSel)
3148        if self.parmChoice != 'Global': 
3149            varSizer.Add(wx.StaticText(self.panel,label='Parameter'))
3150            varSizer.Add(varSel,0)
3151        parmSizer.Add(varSizer,0)
3152        mainSizer.Add(parmSizer,0)
3153        listChoice = ['All','Refined']
3154        listSel = wx.RadioBox(self.panel,wx.ID_ANY,'Parameter type:',choices=listChoice,
3155            majorDimension=0,style=wx.RA_SPECIFY_COLS)
3156        listSel.SetStringSelection(self.listSel)
3157        listSel.Bind(wx.EVT_RADIOBOX,OnListSel)
3158        mainSizer.Add(listSel,0)
3159        subSizer = wx.FlexGridSizer(cols=4,hgap=2,vgap=2)
3160        subSizer.Add((-1,-1))
3161        subSizer.Add(wx.StaticText(self.panel,wx.ID_ANY,'Parameter name  '))
3162        subSizer.Add(wx.StaticText(self.panel,wx.ID_ANY,'refine?'))
3163        subSizer.Add(wx.StaticText(self.panel,wx.ID_ANY,'value'),0,wx.ALIGN_RIGHT)
3164        explainRefine = False
3165        for name in choiceDict[self.parmChoice]:
3166            # skip entries without numerical values
3167            if isinstance(self.parmDict[name],basestring): continue
3168            if 'Refined' in self.listSel and (name not in self.fullVaryList
3169                                              ) and (name not in self.varyList):
3170                continue
3171            if 'Phase' in self.parmChoice:
3172                if self.phasNum != ' ' and name.split(':')[0] != self.phasNum: continue
3173            if 'Histo' in self.parmChoice:
3174                if self.hisNum != ' ' and name.split(':')[1] != self.hisNum: continue
3175            if (self.varName != ' ') and (self.varName not in name): continue
3176            try:
3177                value = G2py3.FormatSigFigs(self.parmDict[name])
3178            except TypeError:
3179                value = str(self.parmDict[name])+' -?' # unexpected
3180                #continue
3181            v = G2obj.getVarDescr(name)
3182            if v is None or v[-1] is None:
3183                subSizer.Add((-1,-1))
3184            else:               
3185                ch = HelpButton(self.panel,G2obj.fmtVarDescr(name))
3186                subSizer.Add(ch,0,wx.LEFT|wx.RIGHT|WACV|wx.ALIGN_CENTER,1)
3187            subSizer.Add(wx.StaticText(self.panel,wx.ID_ANY,str(name)))
3188            if name in self.varyList:
3189                subSizer.Add(wx.StaticText(self.panel,label='R'))   #TODO? maybe a checkbox for one stop refinemnt flag setting?
3190            elif name in self.fullVaryList:
3191                subSizer.Add(wx.StaticText(self.panel,label='C'))
3192                explainRefine = True
3193            else:
3194                subSizer.Add((-1,-1))
3195            subSizer.Add(wx.StaticText(self.panel,label=value),0,wx.ALIGN_RIGHT)
3196
3197        mainSizer.Add(subSizer,0)
3198        if explainRefine:
3199            mainSizer.Add(
3200                wx.StaticText(self.panel,label='"R" indicates a refined variable\n'+
3201                    '"C" indicates generated from a constraint'),0, wx.ALL,0)
3202        # make OK button
3203        btnsizer = wx.BoxSizer(wx.HORIZONTAL)
3204        btn = wx.Button(self.panel, wx.ID_CLOSE,"Close") 
3205        btn.Bind(wx.EVT_BUTTON,self._onClose)
3206        btnsizer.Add(btn)
3207        mainSizer.Add(btnsizer, 0, wx.ALIGN_CENTER|wx.ALL, 5)
3208        # Allow window to be enlarged but not made smaller
3209        self.panel.SetSizer(mainSizer)
3210        self.panel.SetAutoLayout(1)
3211        self.panel.SetupScrolling()
3212        self.panel.SetMinSize(self.GetSize())
3213
3214    def _onClose(self,event):
3215        self.EndModal(wx.ID_CANCEL)
3216
3217################################################################################
3218#####  Customized Grid Support
3219################################################################################           
3220class GSGrid(wg.Grid):
3221    '''Basic wx.Grid implementation
3222    '''
3223    def __init__(self, parent, name=''):
3224        wg.Grid.__init__(self,parent,-1,name=name)
3225        if hasattr(parent.TopLevelParent,'currentGrids'):
3226            parent.TopLevelParent.currentGrids.append(self)      # save a reference to the grid in the Frame
3227        self.SetScrollRate(0,0)         #GSAS-II grids have no scroll bars by default
3228           
3229    def Clear(self):
3230        wg.Grid.ClearGrid(self)
3231       
3232    def SetCellReadOnly(self,r,c,readonly=True):
3233        self.SetReadOnly(r,c,isReadOnly=readonly)
3234       
3235    def SetCellStyle(self,r,c,color="white",readonly=True):
3236        self.SetCellBackgroundColour(r,c,color)
3237        self.SetReadOnly(r,c,isReadOnly=readonly)
3238       
3239    def GetSelection(self):
3240        #this is to satisfy structure drawing stuff in G2plt when focus changes
3241        return None
3242
3243    def InstallGridToolTip(self, rowcolhintcallback,
3244                           colLblCallback=None,rowLblCallback=None):
3245        '''code to display a tooltip for each item on a grid
3246        from http://wiki.wxpython.org/wxGrid%20ToolTips (buggy!), expanded to
3247        column and row labels using hints from
3248        https://groups.google.com/forum/#!topic/wxPython-users/bm8OARRVDCs
3249
3250        :param function rowcolhintcallback: a routine that returns a text
3251          string depending on the selected row and column, to be used in
3252          explaining grid entries.
3253        :param function colLblCallback: a routine that returns a text
3254          string depending on the selected column, to be used in
3255          explaining grid columns (if None, the default), column labels
3256          do not get a tooltip.
3257        :param function rowLblCallback: a routine that returns a text
3258          string depending on the selected row, to be used in
3259          explaining grid rows (if None, the default), row labels
3260          do not get a tooltip.
3261        '''
3262        prev_rowcol = [None,None,None]
3263        def OnMouseMotion(event):
3264            # event.GetRow() and event.GetCol() would be nice to have here,
3265            # but as this is a mouse event, not a grid event, they are not
3266            # available and we need to compute them by hand.
3267            x, y = self.CalcUnscrolledPosition(event.GetPosition())
3268            row = self.YToRow(y)
3269            col = self.XToCol(x)
3270            hinttext = ''
3271            win = event.GetEventObject()
3272            if [row,col,win] == prev_rowcol: # no change from last position
3273                if event: event.Skip()
3274                return
3275            if win == self.GetGridWindow() and row >= 0 and col >= 0:
3276                hinttext = rowcolhintcallback(row, col)
3277            elif win == self.GetGridColLabelWindow() and col >= 0:
3278                if colLblCallback: hinttext = colLblCallback(col)
3279            elif win == self.GetGridRowLabelWindow() and row >= 0:
3280                if rowLblCallback: hinttext = rowLblCallback(row)
3281            else: # this should be the upper left corner, which is empty
3282                if event: event.Skip()
3283                return
3284            if hinttext is None: hinttext = ''
3285            win.SetToolTipString(hinttext)
3286            prev_rowcol[:] = [row,col,win]
3287            if event: event.Skip()
3288
3289        wx.EVT_MOTION(self.GetGridWindow(), OnMouseMotion)
3290        if colLblCallback: wx.EVT_MOTION(self.GetGridColLabelWindow(), OnMouseMotion)
3291        if rowLblCallback: wx.EVT_MOTION(self.GetGridRowLabelWindow(), OnMouseMotion)
3292                                                   
3293################################################################################           
3294class Table(wg.PyGridTableBase):
3295    '''Basic data table for use with GSgrid
3296    '''
3297    def __init__(self, data=[], rowLabels=None, colLabels=None, types = None):
3298        if 'phoenix' in wx.version():
3299            wg.GridTableBase.__init__(self)
3300        else:
3301            wg.PyGridTableBase.__init__(self)
3302        self.colLabels = colLabels
3303        self.rowLabels = rowLabels
3304        self.dataTypes = types
3305        self.data = data
3306       
3307    def AppendRows(self, numRows=1):
3308        self.data.append([])
3309        return True
3310       
3311    def CanGetValueAs(self, row, col, typeName):
3312        if self.dataTypes:
3313            colType = self.dataTypes[col].split(':')[0]
3314            if typeName == colType:
3315                return True
3316            else:
3317                return False
3318        else:
3319            return False
3320
3321    def CanSetValueAs(self, row, col, typeName):
3322        return self.CanGetValueAs(row, col, typeName)
3323
3324    def DeleteRow(self,pos):
3325        data = self.GetData()
3326        self.SetData([])
3327        new = []
3328        for irow,row in enumerate(data):
3329            if irow != pos:
3330                new.append(row)
3331        self.SetData(new)
3332       
3333    def GetColLabelValue(self, col):
3334        if self.colLabels:
3335            return self.colLabels[col]
3336           
3337    def GetData(self):
3338        data = []
3339        for row in range(self.GetNumberRows()):
3340            data.append(self.GetRowValues(row))
3341        return data
3342       
3343    def GetNumberCols(self):
3344        try:
3345            return len(self.colLabels)
3346        except TypeError:
3347            return None
3348       
3349    def GetNumberRows(self):
3350        return len(self.data)
3351       
3352    def GetRowLabelValue(self, row):
3353        if self.rowLabels:
3354            return self.rowLabels[row]
3355       
3356    def GetColValues(self, col):
3357        data = []
3358        for row in range(self.GetNumberRows()):
3359            data.append(self.GetValue(row, col))
3360        return data
3361       
3362    def GetRowValues(self, row):
3363        data = []
3364        for col in range(self.GetNumberCols()):
3365            data.append(self.GetValue(row, col))
3366        return data
3367       
3368    def GetTypeName(self, row, col):
3369        try:
3370            if self.data[row][col] is None: return None
3371            return self.dataTypes[col]
3372        except (TypeError,IndexError):
3373            return None
3374
3375    def GetValue(self, row, col):
3376        try:
3377            if self.data[row][col] is None: return ""
3378            return self.data[row][col]
3379        except IndexError:
3380            return None
3381           
3382    def InsertRows(self, pos, rows):
3383        for row in range(rows):
3384            self.data.insert(pos,[])
3385            pos += 1
3386       
3387    def IsEmptyCell(self,row,col):
3388        try:
3389            return not self.data[row][col]
3390        except IndexError:
3391            return True
3392       
3393    def OnKeyPress(self, event):
3394        dellist = self.GetSelectedRows()
3395        if event.GetKeyCode() == wx.WXK_DELETE and dellist:
3396            grid = self.GetView()
3397            for i in dellist: grid.DeleteRow(i)
3398               
3399    def SetColLabelValue(self, col, label):
3400        numcols = self.GetNumberCols()
3401        if col > numcols-1:
3402            self.colLabels.append(label)
3403        else:
3404            self.colLabels[col]=label
3405       
3406    def SetData(self,data):
3407        for row in range(len(data)):
3408            self.SetRowValues(row,data[row])
3409               
3410    def SetRowLabelValue(self, row, label):
3411        self.rowLabels[row]=label
3412           
3413    def SetRowValues(self,row,data):
3414        self.data[row] = data
3415           
3416    def SetValue(self, row, col, value):
3417        def innerSetValue(row, col, value):
3418            try:
3419                self.data[row][col] = value
3420            except TypeError:
3421                return
3422            except IndexError: # has this been tested?
3423                #print row,col,value
3424                # add a new row
3425                if row > self.GetNumberRows():
3426                    self.data.append([''] * self.GetNumberCols())
3427                elif col > self.GetNumberCols():
3428                    for row in range(self.GetNumberRows()): # bug fixed here
3429                        self.data[row].append('')
3430                #print self.data
3431                self.data[row][col] = value
3432        innerSetValue(row, col, value)
3433
3434################################################################################
3435class GridFractionEditor(wg.PyGridCellEditor):
3436    '''A grid cell editor class that allows entry of values as fractions as well
3437    as sine and cosine values [as s() and c()]
3438    '''
3439    def __init__(self,grid):
3440        if 'phoenix' in wx.version():
3441            wg.GridCellEditor.__init__(self)
3442        else:
3443            wg.PyGridCellEditor.__init__(self)
3444
3445    def Create(self, parent, id, evtHandler):
3446        self._tc = wx.TextCtrl(parent, id, "")
3447        self._tc.SetInsertionPoint(0)
3448        self.SetControl(self._tc)
3449
3450        if evtHandler:
3451            self._tc.PushEventHandler(evtHandler)
3452
3453        self._tc.Bind(wx.EVT_CHAR, self.OnChar)
3454
3455    def SetSize(self, rect):
3456        self._tc.SetDimensions(rect.x, rect.y, rect.width+2, rect.height+2,
3457                               wx.SIZE_ALLOW_MINUS_ONE)
3458
3459    def BeginEdit(self, row, col, grid):
3460        self.startValue = grid.GetTable().GetValue(row, col)
3461        self._tc.SetValue(str(self.startValue))
3462        self._tc.SetInsertionPointEnd()
3463        self._tc.SetFocus()
3464        self._tc.SetSelection(0, self._tc.GetLastPosition())
3465
3466    def EndEdit(self, row, col, grid, oldVal=None):
3467        changed = False
3468
3469        self.nextval = self.startValue
3470        val = self._tc.GetValue().lower().strip()
3471        if val != self.startValue:
3472            changed = True
3473            neg = False
3474            if val.startswith('-'):
3475                neg = True
3476                val = val[1:]
3477            # allow old GSAS s20 and c20 etc for sind(20) and cosd(20)
3478            if val.startswith('s') and '(' not in val:
3479                val = 'sind('+val.strip('s')+')'
3480            elif val.startswith('c') and '(' not in val:
3481                val = 'cosd('+val.strip('c')+')'
3482            if neg:
3483                val = '-' + val
3484            val = G2py3.FormulaEval(val)
3485            if val is not None:
3486                self.nextval = val
3487            else:
3488                return None
3489            if oldVal is None: # this arg appears in 2.9+; before, we should go ahead & change the table
3490                grid.GetTable().SetValue(row, col, val) # update the table
3491            # otherwise self.ApplyEdit gets called
3492
3493        self.startValue = ''
3494        self._tc.SetValue('')
3495        return changed
3496   
3497    def ApplyEdit(self, row, col, grid):
3498        """ Called only in wx >= 2.9
3499        Save the value of the control into the grid if EndEdit() returns as True
3500        """
3501        grid.GetTable().SetValue(row, col, self.nextval) # update the table
3502
3503    def Reset(self):
3504        self._tc.SetValue(self.startValue)
3505        self._tc.SetInsertionPointEnd()
3506
3507    def Clone(self,grid):
3508        return GridFractionEditor(grid)
3509
3510    def StartingKey(self, evt):
3511        self.OnChar(evt)
3512        if evt.GetSkipped():
3513            self._tc.EmulateKeyPress(evt)
3514
3515    def OnChar(self, evt):
3516        key = evt.GetKeyCode()
3517        if key < 32 or key >= 127:
3518            evt.Skip()
3519        elif chr(key).lower() in '.+-*/0123456789cosind()':
3520            evt.Skip()
3521        else:
3522            evt.StopPropagation()
3523           
3524################################################################################
3525#####  Customized Notebook
3526################################################################################           
3527class GSNoteBook(wx.aui.AuiNotebook):
3528    '''Notebook used in various locations; implemented with wx.aui extension
3529    '''
3530    def __init__(self, parent, name='',size = None,style=wx.aui.AUI_NB_TOP |
3531        wx.aui.AUI_NB_SCROLL_BUTTONS):
3532        wx.aui.AuiNotebook.__init__(self, parent, style=style)
3533        if size: self.SetSize(size)
3534        self.parent = parent
3535        self.PageChangeHandler = None
3536       
3537    def PageChangeEvent(self,event):
3538        pass
3539#        G2frame = self.parent.G2frame
3540#        page = event.GetSelection()
3541#        if self.PageChangeHandler:
3542#            if log.LogInfo['Logging']:
3543#                log.MakeTabLog(
3544#                    G2frame.dataWindow.GetTitle(),
3545#                    G2frame.dataDisplay.GetPageText(page)
3546#                    )
3547#            self.PageChangeHandler(event)
3548           
3549#    def Bind(self,eventtype,handler,*args,**kwargs):
3550#        '''Override the Bind() function so that page change events can be trapped
3551#        '''
3552#        if eventtype == wx.aui.EVT_AUINOTEBOOK_PAGE_CHANGED:
3553#            self.PageChangeHandler = handler
3554#            wx.aui.AuiNotebook.Bind(self,eventtype,self.PageChangeEvent)
3555#            return
3556#        wx.aui.AuiNotebook.Bind(self,eventtype,handler,*args,**kwargs)
3557                                                     
3558    def Clear(self):       
3559        GSNoteBook.DeleteAllPages(self)
3560       
3561    def FindPage(self,name):
3562        numPage = self.GetPageCount()
3563        for page in range(numPage):
3564            if self.GetPageText(page) == name:
3565                return page
3566        return None
3567
3568    def ChangeSelection(self,page):
3569        # in wx.Notebook ChangeSelection is like SetSelection, but it
3570        # does not invoke the event related to pressing the tab button
3571        # I don't see a way to do that in aui.
3572        oldPage = self.GetSelection()
3573        self.SetSelection(page)
3574        return oldPage
3575
3576    # def __getattribute__(self,name):
3577    #     '''This method provides a way to print out a message every time
3578    #     that a method in a class is called -- to see what all the calls
3579    #     might be, or where they might be coming from.
3580    #     Cute trick for debugging!
3581    #     '''
3582    #     attr = object.__getattribute__(self, name)
3583    #     if hasattr(attr, '__call__'):
3584    #         def newfunc(*args, **kwargs):
3585    #             print('GSauiNoteBook calling %s' %attr.__name__)
3586    #             result = attr(*args, **kwargs)
3587    #             return result
3588    #         return newfunc
3589    #     else:
3590    #         return attr
3591           
3592################################################################################
3593#### Help support routines
3594################################################################################
3595class MyHelp(wx.Menu):
3596    '''
3597    A class that creates the contents of a help menu.
3598    The menu will start with two entries:
3599
3600    * 'Help on <helpType>': where helpType is a reference to an HTML page to
3601      be opened
3602    * About: opens an About dialog using OnHelpAbout. N.B. on the Mac this
3603      gets moved to the App menu to be consistent with Apple style.
3604
3605    NOTE: for this to work properly with respect to system menus, the title
3606    for the menu must be &Help, or it will not be processed properly:
3607
3608    ::
3609
3610       menu.Append(menu=MyHelp(self,...),title="&Help")
3611
3612    '''
3613    def __init__(self,frame,includeTree=False,morehelpitems=[]):
3614        wx.Menu.__init__(self,'')
3615        self.HelpById = {}
3616        self.frame = frame
3617        self.Append(wx.ID_ABOUT,'&About GSAS-II','')
3618        frame.Bind(wx.EVT_MENU, self.OnHelpAbout, id=wx.ID_ABOUT)
3619        if GSASIIpath.whichsvn():
3620            helpobj = self.Append(wx.ID_ANY,'&Check for updates','')
3621            frame.Bind(wx.EVT_MENU, self.OnCheckUpdates, helpobj)
3622            helpobj = self.Append(wx.ID_ANY,'&Regress to an old GSAS-II version','')
3623            frame.Bind(wx.EVT_MENU, self.OnSelectVersion, helpobj)
3624            # if GSASIIpath.svnTestBranch():
3625            #     msg = "&Switch back to standard GSAS-II version"
3626            # else:
3627            #     msg = "&Switch to test (2frame) GSAS-II version"
3628            # helpobj = self.Append(
3629            #     help='', id=wx.ID_ANY, kind=wx.ITEM_NORMAL,text=msg)
3630            # frame.Bind(wx.EVT_MENU, self.OnSelectBranch, helpobj)
3631        # provide special help topic names for extra items in help menu
3632        for lbl,indx in morehelpitems:
3633            helpobj = self.Append(wx.ID_ANY,lbl,'')
3634            frame.Bind(wx.EVT_MENU, self.OnHelpById, helpobj)
3635            self.HelpById[helpobj.GetId()] = indx
3636        # add help lookup(s) in gsasii.html
3637        self.AppendSeparator()
3638        if includeTree:
3639            helpobj = self.Append(wx.ID_ANY,'Help on Data tree','')
3640            frame.Bind(wx.EVT_MENU, self.OnHelpById, id=helpobj.GetId())
3641            self.HelpById[helpobj.GetId()] = 'Data tree'
3642        helpobj = self.Append(wx.ID_ANY,'Help on current data tree item','')
3643        frame.Bind(wx.EVT_MENU, self.OnHelpById, id=helpobj.GetId())
3644       
3645    def OnHelpById(self,event):
3646        '''Called when Help on... is pressed in a menu. Brings up a web page
3647        for documentation. Uses the helpKey value from the dataWindow window
3648        unless a special help key value has been defined for this menu id in
3649        self.HelpById
3650
3651        Note that self should now (2frame) be child of the main window (G2frame)
3652        '''
3653        if hasattr(self.frame,'dataWindow'):  # Debug code: check this is called from menu in G2frame
3654            # should always be true in 2 Frame version
3655            dW = self.frame.dataWindow
3656        else:
3657            print('help error: not called from standard menu?')
3658            print (self)
3659            return           
3660        try:
3661            helpKey = dW.helpKey # look up help from helpKey in data window
3662            #if GSASIIpath.GetConfigValue('debug'): print 'dataWindow help: key=',helpKey
3663        except AttributeError:
3664            helpKey = ''
3665            if GSASIIpath.GetConfigValue('debug'): print('No helpKey for current dataWindow!')
3666        helpType = self.HelpById.get(event.GetId(),helpKey) # see if there is a special help topic
3667        #if GSASIIpath.GetConfigValue('debug'): print 'helpKey=',helpKey,'  helpType=',helpType
3668        if helpType == 'Tutorials':
3669            dlg = OpenTutorial(self.frame)
3670            dlg.ShowModal()
3671            dlg.Destroy()
3672            return
3673        else:
3674            ShowHelp(helpType,self.frame)
3675
3676    def OnHelpAbout(self, event):
3677        "Display an 'About GSAS-II' box"
3678        import GSASII
3679        try:
3680            import wx.adv as wxadv  # AboutBox moved here in Phoenix
3681        except:
3682            wxadv = wx
3683        info = wxadv.AboutDialogInfo()
3684        info.Name = 'GSAS-II'
3685        ver = GSASIIpath.svnGetRev()
3686        if not ver:
3687            ver = GSASIIpath.GetVersionNumber()
3688        info.SetVersion(ver)
3689        #info.Developers = ['Robert B. Von Dreele','Brian H. Toby']
3690        info.Copyright = ('(c) ' + time.strftime('%Y') +
3691''' Argonne National Laboratory
3692This product includes software developed
3693by the UChicago Argonne, LLC, as
3694Operator of Argonne National Laboratory.''')
3695        info.Description = '''General Structure Analysis System-II (GSAS-II)
3696Robert B. Von Dreele and Brian H. Toby
3697
3698Please cite as:
3699  B.H. Toby & R.B. Von Dreele, J. Appl. Cryst. 46, 544-549 (2013)
3700For small angle use cite:
3701  R.B. Von Dreele, J. Appl. Cryst. 47, 1748-9 (2014)
3702For DIFFaX use cite:
3703  M.M.J. Treacy, J.M. Newsam & M.W. Deem,
3704  Proc. Roy. Soc. Lond. A 433, 499-520 (1991)
3705'''
3706        info.WebSite = ("https://subversion.xray.aps.anl.gov/trac/pyGSAS","GSAS-II home page")
3707        wxadv.AboutBox(info)
3708
3709    def OnCheckUpdates(self,event):
3710        '''Check if the GSAS-II repository has an update for the current source files
3711        and perform that update if requested.
3712        '''           
3713        if not GSASIIpath.whichsvn():
3714            dlg = wx.MessageDialog(self.frame,
3715                                   'No Subversion','Cannot update GSAS-II because subversion (svn) was not found.',
3716                                   wx.OK)
3717            dlg.ShowModal()
3718            dlg.Destroy()
3719            return
3720        wx.BeginBusyCursor()
3721        local = GSASIIpath.svnGetRev()
3722        if local is None: 
3723            wx.EndBusyCursor()
3724            dlg = wx.MessageDialog(self.frame,
3725                                   'Unable to run subversion on the GSAS-II current directory. Is GSAS-II installed correctly?',
3726                                   'Subversion error',
3727                                   wx.OK)
3728            dlg.ShowModal()
3729            dlg.Destroy()
3730            return
3731        print ('Installed GSAS-II version: '+local)
3732        repos = GSASIIpath.svnGetRev(local=False)
3733        wx.EndBusyCursor()
3734        # has the current branch disappeared? If so, switch to the trunk -- not fully tested
3735        if (repos is None and "not found" in GSASIIpath.svnLastError.lower()
3736            and "path" in GSASIIpath.svnLastError.lower()):
3737            print('Repository is gone, will switch to trunk')
3738            GSASIIpath.svnSwitch2branch()
3739            return
3740        elif repos is None: 
3741            dlg = wx.MessageDialog(self.frame,
3742                                   'Unable to access the GSAS-II server. Is this computer on the internet?',
3743                                   'Server unavailable',
3744                                   wx.OK)
3745            dlg.ShowModal()
3746            dlg.Destroy()
3747            return
3748        print ('GSAS-II version on server: '+repos)
3749        if local == repos:
3750            dlg = wx.MessageDialog(self.frame,
3751                                   'GSAS-II is up-to-date. Version '+local+' is already loaded.',
3752                                   'GSAS-II Up-to-date',
3753                                   wx.OK)
3754            dlg.ShowModal()
3755            dlg.Destroy()
3756            return
3757        mods = GSASIIpath.svnFindLocalChanges()
3758        if mods:
3759            dlg = wx.MessageDialog(self.frame,
3760                                   'You have version '+local+
3761                                   ' of GSAS-II installed, but the current version is '+repos+
3762                                   '. However, '+str(len(mods))+
3763                                   ' file(s) on your local computer have been modified.'
3764                                   ' Updating will attempt to merge your local changes with '
3765                                   'the latest GSAS-II version, but if '
3766                                   'conflicts arise, local changes will be '
3767                                   'discarded. It is also possible that the '
3768                                   'local changes my prevent GSAS-II from running. '
3769                                   'Press OK to start an update if this is acceptable:',
3770                                   'Local GSAS-II Mods',
3771                                   wx.OK|wx.CANCEL)
3772            if dlg.ShowModal() != wx.ID_OK:
3773                dlg.Destroy()
3774                return
3775            else:
3776                dlg.Destroy()
3777        else:
3778            dlg = wx.MessageDialog(self.frame,
3779                                   'You have version '+local+
3780                                   ' of GSAS-II installed, but the current version is '+repos+
3781                                   '. Press OK to start an update:',
3782                                   'GSAS-II Updates',
3783                                   wx.OK|wx.CANCEL)
3784            if dlg.ShowModal() != wx.ID_OK:
3785                dlg.Destroy()
3786                return
3787            dlg.Destroy()
3788        print ('start updates')
3789        dlg = wx.MessageDialog(self.frame,
3790                               'Your project will now be saved, GSAS-II will exit and an update '
3791                               'will be performed and GSAS-II will restart. Press Cancel to '
3792                               'abort the update',
3793                               'Start update?',
3794                               wx.OK|wx.CANCEL)
3795        if dlg.ShowModal() != wx.ID_OK:
3796            dlg.Destroy()
3797            return
3798        dlg.Destroy()
3799        self.frame.OnFileSave(event)
3800        GPX = self.frame.GSASprojectfile
3801        GSASIIpath.svnUpdateProcess(projectfile=GPX)
3802        return
3803
3804    def OnSelectVersion(self,event):
3805        '''Allow the user to select a specific version of GSAS-II
3806        '''
3807        if not GSASIIpath.whichsvn():
3808            dlg = wx.MessageDialog(self,'No Subversion','Cannot update GSAS-II because subversion (svn) '+
3809                                   'was not found.'
3810                                   ,wx.OK)
3811            dlg.ShowModal()
3812            return
3813        local = GSASIIpath.svnGetRev()
3814        if local is None: 
3815            dlg = wx.MessageDialog(self.frame,
3816                                   'Unable to run subversion on the GSAS-II current directory. Is GSAS-II installed correctly?',
3817                                   'Subversion error',
3818                                   wx.OK)
3819            dlg.ShowModal()
3820            dlg.Destroy()
3821            return
3822        mods = GSASIIpath.svnFindLocalChanges()
3823        if mods:
3824            dlg = wx.MessageDialog(self.frame,
3825                                   'You have version '+local+
3826                                   ' of GSAS-II installed'
3827                                   '. However, '+str(len(mods))+
3828                                   ' file(s) on your local computer have been modified.'
3829                                   ' Downdating will attempt to merge your local changes with '
3830                                   'the selected GSAS-II version. '
3831                                   'Downdating is not encouraged because '
3832                                   'if merging is not possible, your local changes will be '
3833                                   'discarded. It is also possible that the '
3834                                   'local changes my prevent GSAS-II from running. '
3835                                   'Press OK to continue anyway.',
3836                                   'Local GSAS-II Mods',
3837                                   wx.OK|wx.CANCEL)
3838            if dlg.ShowModal() != wx.ID_OK:
3839                dlg.Destroy()
3840                return
3841            dlg.Destroy()
3842        if GSASIIpath.svnGetRev(local=False) is None:
3843            dlg = wx.MessageDialog(self.frame,
3844                                   'Error obtaining current GSAS-II version. Is internet access working correctly?',
3845                                   'Subversion error',
3846                                   wx.OK)
3847            dlg.ShowModal()
3848            dlg.Destroy()
3849            return
3850        dlg = downdate(parent=self.frame)
3851        if dlg.ShowModal() == wx.ID_OK:
3852            ver = dlg.getVersion()
3853        else:
3854            dlg.Destroy()
3855            return
3856        dlg.Destroy()
3857        print('start regress to '+str(ver))
3858        self.frame.OnFileSave(event)
3859        GPX = self.frame.GSASprojectfile
3860        GSASIIpath.svnUpdateProcess(projectfile=GPX,version=str(ver))
3861        return
3862
3863    # def OnSelectBranch(self,event):
3864    #     '''Allow the user to select branch of GSAS-II or return to trunk
3865    #     N.B. Name of branch to use is hard-coded here. Must contain a slash
3866    #     '''
3867    #     testbranch = '/branch/2frame'
3868    #     if not GSASIIpath.svnTestBranch():
3869    #         dlg = wx.MessageDialog(self.frame,
3870    #                                'Switching to test GSAS-II version',
3871    #                                'Confirm Switch',
3872    #                                wx.OK|wx.CANCEL)
3873    #         if dlg.ShowModal() != wx.ID_OK: return
3874    #         branch = testbranch
3875    #     else:
3876    #         dlg = wx.MessageDialog(self.frame,
3877    #                                'Switching back to standard GSAS-II version',
3878    #                                'Confirm Switch',
3879    #                                wx.OK|wx.CANCEL)
3880    #         if dlg.ShowModal() != wx.ID_OK: return
3881    #         branch = 'trunk'
3882    #     print('start switch')
3883    #     self.frame.OnFileSave(event)
3884    #     GPX = self.frame.GSASprojectfile
3885    #     GSASIIpath.svnUpdateProcess(projectfile=GPX,branch=branch)
3886
3887################################################################################
3888class HelpButton(wx.Button):
3889    '''Create a help button that displays help information.
3890    The text is displayed in a modal message window.
3891
3892    TODO: it might be nice if it were non-modal: e.g. it stays around until
3893    the parent is deleted or the user closes it, but this did not work for
3894    me.
3895
3896    :param parent: the panel which will be the parent of the button
3897    :param str msg: the help text to be displayed
3898    '''
3899    def __init__(self,parent,msg):
3900        if sys.platform == "darwin": 
3901            wx.Button.__init__(self,parent,wx.ID_HELP)
3902        else:
3903            wx.Button.__init__(self,parent,wx.ID_ANY,'?',style=wx.BU_EXACTFIT)
3904        self.Bind(wx.EVT_BUTTON,self._onPress)
3905        self.msg=StripIndents(msg)
3906        self.parent = parent
3907    def _onClose(self,event):
3908        self.dlg.EndModal(wx.ID_CANCEL)
3909    def _onPress(self,event):
3910        'Respond to a button press by displaying the requested text'
3911        #dlg = wx.MessageDialog(self.parent,self.msg,'Help info',wx.OK)
3912        self.dlg = wx.Dialog(self.parent,wx.ID_ANY,'Help information', 
3913                        style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)
3914        #self.dlg.SetBackgroundColour(wx.WHITE)
3915        mainSizer = wx.BoxSizer(wx.VERTICAL)
3916        txt = wx.StaticText(self.dlg,wx.ID_ANY,self.msg)
3917        mainSizer.Add(txt,1,wx.ALL|wx.EXPAND,10)
3918        txt.SetBackgroundColour(wx.WHITE)
3919
3920        btnsizer = wx.BoxSizer(wx.HORIZONTAL)
3921        btn = wx.Button(self.dlg, wx.ID_CLOSE) 
3922        btn.Bind(wx.EVT_BUTTON,self._onClose)
3923        btnsizer.Add(btn)
3924        mainSizer.Add(btnsizer, 0, wx.ALIGN_CENTER|wx.ALL, 5)
3925        self.dlg.SetSizer(mainSizer)
3926        mainSizer.Fit(self.dlg)
3927        self.dlg.CenterOnParent()
3928        self.dlg.ShowModal()
3929        self.dlg.Destroy()
3930################################################################################
3931class MyHtmlPanel(wx.Panel):
3932    '''Defines a panel to display HTML help information, as an alternative to
3933    displaying help information in a web browser.
3934    '''
3935    def __init__(self, frame, id):
3936        self.frame = frame
3937        wx.Panel.__init__(self, frame, id)
3938        sizer = wx.BoxSizer(wx.VERTICAL)
3939        back = wx.Button(self, -1, "Back")
3940        back.Bind(wx.EVT_BUTTON, self.OnBack)
3941        self.htmlwin = G2HtmlWindow(self, id, size=(750,450))
3942        sizer.Add(self.htmlwin, 1,wx.EXPAND)
3943        sizer.Add(back, 0, wx.ALIGN_LEFT, 0)
3944        self.SetSizer(sizer)
3945        sizer.Fit(frame)       
3946        self.Bind(wx.EVT_SIZE,self.OnHelpSize)
3947    def OnHelpSize(self,event):         #does the job but weirdly!!
3948        anchor = self.htmlwin.GetOpenedAnchor()
3949        if anchor:           
3950            self.htmlwin.ScrollToAnchor(anchor)
3951            wx.CallAfter(self.htmlwin.ScrollToAnchor,anchor)
3952            if event: event.Skip()
3953    def OnBack(self, event):
3954        self.htmlwin.HistoryBack()
3955    def LoadFile(self,file):
3956        pos = file.rfind('#')
3957        if pos != -1:
3958            helpfile = file[:pos]
3959            helpanchor = file[pos+1:]
3960        else:
3961            helpfile = file
3962            helpanchor = None
3963        self.htmlwin.LoadPage(helpfile)
3964        if helpanchor is not None:
3965            self.htmlwin.ScrollToAnchor(helpanchor)
3966            xs,ys = self.htmlwin.GetViewStart()
3967            self.htmlwin.Scroll(xs,ys-1)
3968################################################################################
3969class G2HtmlWindow(wx.html.HtmlWindow):
3970    '''Displays help information in a primitive HTML browser type window
3971    '''
3972    def __init__(self, parent, *args, **kwargs):
3973        self.parent = parent
3974        wx.html.HtmlWindow.__init__(self, parent, *args, **kwargs)
3975    def LoadPage(self, *args, **kwargs):
3976        wx.html.HtmlWindow.LoadPage(self, *args, **kwargs)
3977        self.TitlePage()
3978    def OnLinkClicked(self, *args, **kwargs):
3979        wx.html.HtmlWindow.OnLinkClicked(self, *args, **kwargs)
3980        xs,ys = self.GetViewStart()
3981        self.Scroll(xs,ys-1)
3982        self.TitlePage()
3983    def HistoryBack(self, *args, **kwargs):
3984        wx.html.HtmlWindow.HistoryBack(self, *args, **kwargs)
3985        self.TitlePage()
3986    def TitlePage(self):
3987        self.parent.frame.SetTitle(self.GetOpenedPage() + ' -- ' + 
3988            self.GetOpenedPageTitle())
3989
3990################################################################################
3991def StripIndents(msg):
3992    'Strip indentation from multiline strings'
3993    msg1 = msg.replace('\n ','\n')
3994    while msg != msg1:
3995        msg = msg1
3996        msg1 = msg.replace('\n ','\n')
3997    return msg.replace('\n\t','\n')
3998
3999def StripUnicode(string,subs='.'):
4000    '''Strip non-ASCII characters from strings
4001   
4002    :param str string: string to strip Unicode characters from
4003    :param str subs: character(s) to place into string in place of each
4004      Unicode character. Defaults to '.'
4005
4006    :returns: a new string with only ASCII characters
4007    '''
4008    s = ''
4009    for c in string:
4010        if ord(c) < 128:
4011            s += c
4012        else:
4013            s += subs
4014    return s.encode('ascii','replace')
4015       
4016################################################################################
4017# configuration routines (for editing config.py)
4018def SaveGPXdirectory(path):
4019    if GSASIIpath.GetConfigValue('Starting_directory') == path: return
4020    vars = GetConfigValsDocs()
4021    try:
4022        vars['Starting_directory'][1] = path
4023        if GSASIIpath.GetConfigValue('debug'): print('Saving GPX path: '+path)
4024        SaveConfigVars(vars)
4025    except KeyError:
4026        pass
4027
4028def SaveImportDirectory(path):
4029    if GSASIIpath.GetConfigValue('Import_directory') == path: return
4030    vars = GetConfigValsDocs()
4031    try:
4032        vars['Import_directory'][1] = path
4033        if GSASIIpath.GetConfigValue('debug'): print('Saving Import path: '+path)
4034        SaveConfigVars(vars)
4035    except KeyError:
4036        pass
4037
4038def GetConfigValsDocs():
4039    '''Reads the module referenced in fname (often <module>.__file__) and
4040    return a dict with names of global variables as keys.
4041    For each global variable, the value contains four items:
4042
4043    :returns: a dict where keys are names defined in module config_example.py
4044      where the value is a list of four items, as follows:
4045
4046         * item 0: the default value
4047         * item 1: the current value
4048         * item 2: the initial value (starts same as item 1)
4049         * item 3: the "docstring" that follows variable definition
4050
4051    '''
4052    import config_example
4053    import ast
4054    fname = os.path.splitext(config_example.__file__)[0]+'.py' # convert .pyc to .py
4055    with open(fname, 'r') as f:
4056        fstr = f.read()
4057    fstr = fstr.replace('\r\n', '\n').replace('\r', '\n')
4058    if not fstr.endswith('\n'):
4059        fstr += '\n'
4060    tree = ast.parse(fstr)
4061    d = {}
4062    key = None
4063    for node in ast.walk(tree):
4064        if isinstance(node,ast.Assign):
4065            key = node.targets[0].id
4066            d[key] = [config_example.__dict__.get(key),
4067                      GSASIIpath.configDict.get(key),
4068                      GSASIIpath.configDict.get(key),'']
4069        elif isinstance(node,ast.Expr) and key:
4070            d[key][3] = node.value.s.strip()
4071        else:
4072            key = None
4073    return d
4074
4075def SaveConfigVars(vars,parent=None):
4076    '''Write the current config variable values to config.py
4077
4078    :params dict vars: a dictionary of variable settings and meanings as
4079      created in :func:`GetConfigValsDocs`.
4080    :param parent: wx.Frame object or None (default) for parent
4081      of error message if no file can be written.
4082    :returns: True if unable to write the file, None otherwise
4083    '''
4084    # try to write to where an old config file is located
4085    try:
4086        import config
4087        savefile = config.__file__
4088    except ImportError: # no config.py file yet
4089        savefile = os.path.join(GSASIIpath.path2GSAS2,'config.py')
4090    # try to open file for write
4091    try:
4092        savefile = os.path.splitext(savefile)[0]+'.py' # convert .pyc to .py
4093        fp = open(savefile,'w')
4094    except IOError:  # can't write there, write in local mods directory
4095        # create a local mods directory, if needed
4096        g2local = os.path.expanduser('~/.G2local/')
4097        if not os.path.exists(g2local):
4098            try:
4099                print(u'Creating directory '+g2local)
4100                os.mkdir(g2local)
4101            except:
4102                if parent:
4103                    G2MessageBox(parent,u'Error trying to create directory '+g2local,
4104                        'Unable to save')
4105                else:
4106                    print(u'Error trying to create directory '+g2local)
4107                return True
4108            sys.path.insert(0,os.path.expanduser('~/.G2local/'))
4109        savefile = os.path.join(os.path.expanduser('~/.G2local/'),'config.py')
4110        try:
4111            fp = open(savefile,'w')
4112        except IOError:
4113            if parent:
4114                G2MessageBox(parent,'Error trying to write configuration to '+savefile,
4115                    'Unable to save')
4116            else:
4117                print('Error trying to write configuration to '+savefile)
4118            return True
4119    import datetime
4120    fp.write("'''\n")
4121    fp.write("*config.py: Configuration options*\n----------------------------------\n")
4122    fp.write("This file created in SelectConfigSetting on {:%d %b %Y %H:%M}\n".
4123             format(datetime.datetime.now()))
4124    fp.write("'''\n\n")
4125    fp.write("import os.path\n")
4126    fp.write("import GSASIIpath\n\n")
4127    for var in sorted(vars.keys(),key=lambda s: s.lower()):
4128        if vars[var][1] is None: continue
4129        if vars[var][1] == '': continue
4130        if vars[var][0] == vars[var][1]: continue
4131        try:
4132            float(vars[var][1]) # test for number
4133            fp.write(var + ' = ' + str(vars[var][1])+'\n')
4134        except:
4135            try:
4136                eval(vars[var][1]) # test for an expression
4137                fp.write(var + ' = ' + str(vars[var][1])+'\n')
4138            except: # must be a string
4139                varstr = vars[var][1]
4140                if '\\' in varstr:
4141                    fp.write(var + ' = os.path.normpath("' + varstr.replace('\\','/') +'")\n')
4142                else:
4143                    fp.write(var + ' = "' + str(varstr)+'"\n')
4144        if vars[var][3]:
4145            fp.write("'''" + str(vars[var][3]) + "\n'''\n\n")
4146    fp.close()
4147    print('wrote file '+savefile)
4148
4149class SelectConfigSetting(wx.Dialog):
4150    '''Dialog to select configuration variables and set associated values.
4151    '''
4152    def __init__(self,parent=None):
4153        style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER
4154        wx.Dialog.__init__(self, parent, wx.ID_ANY, 'Set Config Variable', style=style)
4155        self.sizer = wx.BoxSizer(wx.VERTICAL)
4156        self.vars = GetConfigValsDocs()
4157       
4158        label = wx.StaticText(
4159            self,  wx.ID_ANY,
4160            'Select a GSAS-II configuration variable to change'
4161            )
4162        self.sizer.Add(label, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
4163        self.choice = {}
4164        btn = G2ChoiceButton(self, sorted(self.vars.keys(),key=lambda s: s.lower()),
4165            strLoc=self.choice,strKey=0,onChoice=self.OnSelection)
4166        btn.SetLabel("")
4167        self.sizer.Add(btn)
4168
4169        self.varsizer = wx.BoxSizer(wx.VERTICAL)
4170        self.sizer.Add(self.varsizer,1,wx.ALL|wx.EXPAND,1)
4171       
4172        self.doclbl = wx.StaticBox(self, wx.ID_ANY, "")
4173        self.doclblsizr = wx.StaticBoxSizer(self.doclbl)
4174        self.docinfo = wx.StaticText(self,  wx.ID_ANY, "")
4175        self.doclblsizr.Add(self.docinfo, 0, wx.ALIGN_LEFT|wx.ALL, 5)
4176        self.sizer.Add(self.doclblsizr, 0, wx.ALIGN_LEFT|wx.EXPAND|wx.ALL, 5)
4177        btnsizer = wx.BoxSizer(wx.HORIZONTAL)
4178        self.saveBtn = wx.Button(self,-1,"Save current settings")
4179        btnsizer.Add(self.saveBtn, 0, wx.ALL, 2) 
4180        self.saveBtn.Bind(wx.EVT_BUTTON, self.OnSave)
4181        self.saveBtn.Enable(False)
4182        self.applyBtn = wx.Button(self,-1,"Use current (no save)")
4183        btnsizer.Add(self.applyBtn, 0, wx.ALL, 2) 
4184        self.applyBtn.Bind(wx.EVT_BUTTON, self.OnApplyChanges)
4185        self.applyBtn.Enable(False)
4186       
4187        btn = wx.Button(self,wx.ID_CANCEL)
4188        btnsizer.Add(btn, 0, wx.ALL, 2) 
4189        self.sizer.Add(btnsizer, 0, wx.ALIGN_CENTRE|wx.ALL, 5) 
4190               
4191        self.SetSizer(self.sizer)
4192        self.sizer.Fit(self)
4193        self.CenterOnParent()
4194       
4195    def OnChange(self,event=None):
4196        ''' Check if anything been changed. Turn the save button on/off.
4197        '''
4198        for var in self.vars:
4199            if self.vars[var][0] is None and self.vars[var][1] is not None:
4200                # make blank strings into None, if that is the default
4201                if self.vars[var][1].strip() == '': self.vars[var][1] = None
4202            if self.vars[var][1] != self.vars[var][2]:
4203                #print 'changed',var,self.vars[var][:3]
4204                self.saveBtn.Enable(True)
4205                self.applyBtn.Enable(True)
4206                break
4207        else:
4208            self.saveBtn.Enable(False)
4209            self.applyBtn.Enable(False)
4210        try:
4211            self.resetBtn.Enable(True)
4212        except:
4213            pass
4214       
4215    def OnApplyChanges(self,event=None):
4216        'Set config variables to match the current settings'
4217        GSASIIpath.SetConfigValue(self.vars)
4218        self.EndModal(wx.ID_OK)
4219        import GSASIImpsubs as G2mp
4220        G2mp.ResetMP()
4221       
4222    def OnSave(self,event):
4223        '''Write the config variables to config.py and then set them
4224        as the current settings
4225        '''
4226        if not SaveConfigVars(self.vars,parent=self):
4227            self.OnApplyChanges() # force a reload of the config settings
4228        else:
4229            self.EndModal(wx.ID_OK)
4230
4231    def OnBoolSelect(self,event):
4232        'Respond to a change in a True/False variable'
4233        rb = event.GetEventObject()
4234        var = self.choice[0]
4235        self.vars[var][1] = (rb.GetSelection() == 0)
4236        self.OnChange()
4237        wx.CallAfter(self.OnSelection)
4238       
4239    def onSelDir(self,event):
4240        'Select a directory from a menu'
4241        dlg = wx.DirDialog(self, "Choose a directory:",style=wx.DD_DEFAULT_STYLE)
4242        if dlg.ShowModal() == wx.ID_OK:
4243            var = self.choice[0]
4244            self.vars[var][1] = dlg.GetPath()
4245            self.strEd.SetValue(self.vars[var][1])
4246            self.OnChange()
4247        dlg.Destroy()
4248       
4249    def OnSelection(self):
4250        'show a selected variable'
4251        def OnNewColorBar(event):
4252            self.vars['Contour_color'][1] = self.colSel.GetValue()
4253            self.OnChange(event)
4254
4255        if 'phoenix' in wx.version():
4256            self.varsizer.Clear(True)
4257        else:
4258            self.varsizer.DeleteWindows()
4259        var = self.choice[0]
4260        showdef = True
4261        if var not in self.vars:
4262            raise Exception("How did this happen?")
4263        if type(self.vars[var][0]) is int:
4264            ed = ValidatedTxtCtrl(self,self.vars[var],1,typeHint=int,OKcontrol=self.OnChange)
4265            self.varsizer.Add(ed, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
4266        elif type(self.vars[var][0]) is float:
4267            ed = ValidatedTxtCtrl(self,self.vars[var],1,typeHint=float,OKcontrol=self.OnChange)
4268            self.varsizer.Add(ed, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
4269        elif type(self.vars[var][0]) is bool:
4270            showdef = False
4271            lbl = "value for "+var
4272            ch = []
4273            for i,v in enumerate((True,False)):
4274                s = str(v)
4275                if v == self.vars[var][0]:
4276                    defopt = i
4277                    s += ' (default)'
4278                ch += [s]
4279            rb = wx.RadioBox(self, wx.ID_ANY, lbl, wx.DefaultPosition, wx.DefaultSize,
4280                ch, 1, wx.RA_SPECIFY_COLS)
4281            # set initial value
4282            if self.vars[var][1] is None:
4283                rb.SetSelection(defopt)
4284            elif self.vars[var][1]:
4285                rb.SetSelection(0)
4286            else:
4287                rb.SetSelection(1)
4288            rb.Bind(wx.EVT_RADIOBOX,self.OnBoolSelect)
4289            self.varsizer.Add(rb, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
4290        else:
4291            if var.endswith('_directory') or var.endswith('_location'):
4292                btn = wx.Button(self,wx.ID_ANY,'Select from dialog...')
4293                sz = (400,-1)
4294            else:
4295                btn = None
4296                sz = (250,-1)
4297            if var == 'Contour_color':
4298                if self.vars[var][1] is None:
4299                    self.vars[var][1] = 'Paired'
4300                colorList = sorted([m for m in mpl.cm.datad.keys() ],key=lambda s: s.lower())   #if not m.endswith("_r")
4301                self.colSel = wx.ComboBox(self,value=self.vars[var][1],choices=colorList,
4302                    style=wx.CB_READONLY|wx.CB_DROPDOWN)
4303                self.colSel.Bind(wx.EVT_COMBOBOX, OnNewColorBar)
4304                self.varsizer.Add(self.colSel, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
4305            else:
4306                self.strEd = ValidatedTxtCtrl(self,self.vars[var],1,typeHint=str,
4307                    OKcontrol=self.OnChange,size=sz)
4308                if self.vars[var][1] is not None:
4309                    self.strEd.SetValue(self.vars[var][1])
4310                self.varsizer.Add(self.strEd, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
4311            if btn:
4312                btn.Bind(wx.EVT_BUTTON,self.onSelDir)
4313                self.varsizer.Add(btn, 0, wx.ALIGN_CENTRE|wx.ALL, 5) 
4314        # button for reset to default value
4315        lbl = "Reset to Default"
4316        if showdef: # spell out default when needed
4317            lbl += ' (='+str(self.vars[var][0])+')'
4318            #label = wx.StaticText(self,  wx.ID_ANY, 'Default value = '+str(self.vars[var][0]))
4319            #self.varsizer.Add(label, 0, wx.ALIGN_LEFT|wx.ALL, 5)
4320        self.resetBtn = wx.Button(self,-1,lbl)
4321        self.resetBtn.Bind(wx.EVT_BUTTON, self.OnClear)
4322        if self.vars[var][1] is not None and self.vars[var][1] != '': # show current value, if one
4323            #label = wx.StaticText(self,  wx.ID_ANY, 'Current value = '+str(self.vars[var][1]))
4324            #self.varsizer.Add(label, 0, wx.ALIGN_LEFT|wx.ALL, 5)
4325            self.resetBtn.Enable(True)
4326        else:
4327            self.resetBtn.Enable(False)
4328        self.varsizer.Add(self.resetBtn, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
4329        # show meaning, if defined
4330        self.doclbl.SetLabel("Description of "+str(var)) 
4331        if self.vars[var][3]:
4332            self.docinfo.SetLabel(self.vars[var][3])
4333        else:
4334            self.docinfo.SetLabel("(not documented)")
4335        self.sizer.Fit(self)
4336        self.CenterOnParent()
4337        wx.CallAfter(self.SendSizeEvent)
4338
4339    def OnClear(self, event):
4340        var = self.choice[0]
4341        self.vars[var][1] = self.vars[var][0]
4342        self.OnChange()
4343        wx.CallAfter(self.OnSelection)
4344       
4345################################################################################
4346class downdate(wx.Dialog):
4347    '''Dialog to allow a user to select a version of GSAS-II to install
4348    '''
4349    def __init__(self,parent=None):
4350        style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER
4351        wx.Dialog.__init__(self, parent, wx.ID_ANY, 'Select Version', style=style)
4352        pnl = wx.Panel(self)
4353        sizer = wx.BoxSizer(wx.VERTICAL)
4354        insver = GSASIIpath.svnGetRev(local=True)
4355        curver = int(GSASIIpath.svnGetRev(local=False))
4356        label = wx.StaticText(
4357            pnl,  wx.ID_ANY,
4358            'Select a specific GSAS-II version to install'
4359            )
4360        sizer.Add(label, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
4361        sizer1 = wx.BoxSizer(wx.HORIZONTAL)
4362        sizer1.Add(
4363            wx.StaticText(pnl,  wx.ID_ANY,
4364                          'Currently installed version: '+str(insver)),
4365            0, wx.ALIGN_CENTRE|wx.ALL, 5)
4366        sizer.Add(sizer1)
4367        sizer1 = wx.BoxSizer(wx.HORIZONTAL)
4368        sizer1.Add(
4369            wx.StaticText(pnl,  wx.ID_ANY,
4370                          'Select GSAS-II version to install: '),
4371            0, wx.ALIGN_CENTRE|wx.ALL, 5)
4372        self.spin = wx.SpinCtrl(pnl, wx.ID_ANY,size=(150,-1))
4373        self.spin.SetRange(1, curver)
4374        self.spin.SetValue(curver)
4375        self.Bind(wx.EVT_SPINCTRL, self._onSpin, self.spin)
4376        self.Bind(wx.EVT_KILL_FOCUS, self._onSpin, self.spin)
4377        sizer1.Add(self.spin)
4378        sizer.Add(sizer1)
4379
4380        line = wx.StaticLine(pnl,-1, size=(-1,3), style=wx.LI_HORIZONTAL)
4381        sizer.Add(line, 0, wx.EXPAND|wx.ALIGN_CENTER|wx.ALL, 10)
4382
4383        self.text = wx.StaticText(pnl,  wx.ID_ANY, "")
4384        sizer.Add(self.text, 0, wx.ALIGN_LEFT|wx.EXPAND|wx.ALL, 5)
4385
4386        line = wx.StaticLine(pnl,-1, size=(-1,3), style=wx.LI_HORIZONTAL)
4387        sizer.Add(line, 0, wx.EXPAND|wx.ALIGN_CENTER|wx.ALL, 10)
4388        sizer.Add(
4389            wx.StaticText(
4390                pnl,  wx.ID_ANY,
4391                'If "Install" is pressed, your project will be saved;\n'
4392                'GSAS-II will exit; The specified version will be loaded\n'
4393                'and GSAS-II will restart. Press "Cancel" to abort.'),
4394            0, wx.EXPAND|wx.ALL, 10)
4395        btnsizer = wx.StdDialogButtonSizer()
4396        btn = wx.Button(pnl, wx.ID_OK, "Install")
4397        btn.SetDefault()
4398        btnsizer.AddButton(btn)
4399        btn = wx.Button(pnl, wx.ID_CANCEL)
4400        btnsizer.AddButton(btn)
4401        btnsizer.Realize()
4402        sizer.Add(btnsizer, 0, wx.ALIGN_CENTER|wx.ALL, 5)
4403        pnl.SetSizer(sizer)
4404        sizer.Fit(self)
4405        self.topsizer=sizer
4406        self.CenterOnParent()
4407        self._onSpin(None)
4408
4409    def _onSpin(self,event):
4410        'Called to load info about the selected version in the dialog'
4411        if event: event.Skip()
4412        ver = self.spin.GetValue()
4413        d = GSASIIpath.svnGetLog(version=ver)
4414        date = d.get('date','?').split('T')[0]
4415        s = '(Version '+str(ver)+' created '+date
4416        s += ' by '+d.get('author','?')+')'
4417        msg = d.get('msg')
4418        if msg: s += '\n\nComment: '+msg
4419        self.text.SetLabel(s)
4420        self.topsizer.Fit(self)
4421
4422    def getVersion(self):
4423        'Get the version number in the dialog'
4424        return self.spin.GetValue()
4425
4426################################################################################
4427#### Display Help information
4428################################################################################
4429# define some globals
4430htmlPanel = None
4431htmlFrame = None
4432htmlFirstUse = True
4433#helpLocDict = {}  # to be implemented if we ever split gsasii.html over multiple files
4434path2GSAS2 = os.path.dirname(os.path.realpath(__file__)) # save location of this file
4435def ShowHelp(helpType,frame):
4436    '''Called to bring up a web page for documentation.'''
4437    global htmlFirstUse,htmlPanel,htmlFrame
4438    # no defined link to use, create a default based on key
4439    helplink = 'gsasII.html'
4440    if helpType:
4441        helplink += '#'+helpType.replace(')','').replace('(','_').replace(' ','_')
4442    # determine if a web browser or the internal viewer should be used for help info
4443    if GSASIIpath.GetConfigValue('Help_mode'):
4444        helpMode = GSASIIpath.GetConfigValue('Help_mode')
4445    else:
4446        helpMode = 'browser'
4447    if helpMode == 'internal':
4448        helplink = os.path.join(path2GSAS2,'help',helplink)
4449        try:
4450            htmlPanel.LoadFile(helplink)
4451            htmlFrame.Raise()
4452        except:
4453            htmlFrame = wx.Frame(frame, -1, size=(610, 510))
4454            htmlFrame.Show(True)
4455            htmlFrame.SetTitle("HTML Window") # N.B. reset later in LoadFile
4456            htmlPanel = MyHtmlPanel(htmlFrame,-1)
4457            htmlPanel.LoadFile(helplink)
4458    else:
4459        if sys.platform == "darwin": # for Mac, force use of safari to preserve anchors on file URLs
4460            wb = webbrowser.MacOSXOSAScript('safari')
4461        else:
4462            wb = webbrowser
4463        helplink = os.path.join(path2GSAS2,'help',helplink)
4464        pfx = "file://"
4465        if sys.platform.lower().startswith('win'):
4466            pfx = ''
4467        #if GSASIIpath.GetConfigValue('debug'): print 'Help link=',pfx+helplink
4468        if htmlFirstUse:
4469            wb.open_new(pfx+helplink)
4470            htmlFirstUse = False
4471        else:
4472            wb.open(pfx+helplink, new=0, autoraise=True)
4473
4474def ShowWebPage(URL,frame):
4475    '''Called to show a tutorial web page.
4476    '''
4477    global htmlFirstUse,htmlPanel,htmlFrame
4478    # determine if a web browser or the internal viewer should be used for help info
4479    if GSASIIpath.GetConfigValue('Help_mode'):
4480        helpMode = GSASIIpath.GetConfigValue('Help_mode')
4481    else:
4482        helpMode = 'browser'
4483    if helpMode == 'internal':
4484        try:
4485            htmlPanel.LoadFile(URL)
4486            htmlFrame.Raise()
4487        except:
4488            htmlFrame = wx.Frame(frame, -1, size=(610, 510))
4489            htmlFrame.Show(True)
4490            htmlFrame.SetTitle("HTML Window") # N.B. reset later in LoadFile
4491            htmlPanel = MyHtmlPanel(htmlFrame,-1)
4492            htmlPanel.LoadFile(URL)
4493    else:
4494        if URL.startswith('http'): 
4495            pfx = ''
4496        elif sys.platform.lower().startswith('win'):
4497            pfx = ''
4498        else:
4499            pfx = "file://"
4500        if htmlFirstUse:
4501            webbrowser.open_new(pfx+URL)
4502            htmlFirstUse = False
4503        else:
4504            webbrowser.open(pfx+URL, new=0, autoraise=True)
4505
4506################################################################################
4507#### Tutorials support
4508################################################################################
4509G2BaseURL = "https://subversion.xray.aps.anl.gov/pyGSAS"
4510tutorialIndex = (
4511    # tutorial dir,      web page file name,      title for page
4512    ['Getting started'],
4513    ['StartingGSASII', 'Starting GSAS.htm', 'Starting GSAS-II'],
4514
4515    ['Rietveld fitting'],
4516       
4517    ['LabData', 'Laboratory X.htm', 'Fitting laboratory X-ray powder data for fluoroapatite'],
4518    ['CWNeutron', 'Neutron CW Powder Data.htm', 'CW Neutron Powder fit for Yttrium-Iron Garnet'],
4519    ['CWCombined', 'Combined refinement.htm', 'Combined X-ray/CW-neutron refinement of PbSO4'],
4520    ['TOF-CW Joint Refinement', 'TOF combined XN Rietveld refinement in GSAS.htm', 'Combined X-ray/TOF-neutron Rietveld refinement'],
4521    ['BkgFit', 'FitBkgTut.htm',  'Fitting the Starting Background using Fixed Points'],
4522   
4523    ['Parametric Rietveld fitting'],
4524    ['SeqRefine', 'SequentialTutorial.htm', 'Sequential refinement of multiple datasets'],
4525    ['SeqParametric', 'ParametricFitting.htm', '     Parametric Fitting and Pseudo Variables for Sequential Fits'],
4526
4527    ['Structure solution'],
4528    ['FitPeaks', 'Fit Peaks.htm', 'Fitting individual peaks & autoindexing'],
4529    ['CFjadarite', 'Charge Flipping in GSAS.htm', '     Charge Flipping structure solution for jadarite'],
4530    ['CFsucrose', 'Charge Flipping - sucrose.htm','     Charge Flipping structure solution for sucrose'],
4531    ['CFXraySingleCrystal', 'CFSingleCrystal.htm', 'Charge Flipping structure solution with Xray single crystal data'],       
4532    ['TOF Charge Flipping', 'Charge Flipping with TOF single crystal data in GSASII.htm', 'Charge flipping with neutron TOF single crystal data'],
4533    ['MCsimanneal', 'MCSA in GSAS.htm', 'Monte-Carlo simulated annealing structure determination'],
4534
4535    ['Stacking Fault Modeling'],
4536    ['StackingFaults-I', 'Stacking Faults-I.htm', 'Stacking fault simulations for diamond'],
4537    ['StackingFaults-II', 'Stacking Faults II.htm', 'Stacking fault simulations for Keokuk kaolinite'],
4538    ['StackingFaults-III', 'Stacking Faults-III.htm', 'Stacking fault simulations for Georgia kaolinite'],
4539
4540    ['Image Calibration/Integration'],
4541    ['2DCalibration', 'Calibration of an area detector in GSAS.htm', 'Calibration of an area detector'],
4542    ['2DIntegration', 'Integration of area detector data in GSAS.htm', '     Integration of area detector data'],
4543    ['TOF Calibration', 'Calibration of a TOF powder diffractometer.htm', 'Calibration of a Neutron TOF diffractometer'],
4544
4545    ['Small-Angle Scattering'],       
4546    ['2DStrain', 'Strain fitting of 2D data in GSAS-II.htm', 'Strain fitting of 2D data'],
4547    ['2DTexture', 'Texture analysis of 2D data in GSAS-II.htm', 'Texture analysis of 2D data'],
4548             
4549    ['SAsize', 'Small Angle Size Distribution.htm', 'Small angle x-ray data size distribution (alumina powder)'],
4550    ['SAfit', 'Fitting Small Angle Scattering Data.htm', '     Fitting small angle x-ray data (alumina powder)'],
4551    ['SAimages', 'Small Angle Image Processing.htm', 'Image Processing of small angle x-ray data'],
4552    ['SAseqref', 'Sequential Refinement of Small Angle Scattering Data.htm', 'Sequential refinement with small angle scattering data'],
4553
4554    ['Other'],   
4555    ['MerohedralTwins', 'Merohedral twin refinement in GSAS.htm', 'Merohedral twin refinements'],
4556    ['TOF Single Crystal Refinement', 'TOF single crystal refinement in GSAS.htm', 'Single crystal refinement from TOF data'],
4557    ['PythonScript','Scripting.htm','Scripting a GSAS-II Refinement from Python']
4558   
4559    #['ExampleDir', 'ExamplePage.html', 'Example Tutorial Title'],
4560    )
4561tutorialCatalog = [l for l in tutorialIndex if len(l) == 3]
4562
4563class OpenTutorial(wx.Dialog):
4564    '''Open a tutorial web page, optionally copying the web page, screen images and
4565    data file(s) to the local disk.
4566    '''
4567   
4568    def __init__(self,parent):
4569        style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER
4570        wx.Dialog.__init__(self, parent, wx.ID_ANY, 'Open Tutorial', style=style)
4571        self.G2frame = self.frame = parent
4572        pnl = wx.Panel(self)
4573        sizer = wx.BoxSizer(wx.VERTICAL)
4574        sizer1 = wx.BoxSizer(wx.HORIZONTAL)       
4575        label = wx.StaticText(
4576            pnl,  wx.ID_ANY,
4577            'Select the tutorial to be run and the mode of access'
4578            )
4579        msg = '''GSAS-II tutorials and their sample data files
4580        require a fair amount of storage space; few users will
4581        use all of them. This dialog allows users to load selected
4582        tutorials (along with their sample data) to their computer;
4583        optionally all tutorials can be downloaded.
4584
4585        Downloaded tutorials can be viewed and run without internet
4586        access. Tutorials can also be viewed without download, but
4587        users will need to download the sample data files manually.
4588
4589        The location used to download tutorials is set using the
4590        "Set download location" which is saved as the "Tutorial_location"
4591        configuration option see File/Preference or the
4592        config_example.py file. System managers can select to have
4593        tutorial files installed at a shared location.
4594        '''
4595        self.SetTutorialPath()
4596        hlp = HelpButton(pnl,msg)
4597        sizer1.Add((-1,-1),1, wx.EXPAND, 0)
4598        sizer1.Add(label, 0, wx.ALIGN_CENTRE|wx.ALL, 0)
4599        sizer1.Add((-1,-1),1, wx.EXPAND, 0)
4600        sizer1.Add(hlp,0,wx.ALIGN_RIGHT|wx.ALL)
4601        sizer.Add(sizer1,0,wx.EXPAND|wx.ALL,0)
4602        sizer.Add((10,10))
4603        sizer0 = wx.BoxSizer(wx.HORIZONTAL)       
4604        sizer1a = wx.BoxSizer(wx.VERTICAL)
4605        sizer1b = wx.BoxSizer(wx.VERTICAL)
4606        btn = wx.Button(pnl, wx.ID_ANY, "Download a tutorial and view")
4607        btn.Bind(wx.EVT_BUTTON, self.SelectAndDownload)
4608        sizer1a.Add(btn,0,WACV)
4609        btn = wx.Button(pnl, wx.ID_ANY, "Select from downloaded tutorials")
4610        btn.Bind(wx.EVT_BUTTON, self.onSelectDownloaded)
4611        sizer1a.Add(btn,0,WACV)
4612        btn = wx.Button(pnl, wx.ID_ANY, "Browse tutorial on web")
4613        btn.Bind(wx.EVT_BUTTON, self.onWebBrowse)
4614        sizer1a.Add(btn,0,WACV)
4615        btn = wx.Button(pnl, wx.ID_ANY, "Update downloaded tutorials")
4616        btn.Bind(wx.EVT_BUTTON, self.UpdateDownloaded)
4617        sizer1b.Add(btn,0,WACV)
4618        btn = wx.Button(pnl, wx.ID_ANY, "Download all tutorials")
4619        btn.Bind(wx.EVT_BUTTON, self.DownloadAll)
4620        sizer1b.Add(btn,0,WACV)
4621        sizer0.Add(sizer1a,0,wx.EXPAND|wx.ALL,0)
4622        sizer0.Add(sizer1b,0,wx.EXPAND|wx.ALL,0)
4623        sizer.Add(sizer0,5,wx.EXPAND|wx.ALL,5)
4624       
4625        sizer.Add((10,10))
4626        sizer1 = wx.BoxSizer(wx.HORIZONTAL)
4627        btn = wx.Button(pnl, wx.ID_ANY, "Set download location")
4628        btn.Bind(wx.EVT_BUTTON, self.SelectDownloadLoc)
4629        sizer1.Add(btn,0,WACV)
4630        self.dataLoc = wx.StaticText(pnl, wx.ID_ANY,self.tutorialPath)
4631        sizer1.Add(self.dataLoc,0,WACV)
4632        sizer.Add(sizer1)
4633       
4634        btnsizer = wx.StdDialogButtonSizer()
4635        btn = wx.Button(pnl, wx.ID_CANCEL,"Done")
4636        btnsizer.AddButton(btn)
4637        btnsizer.Realize()
4638        sizer.Add(btnsizer, 0, wx.ALIGN_CENTER|wx.ALL, 5)
4639        pnl.SetSizer(sizer)
4640        sizer.Fit(self)
4641        self.topsizer=sizer
4642        self.CenterOnParent()
4643
4644    def SetTutorialPath(self):
4645        '''Get the tutorial location if set; if not pick a default
4646        directory in a logical place
4647        '''
4648        if GSASIIpath.GetConfigValue('Tutorial_location'):
4649            self.tutorialPath = os.path.abspath(GSASIIpath.GetConfigValue('Tutorial_location'))
4650        elif (sys.platform.lower().startswith('win') and
4651              os.path.exists(os.path.abspath(os.path.expanduser('~/My Documents')))):
4652            self.tutorialPath = os.path.abspath(os.path.expanduser('~/My Documents/G2tutorials'))
4653        elif (sys.platform.lower().startswith('win') and
4654              os.path.exists(os.path.abspath(os.path.expanduser('~/Documents')))):
4655            self.tutorialPath = os.path.abspath(os.path.expanduser('~/Documents/G2tutorials'))
4656        else:
4657            self.tutorialPath = os.path.abspath(os.path.expanduser('~/G2tutorials'))
4658
4659    def SelectAndDownload(self,event):
4660        '''Make a list of all tutorials on web and allow user to choose one to
4661        download and then view
4662        '''
4663        indices = [j for j,i in enumerate(tutorialCatalog)
4664            if not os.path.exists(os.path.join(self.tutorialPath,i[0],i[1]))]
4665        if not indices:
4666            G2MessageBox(self,'All tutorials are downloaded','None to download')
4667            return
4668        choices = [tutorialCatalog[i][2] for i in indices]
4669        selected = self.ChooseTutorial(choices)
4670        if selected is None: return
4671        j = indices[selected]
4672        fullpath = os.path.join(self.tutorialPath,tutorialCatalog[j][0],tutorialCatalog[j][1])
4673        fulldir = os.path.join(self.tutorialPath,tutorialCatalog[j][0])
4674        URL = G2BaseURL+'/Tutorials/'+tutorialCatalog[j][0]+'/'
4675        if GSASIIpath.svnInstallDir(URL,fulldir):
4676            ShowWebPage(fullpath,self.frame)
4677        else:
4678            G2MessageBox(self,'Error downloading tutorial','Download error')
4679        self.EndModal(wx.ID_OK)
4680        self.G2frame.TutorialImportDir = os.path.join(self.tutorialPath,tutorialCatalog[j][0],'data')
4681
4682    def onSelectDownloaded(self,event):
4683        '''Select a previously downloaded tutorial
4684        '''
4685        indices = [j for j,i in enumerate(tutorialCatalog)
4686            if os.path.exists(os.path.join(self.tutorialPath,i[0],i[1]))]
4687        if not indices:
4688            G2MessageBox(self,
4689                         'There are no downloaded tutorials in '+self.tutorialPath,
4690                         'None downloaded')
4691            return
4692        choices = [tutorialCatalog[i][2] for i in indices]
4693        selected = self.ChooseTutorial(choices)
4694        if selected is None: return
4695        j = indices[selected]
4696        fullpath = os.path.join(self.tutorialPath,tutorialCatalog[j][0],tutorialCatalog[j][1])
4697        self.EndModal(wx.ID_OK)
4698        ShowWebPage(fullpath,self.frame)
4699        self.G2frame.TutorialImportDir = os.path.join(self.tutorialPath,tutorialCatalog[j][0],'data')
4700       
4701    def onWebBrowse(self,event):
4702        '''Make a list of all tutorials on web and allow user to view one.
4703        '''
4704        choices = [i[2] for i in tutorialCatalog]
4705        selected = self.ChooseTutorial(choices)
4706        if selected is None: return       
4707        tutdir = tutorialCatalog[selected][0]
4708        tutfil = tutorialCatalog[selected][1]
4709        # open web page remotely, don't worry about data
4710        URL = G2BaseURL+'/Tutorials/'+tutdir+'/'+tutfil
4711        self.EndModal(wx.ID_OK)
4712        ShowWebPage(URL,self.frame)
4713       
4714    def ChooseTutorial(self,choices):
4715        'choose a tutorial from a list'
4716        def onDoubleClick(event):
4717            'double-click closes the dialog'
4718            dlg.EndModal(wx.ID_OK)
4719        dlg = wx.Dialog(self,wx.ID_ANY,
4720                        'Select a tutorial to view. NB: indented ones require prerequisite',
4721                        style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)
4722        pnl = wx.Panel(dlg)
4723        sizer = wx.BoxSizer(wx.VERTICAL)
4724        listbox = wx.ListBox(pnl, wx.ID_ANY, choices=choices,
4725                             size=(450, 100),
4726                             style=wx.LB_SINGLE)
4727        sizer.Add(listbox,1,WACV|wx.EXPAND|wx.ALL,1)
4728        listbox.Bind(wx.EVT_LISTBOX_DCLICK, onDoubleClick)
4729        sizer.Add((10,10))
4730        btnsizer = wx.StdDialogButtonSizer()
4731        btn = wx.Button(pnl, wx.ID_CANCEL)
4732        btnsizer.AddButton(btn)
4733        OKbtn = wx.Button(pnl, wx.ID_OK)
4734        OKbtn.SetDefault()
4735        btnsizer.AddButton(OKbtn)
4736        btnsizer.Realize()
4737        sizer.Add((-1,5))
4738        sizer.Add(btnsizer,0,wx.ALIGN_RIGHT,50)
4739       
4740        pnl.SetSizer(sizer)
4741        sizer.Fit(dlg)
4742        self.CenterOnParent()
4743        if dlg.ShowModal() != wx.ID_OK:
4744            dlg.Destroy()
4745            return
4746        selected = listbox.GetSelection()
4747        dlg.Destroy()
4748        wx.Yield() # close window right away so user sees something happen
4749        if selected < 0: return
4750        return selected
4751
4752    def UpdateDownloaded(self,event):
4753        'Find the downloaded tutorials and run an svn update on them'
4754        updated = 0
4755        for i in tutorialCatalog:
4756            if not os.path.exists(os.path.join(self.tutorialPath,i[0],i[1])): continue
4757            print('Updating '+i[0])
4758            GSASIIpath.svnUpdateDir(os.path.join(self.tutorialPath,i[0]))
4759            updated += 0
4760        if not updated:
4761            G2MessageBox(self,'Warning, you have no downloaded tutorials','None Downloaded')
4762        self.EndModal(wx.ID_OK)
4763       
4764    def DownloadAll(self,event):
4765        'Download or update all tutorials'
4766        fail = ''
4767        for i in tutorialCatalog:
4768            if os.path.exists(os.path.join(self.tutorialPath,i[0],i[1])):
4769                print('Updating '+i[0])
4770                GSASIIpath.svnUpdateDir(os.path.join(self.tutorialPath,i[0]))
4771            else:
4772                fulldir = os.path.join(self.tutorialPath,i[0])
4773                URL = G2BaseURL+'/Tutorials/'+i[0]+'/'
4774                if not GSASIIpath.svnInstallDir(URL,fulldir):
4775                    if fail: fail += ', '
4776                    fail += i[0]
4777        if fail: 
4778            G2MessageBox(self,'Error downloading tutorial(s)\n\t'+fail,'Download error')
4779        self.EndModal(wx.ID_OK)
4780                   
4781    def SelectDownloadLoc(self,event):
4782        '''Select a download location,
4783        Cancel resets to the default
4784        '''
4785        dlg = wx.DirDialog(self, "Choose a directory for tutorial downloads:",
4786                           defaultPath=self.tutorialPath)#,style=wx.DD_DEFAULT_STYLE)
4787                           #)
4788        try:
4789            if dlg.ShowModal() != wx.ID_OK:
4790                return
4791            pth = dlg.GetPath()
4792        finally:
4793            dlg.Destroy()
4794
4795        if not os.path.exists(pth):
4796            try:
4797                os.makedirs(pth)    #failing for no obvious reason
4798            except OSError:
4799                msg = 'The selected directory is not valid.\n\t'
4800                msg += pth
4801                msg += '\n\nAn attempt to create the directory failed'
4802                G2MessageBox(self.frame,msg)
4803                return
4804        if os.path.exists(os.path.join(pth,"help")) and os.path.exists(os.path.join(pth,"Exercises")):
4805            print("Note that you may have old tutorial files in the following directories")
4806            print('\t'+os.path.join(pth,"help"))
4807            print('\t'+os.path.join(pth,"Exercises"))
4808            print('Subdirectories in the above can be deleted to save space\n\n')
4809        self.tutorialPath = pth
4810        self.dataLoc.SetLabel(self.tutorialPath)
4811        if GSASIIpath.GetConfigValue('Tutorial_location') == pth: return
4812        vars = GetConfigValsDocs()
4813        try:
4814            vars['Tutorial_location'][1] = pth
4815            if GSASIIpath.GetConfigValue('debug'): print('Saving Tutorial_location: '+pth)
4816            GSASIIpath.SetConfigValue(vars)
4817            SaveConfigVars(vars)
4818        except KeyError:
4819            pass
4820           
4821if __name__ == '__main__':
4822    app = wx.PySimpleApp()
4823    GSASIIpath.InvokeDebugOpts()
4824    frm = wx.Frame(None) # create a frame
4825    frm.Show(True)
4826   
4827    #======================================================================
4828    # test Grid with GridFractionEditor
4829    #======================================================================
4830    # tbl = [[1.,2.,3.],[1.1,2.1,3.1]]
4831    # colTypes = 3*[wg.GRID_VALUE_FLOAT+':10,5',]
4832    # Gtbl = Table(tbl,types=colTypes,rowLabels=['a','b'],colLabels=['1','2','3'])
4833    # Grid = GSGrid(frm)
4834    # Grid.SetTable(Gtbl,True)
4835    # for i in (0,1,2):
4836    #     attr = wx.grid.GridCellAttr()
4837    #     attr.IncRef()
4838    #     attr.SetEditor(GridFractionEditor(Grid))
4839    #     Grid.SetColAttr(i, attr)
4840    # frm.SetSize((400,200))
4841    # app.MainLoop()
4842    # sys.exit()
4843    #======================================================================
4844    # test Tutorial access
4845    #======================================================================
4846    # dlg = OpenTutorial(frm)
4847    # if dlg.ShowModal() == wx.ID_OK:
4848    #     print "OK"
4849    # else:
4850    #     print "Cancel"
4851    # dlg.Destroy()
4852    # sys.exit()
4853    #======================================================================
4854    # test ScrolledMultiEditor
4855    #======================================================================
4856    # Data1 = {
4857    #      'Order':1,
4858    #      'omega':'string',
4859    #      'chi':2.0,
4860    #      'phi':'',
4861    #      }
4862    # elemlst = sorted(Data1.keys())
4863    # prelbl = sorted(Data1.keys())
4864    # dictlst = len(elemlst)*[Data1,]
4865    #Data2 = [True,False,False,True]
4866    #Checkdictlst = len(Data2)*[Data2,]
4867    #Checkelemlst = range(len(Checkdictlst))
4868    # print 'before',Data1,'\n',Data2
4869    # dlg = ScrolledMultiEditor(
4870    #     frm,dictlst,elemlst,prelbl,
4871    #     checkdictlst=Checkdictlst,checkelemlst=Checkelemlst,
4872    #     checklabel="Refine?",
4873    #     header="test")
4874    # if dlg.ShowModal() == wx.ID_OK:
4875    #     print "OK"
4876    # else:
4877    #     print "Cancel"
4878    # print 'after',Data1,'\n',Data2
4879    # dlg.Destroy()
4880    Data3 = {
4881         'Order':1.0,
4882         'omega':1.1,
4883         'chi':2.0,
4884         'phi':2.3,
4885         'Order1':1.0,
4886         'omega1':1.1,
4887         'chi1':2.0,
4888         'phi1':2.3,
4889         'Order2':1.0,
4890         'omega2':1.1,
4891         'chi2':2.0,
4892         'phi2':2.3,
4893         }
4894    elemlst = sorted(Data3.keys())
4895    dictlst = len(elemlst)*[Data3,]
4896    prelbl = elemlst[:]
4897    prelbl[0]="this is a much longer label to stretch things out"
4898    Data2 = len(elemlst)*[False,]
4899    Data2[1] = Data2[3] = True
4900    Checkdictlst = len(elemlst)*[Data2,]
4901    Checkelemlst = range(len(Checkdictlst))
4902    #print 'before',Data3,'\n',Data2
4903    #print dictlst,"\n",elemlst
4904    #print Checkdictlst,"\n",Checkelemlst
4905    # dlg = ScrolledMultiEditor(
4906    #     frm,dictlst,elemlst,prelbl,
4907    #     checkdictlst=Checkdictlst,checkelemlst=Checkelemlst,
4908    #     checklabel="Refine?",
4909    #     header="test",CopyButton=True)
4910    # if dlg.ShowModal() == wx.ID_OK:
4911    #     print "OK"
4912    # else:
4913    #     print "Cancel"
4914    #print 'after',Data3,'\n',Data2
4915
4916    # Data2 = list(range(100))
4917    # elemlst += range(2,6)
4918    # postlbl += range(2,6)
4919    # dictlst += len(range(2,6))*[Data2,]
4920
4921    # prelbl = range(len(elemlst))
4922    # postlbl[1] = "a very long label for the 2nd item to force a horiz. scrollbar"
4923    # header="""This is a longer\nmultiline and perhaps silly header"""
4924    # dlg = ScrolledMultiEditor(frm,dictlst,elemlst,prelbl,postlbl,
4925    #                           header=header,CopyButton=True)
4926    # print Data1
4927    # if dlg.ShowModal() == wx.ID_OK:
4928    #     for d,k in zip(dictlst,elemlst):
4929    #         print k,d[k]
4930    # dlg.Destroy()
4931    # if CallScrolledMultiEditor(frm,dictlst,elemlst,prelbl,postlbl,
4932    #                            header=header):
4933    #     for d,k in zip(dictlst,elemlst):
4934    #         print k,d[k]
4935
4936    #======================================================================
4937    # test G2MultiChoiceDialog
4938    #======================================================================
4939    choices = []
4940    for i in range(21):
4941        choices.append("option_"+str(i))
4942    od = {
4943        'label_1':'This is a bool','value_1':True,
4944        'label_2':'This is a int','value_2':-1,
4945        'label_3':'This is a float','value_3':1.0,
4946        'label_4':'This is a string','value_4':'test',}
4947    dlg = G2MultiChoiceDialog(frm, 'Sequential refinement',
4948                              'Select dataset to include',
4949                              choices,extraOpts=od)
4950    sel = range(2,11,2)
4951    dlg.SetSelections(sel)
4952    dlg.SetSelections((1,5))
4953    if dlg.ShowModal() == wx.ID_OK:
4954        for sel in dlg.GetSelections():
4955            print (sel,choices[sel])
4956    print (od)
4957    od = {}
4958    dlg = G2MultiChoiceDialog(frm, 'Sequential refinement',
4959                              'Select dataset to include',
4960                              choices,extraOpts=od)
4961    sel = range(2,11,2)
4962    dlg.SetSelections(sel)
4963    dlg.SetSelections((1,5))
4964    if dlg.ShowModal() == wx.ID_OK: pass
4965    #======================================================================
4966    # test wx.MultiChoiceDialog
4967    #======================================================================
4968    # dlg = wx.MultiChoiceDialog(frm, 'Sequential refinement',
4969    #                           'Select dataset to include',
4970    #                           choices)
4971    # sel = range(2,11,2)
4972    # dlg.SetSelections(sel)
4973    # dlg.SetSelections((1,5))
4974    # if dlg.ShowModal() == wx.ID_OK:
4975    #     for sel in dlg.GetSelections():
4976    #         print sel,choices[sel]
4977
4978    # pnl = wx.Panel(frm)
4979    # siz = wx.BoxSizer(wx.VERTICAL)
4980
4981    # td = {'Goni':200.,'a':1.,'calc':1./3.,'string':'s'}
4982    # for key in sorted(td):
4983    #     txt = ValidatedTxtCtrl(pnl,td,key)
4984    #     siz.Add(txt)
4985    # pnl.SetSizer(siz)
4986    # siz.Fit(frm)
4987    # app.MainLoop()
4988    # print td