source: trunk/GSASIIctrlGUI.py @ 3151

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

male all default image pixel sizes [200.,200.] so ValidatedTxtCtrl? treats them as floats.
Fix crash when data is deleted from tree.

  • 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-06 22:32:17 +0000 (Mon, 06 Nov 2017) $
5# $Author: vondreele $
6# $Revision: 3151 $
7# $URL: trunk/GSASIIctrlGUI.py $
8# $Id: GSASIIctrlGUI.py 3151 2017-11-06 22:32:17Z vondreele $
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: 3151 $")
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 isinstance(val,int) 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 isinstance(val,float) 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        info = wx.AboutDialogInfo()
3680        info.Name = 'GSAS-II'
3681        ver = GSASIIpath.svnGetRev()
3682        if ver: 
3683            info.Version = 'Revision '+str(ver)+' (svn), version '+GSASII.__version__
3684        else:
3685            info.Version = 'Revision '+str(GSASIIpath.GetVersionNumber())+' (.py files), version '+GSASII.__version__
3686        #info.Developers = ['Robert B. Von Dreele','Brian H. Toby']
3687        info.Copyright = ('(c) ' + time.strftime('%Y') +
3688''' Argonne National Laboratory
3689This product includes software developed
3690by the UChicago Argonne, LLC, as
3691Operator of Argonne National Laboratory.''')
3692        info.Description = '''General Structure Analysis System-II (GSAS-II)
3693Robert B. Von Dreele and Brian H. Toby
3694
3695Please cite as:
3696  B.H. Toby & R.B. Von Dreele, J. Appl. Cryst. 46, 544-549 (2013)
3697For small angle use cite:
3698  R.B. Von Dreele, J. Appl. Cryst. 47, 1748-9 (2014)
3699For DIFFaX use cite:
3700  M.M.J. Treacy, J.M. Newsam & M.W. Deem,
3701  Proc. Roy. Soc. Lond. A 433, 499-520 (1991)
3702'''
3703        info.WebSite = ("https://subversion.xray.aps.anl.gov/trac/pyGSAS","GSAS-II home page")
3704        wx.AboutBox(info)
3705
3706    def OnCheckUpdates(self,event):
3707        '''Check if the GSAS-II repository has an update for the current source files
3708        and perform that update if requested.
3709        '''           
3710        if not GSASIIpath.whichsvn():
3711            dlg = wx.MessageDialog(self.frame,
3712                                   'No Subversion','Cannot update GSAS-II because subversion (svn) was not found.',
3713                                   wx.OK)
3714            dlg.ShowModal()
3715            dlg.Destroy()
3716            return
3717        wx.BeginBusyCursor()
3718        local = GSASIIpath.svnGetRev()
3719        if local is None: 
3720            wx.EndBusyCursor()
3721            dlg = wx.MessageDialog(self.frame,
3722                                   'Unable to run subversion on the GSAS-II current directory. Is GSAS-II installed correctly?',
3723                                   'Subversion error',
3724                                   wx.OK)
3725            dlg.ShowModal()
3726            dlg.Destroy()
3727            return
3728        print ('Installed GSAS-II version: '+local)
3729        repos = GSASIIpath.svnGetRev(local=False)
3730        wx.EndBusyCursor()
3731        # has the current branch disappeared? If so, switch to the trunk -- not fully tested
3732        if (repos is None and "not found" in GSASIIpath.svnLastError.lower()
3733            and "path" in GSASIIpath.svnLastError.lower()):
3734            print('Repository is gone, will switch to trunk')
3735            GSASIIpath.svnSwitch2branch()
3736            return
3737        elif repos is None: 
3738            dlg = wx.MessageDialog(self.frame,
3739                                   'Unable to access the GSAS-II server. Is this computer on the internet?',
3740                                   'Server unavailable',
3741                                   wx.OK)
3742            dlg.ShowModal()
3743            dlg.Destroy()
3744            return
3745        print ('GSAS-II version on server: '+repos)
3746        if local == repos:
3747            dlg = wx.MessageDialog(self.frame,
3748                                   'GSAS-II is up-to-date. Version '+local+' is already loaded.',
3749                                   'GSAS-II Up-to-date',
3750                                   wx.OK)
3751            dlg.ShowModal()
3752            dlg.Destroy()
3753            return
3754        mods = GSASIIpath.svnFindLocalChanges()
3755        if mods:
3756            dlg = wx.MessageDialog(self.frame,
3757                                   'You have version '+local+
3758                                   ' of GSAS-II installed, but the current version is '+repos+
3759                                   '. However, '+str(len(mods))+
3760                                   ' file(s) on your local computer have been modified.'
3761                                   ' Updating will attempt to merge your local changes with '
3762                                   'the latest GSAS-II version, but if '
3763                                   'conflicts arise, local changes will be '
3764                                   'discarded. It is also possible that the '
3765                                   'local changes my prevent GSAS-II from running. '
3766                                   'Press OK to start an update if this is acceptable:',
3767                                   'Local GSAS-II Mods',
3768                                   wx.OK|wx.CANCEL)
3769            if dlg.ShowModal() != wx.ID_OK:
3770                dlg.Destroy()
3771                return
3772            else:
3773                dlg.Destroy()
3774        else:
3775            dlg = wx.MessageDialog(self.frame,
3776                                   'You have version '+local+
3777                                   ' of GSAS-II installed, but the current version is '+repos+
3778                                   '. Press OK to start an update:',
3779                                   'GSAS-II Updates',
3780                                   wx.OK|wx.CANCEL)
3781            if dlg.ShowModal() != wx.ID_OK:
3782                dlg.Destroy()
3783                return
3784            dlg.Destroy()
3785        print ('start updates')
3786        dlg = wx.MessageDialog(self.frame,
3787                               'Your project will now be saved, GSAS-II will exit and an update '
3788                               'will be performed and GSAS-II will restart. Press Cancel to '
3789                               'abort the update',
3790                               'Start update?',
3791                               wx.OK|wx.CANCEL)
3792        if dlg.ShowModal() != wx.ID_OK:
3793            dlg.Destroy()
3794            return
3795        dlg.Destroy()
3796        self.frame.OnFileSave(event)
3797        GPX = self.frame.GSASprojectfile
3798        GSASIIpath.svnUpdateProcess(projectfile=GPX)
3799        return
3800
3801    def OnSelectVersion(self,event):
3802        '''Allow the user to select a specific version of GSAS-II
3803        '''
3804        if not GSASIIpath.whichsvn():
3805            dlg = wx.MessageDialog(self,'No Subversion','Cannot update GSAS-II because subversion (svn) '+
3806                                   'was not found.'
3807                                   ,wx.OK)
3808            dlg.ShowModal()
3809            return
3810        local = GSASIIpath.svnGetRev()
3811        if local is None: 
3812            dlg = wx.MessageDialog(self.frame,
3813                                   'Unable to run subversion on the GSAS-II current directory. Is GSAS-II installed correctly?',
3814                                   'Subversion error',
3815                                   wx.OK)
3816            dlg.ShowModal()
3817            dlg.Destroy()
3818            return
3819        mods = GSASIIpath.svnFindLocalChanges()
3820        if mods:
3821            dlg = wx.MessageDialog(self.frame,
3822                                   'You have version '+local+
3823                                   ' of GSAS-II installed'
3824                                   '. However, '+str(len(mods))+
3825                                   ' file(s) on your local computer have been modified.'
3826                                   ' Downdating will attempt to merge your local changes with '
3827                                   'the selected GSAS-II version. '
3828                                   'Downdating is not encouraged because '
3829                                   'if merging is not possible, your local changes will be '
3830                                   'discarded. It is also possible that the '
3831                                   'local changes my prevent GSAS-II from running. '
3832                                   'Press OK to continue anyway.',
3833                                   'Local GSAS-II Mods',
3834                                   wx.OK|wx.CANCEL)
3835            if dlg.ShowModal() != wx.ID_OK:
3836                dlg.Destroy()
3837                return
3838            dlg.Destroy()
3839        if GSASIIpath.svnGetRev(local=False) is None:
3840            dlg = wx.MessageDialog(self.frame,
3841                                   'Error obtaining current GSAS-II version. Is internet access working correctly?',
3842                                   'Subversion error',
3843                                   wx.OK)
3844            dlg.ShowModal()
3845            dlg.Destroy()
3846            return
3847        dlg = downdate(parent=self.frame)
3848        if dlg.ShowModal() == wx.ID_OK:
3849            ver = dlg.getVersion()
3850        else:
3851            dlg.Destroy()
3852            return
3853        dlg.Destroy()
3854        print('start regress to '+str(ver))
3855        self.frame.OnFileSave(event)
3856        GPX = self.frame.GSASprojectfile
3857        GSASIIpath.svnUpdateProcess(projectfile=GPX,version=str(ver))
3858        return
3859
3860    # def OnSelectBranch(self,event):
3861    #     '''Allow the user to select branch of GSAS-II or return to trunk
3862    #     N.B. Name of branch to use is hard-coded here. Must contain a slash
3863    #     '''
3864    #     testbranch = '/branch/2frame'
3865    #     if not GSASIIpath.svnTestBranch():
3866    #         dlg = wx.MessageDialog(self.frame,
3867    #                                'Switching to test GSAS-II version',
3868    #                                'Confirm Switch',
3869    #                                wx.OK|wx.CANCEL)
3870    #         if dlg.ShowModal() != wx.ID_OK: return
3871    #         branch = testbranch
3872    #     else:
3873    #         dlg = wx.MessageDialog(self.frame,
3874    #                                'Switching back to standard GSAS-II version',
3875    #                                'Confirm Switch',
3876    #                                wx.OK|wx.CANCEL)
3877    #         if dlg.ShowModal() != wx.ID_OK: return
3878    #         branch = 'trunk'
3879    #     print('start switch')
3880    #     self.frame.OnFileSave(event)
3881    #     GPX = self.frame.GSASprojectfile
3882    #     GSASIIpath.svnUpdateProcess(projectfile=GPX,branch=branch)
3883
3884################################################################################
3885class HelpButton(wx.Button):
3886    '''Create a help button that displays help information.
3887    The text is displayed in a modal message window.
3888
3889    TODO: it might be nice if it were non-modal: e.g. it stays around until
3890    the parent is deleted or the user closes it, but this did not work for
3891    me.
3892
3893    :param parent: the panel which will be the parent of the button
3894    :param str msg: the help text to be displayed
3895    '''
3896    def __init__(self,parent,msg):
3897        if sys.platform == "darwin": 
3898            wx.Button.__init__(self,parent,wx.ID_HELP)
3899        else:
3900            wx.Button.__init__(self,parent,wx.ID_ANY,'?',style=wx.BU_EXACTFIT)
3901        self.Bind(wx.EVT_BUTTON,self._onPress)
3902        self.msg=StripIndents(msg)
3903        self.parent = parent
3904    def _onClose(self,event):
3905        self.dlg.EndModal(wx.ID_CANCEL)
3906    def _onPress(self,event):
3907        'Respond to a button press by displaying the requested text'
3908        #dlg = wx.MessageDialog(self.parent,self.msg,'Help info',wx.OK)
3909        self.dlg = wx.Dialog(self.parent,wx.ID_ANY,'Help information', 
3910                        style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)
3911        #self.dlg.SetBackgroundColour(wx.WHITE)
3912        mainSizer = wx.BoxSizer(wx.VERTICAL)
3913        txt = wx.StaticText(self.dlg,wx.ID_ANY,self.msg)
3914        mainSizer.Add(txt,1,wx.ALL|wx.EXPAND,10)
3915        txt.SetBackgroundColour(wx.WHITE)
3916
3917        btnsizer = wx.BoxSizer(wx.HORIZONTAL)
3918        btn = wx.Button(self.dlg, wx.ID_CLOSE) 
3919        btn.Bind(wx.EVT_BUTTON,self._onClose)
3920        btnsizer.Add(btn)
3921        mainSizer.Add(btnsizer, 0, wx.ALIGN_CENTER|wx.ALL, 5)
3922        self.dlg.SetSizer(mainSizer)
3923        mainSizer.Fit(self.dlg)
3924        self.dlg.CenterOnParent()
3925        self.dlg.ShowModal()
3926        self.dlg.Destroy()
3927################################################################################
3928class MyHtmlPanel(wx.Panel):
3929    '''Defines a panel to display HTML help information, as an alternative to
3930    displaying help information in a web browser.
3931    '''
3932    def __init__(self, frame, id):
3933        self.frame = frame
3934        wx.Panel.__init__(self, frame, id)
3935        sizer = wx.BoxSizer(wx.VERTICAL)
3936        back = wx.Button(self, -1, "Back")
3937        back.Bind(wx.EVT_BUTTON, self.OnBack)
3938        self.htmlwin = G2HtmlWindow(self, id, size=(750,450))
3939        sizer.Add(self.htmlwin, 1,wx.EXPAND)
3940        sizer.Add(back, 0, wx.ALIGN_LEFT, 0)
3941        self.SetSizer(sizer)
3942        sizer.Fit(frame)       
3943        self.Bind(wx.EVT_SIZE,self.OnHelpSize)
3944    def OnHelpSize(self,event):         #does the job but weirdly!!
3945        anchor = self.htmlwin.GetOpenedAnchor()
3946        if anchor:           
3947            self.htmlwin.ScrollToAnchor(anchor)
3948            wx.CallAfter(self.htmlwin.ScrollToAnchor,anchor)
3949            if event: event.Skip()
3950    def OnBack(self, event):
3951        self.htmlwin.HistoryBack()
3952    def LoadFile(self,file):
3953        pos = file.rfind('#')
3954        if pos != -1:
3955            helpfile = file[:pos]
3956            helpanchor = file[pos+1:]
3957        else:
3958            helpfile = file
3959            helpanchor = None
3960        self.htmlwin.LoadPage(helpfile)
3961        if helpanchor is not None:
3962            self.htmlwin.ScrollToAnchor(helpanchor)
3963            xs,ys = self.htmlwin.GetViewStart()
3964            self.htmlwin.Scroll(xs,ys-1)
3965################################################################################
3966class G2HtmlWindow(wx.html.HtmlWindow):
3967    '''Displays help information in a primitive HTML browser type window
3968    '''
3969    def __init__(self, parent, *args, **kwargs):
3970        self.parent = parent
3971        wx.html.HtmlWindow.__init__(self, parent, *args, **kwargs)
3972    def LoadPage(self, *args, **kwargs):
3973        wx.html.HtmlWindow.LoadPage(self, *args, **kwargs)
3974        self.TitlePage()
3975    def OnLinkClicked(self, *args, **kwargs):
3976        wx.html.HtmlWindow.OnLinkClicked(self, *args, **kwargs)
3977        xs,ys = self.GetViewStart()
3978        self.Scroll(xs,ys-1)
3979        self.TitlePage()
3980    def HistoryBack(self, *args, **kwargs):
3981        wx.html.HtmlWindow.HistoryBack(self, *args, **kwargs)
3982        self.TitlePage()
3983    def TitlePage(self):
3984        self.parent.frame.SetTitle(self.GetOpenedPage() + ' -- ' + 
3985            self.GetOpenedPageTitle())
3986
3987################################################################################
3988def StripIndents(msg):
3989    'Strip indentation from multiline strings'
3990    msg1 = msg.replace('\n ','\n')
3991    while msg != msg1:
3992        msg = msg1
3993        msg1 = msg.replace('\n ','\n')
3994    return msg.replace('\n\t','\n')
3995
3996def StripUnicode(string,subs='.'):
3997    '''Strip non-ASCII characters from strings
3998   
3999    :param str string: string to strip Unicode characters from
4000    :param str subs: character(s) to place into string in place of each
4001      Unicode character. Defaults to '.'
4002
4003    :returns: a new string with only ASCII characters
4004    '''
4005    s = ''
4006    for c in string:
4007        if ord(c) < 128:
4008            s += c
4009        else:
4010            s += subs
4011    return s.encode('ascii','replace')
4012       
4013################################################################################
4014# configuration routines (for editing config.py)
4015def SaveGPXdirectory(path):
4016    if GSASIIpath.GetConfigValue('Starting_directory') == path: return
4017    vars = GetConfigValsDocs()
4018    try:
4019        vars['Starting_directory'][1] = path
4020        if GSASIIpath.GetConfigValue('debug'): print('Saving GPX path: '+path)
4021        SaveConfigVars(vars)
4022    except KeyError:
4023        pass
4024
4025def SaveImportDirectory(path):
4026    if GSASIIpath.GetConfigValue('Import_directory') == path: return
4027    vars = GetConfigValsDocs()
4028    try:
4029        vars['Import_directory'][1] = path
4030        if GSASIIpath.GetConfigValue('debug'): print('Saving Import path: '+path)
4031        SaveConfigVars(vars)
4032    except KeyError:
4033        pass
4034
4035def GetConfigValsDocs():
4036    '''Reads the module referenced in fname (often <module>.__file__) and
4037    return a dict with names of global variables as keys.
4038    For each global variable, the value contains four items:
4039
4040    :returns: a dict where keys are names defined in module config_example.py
4041      where the value is a list of four items, as follows:
4042
4043         * item 0: the default value
4044         * item 1: the current value
4045         * item 2: the initial value (starts same as item 1)
4046         * item 3: the "docstring" that follows variable definition
4047
4048    '''
4049    import config_example
4050    import ast
4051    fname = os.path.splitext(config_example.__file__)[0]+'.py' # convert .pyc to .py
4052    with open(fname, 'r') as f:
4053        fstr = f.read()
4054    fstr = fstr.replace('\r\n', '\n').replace('\r', '\n')
4055    if not fstr.endswith('\n'):
4056        fstr += '\n'
4057    tree = ast.parse(fstr)
4058    d = {}
4059    key = None
4060    for node in ast.walk(tree):
4061        if isinstance(node,ast.Assign):
4062            key = node.targets[0].id
4063            d[key] = [config_example.__dict__.get(key),
4064                      GSASIIpath.configDict.get(key),
4065                      GSASIIpath.configDict.get(key),'']
4066        elif isinstance(node,ast.Expr) and key:
4067            d[key][3] = node.value.s.strip()
4068        else:
4069            key = None
4070    return d
4071
4072def SaveConfigVars(vars,parent=None):
4073    '''Write the current config variable values to config.py
4074
4075    :params dict vars: a dictionary of variable settings and meanings as
4076      created in :func:`GetConfigValsDocs`.
4077    :param parent: wx.Frame object or None (default) for parent
4078      of error message if no file can be written.
4079    :returns: True if unable to write the file, None otherwise
4080    '''
4081    # try to write to where an old config file is located
4082    try:
4083        import config
4084        savefile = config.__file__
4085    except ImportError: # no config.py file yet
4086        savefile = os.path.join(GSASIIpath.path2GSAS2,'config.py')
4087    # try to open file for write
4088    try:
4089        savefile = os.path.splitext(savefile)[0]+'.py' # convert .pyc to .py
4090        fp = open(savefile,'w')
4091    except IOError:  # can't write there, write in local mods directory
4092        # create a local mods directory, if needed
4093        g2local = os.path.expanduser('~/.G2local/')
4094        if not os.path.exists(g2local):
4095            try:
4096                print(u'Creating directory '+g2local)
4097                os.mkdir(g2local)
4098            except:
4099                if parent:
4100                    G2MessageBox(parent,u'Error trying to create directory '+g2local,
4101                        'Unable to save')
4102                else:
4103                    print(u'Error trying to create directory '+g2local)
4104                return True
4105            sys.path.insert(0,os.path.expanduser('~/.G2local/'))
4106        savefile = os.path.join(os.path.expanduser('~/.G2local/'),'config.py')
4107        try:
4108            fp = open(savefile,'w')
4109        except IOError:
4110            if parent:
4111                G2MessageBox(parent,'Error trying to write configuration to '+savefile,
4112                    'Unable to save')
4113            else:
4114                print('Error trying to write configuration to '+savefile)
4115            return True
4116    import datetime
4117    fp.write("'''\n")
4118    fp.write("*config.py: Configuration options*\n----------------------------------\n")
4119    fp.write("This file created in SelectConfigSetting on {:%d %b %Y %H:%M}\n".
4120             format(datetime.datetime.now()))
4121    fp.write("'''\n\n")
4122    fp.write("import os.path\n")
4123    fp.write("import GSASIIpath\n\n")
4124    for var in sorted(vars.keys(),key=lambda s: s.lower()):
4125        if vars[var][1] is None: continue
4126        if vars[var][1] == '': continue
4127        if vars[var][0] == vars[var][1]: continue
4128        try:
4129            float(vars[var][1]) # test for number
4130            fp.write(var + ' = ' + str(vars[var][1])+'\n')
4131        except:
4132            try:
4133                eval(vars[var][1]) # test for an expression
4134                fp.write(var + ' = ' + str(vars[var][1])+'\n')
4135            except: # must be a string
4136                varstr = vars[var][1]
4137                if '\\' in varstr:
4138                    fp.write(var + ' = os.path.normpath("' + varstr.replace('\\','/') +'")\n')
4139                else:
4140                    fp.write(var + ' = "' + str(varstr)+'"\n')
4141        if vars[var][3]:
4142            fp.write("'''" + str(vars[var][3]) + "\n'''\n\n")
4143    fp.close()
4144    print('wrote file '+savefile)
4145
4146class SelectConfigSetting(wx.Dialog):
4147    '''Dialog to select configuration variables and set associated values.
4148    '''
4149    def __init__(self,parent=None):
4150        style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER
4151        wx.Dialog.__init__(self, parent, wx.ID_ANY, 'Set Config Variable', style=style)
4152        self.sizer = wx.BoxSizer(wx.VERTICAL)
4153        self.vars = GetConfigValsDocs()
4154       
4155        label = wx.StaticText(
4156            self,  wx.ID_ANY,
4157            'Select a GSAS-II configuration variable to change'
4158            )
4159        self.sizer.Add(label, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
4160        self.choice = {}
4161        btn = G2ChoiceButton(self, sorted(self.vars.keys(),key=lambda s: s.lower()),
4162            strLoc=self.choice,strKey=0,onChoice=self.OnSelection)
4163        btn.SetLabel("")
4164        self.sizer.Add(btn)
4165
4166        self.varsizer = wx.BoxSizer(wx.VERTICAL)
4167        self.sizer.Add(self.varsizer,1,wx.ALL|wx.EXPAND,1)
4168       
4169        self.doclbl = wx.StaticBox(self, wx.ID_ANY, "")
4170        self.doclblsizr = wx.StaticBoxSizer(self.doclbl)
4171        self.docinfo = wx.StaticText(self,  wx.ID_ANY, "")
4172        self.doclblsizr.Add(self.docinfo, 0, wx.ALIGN_LEFT|wx.ALL, 5)
4173        self.sizer.Add(self.doclblsizr, 0, wx.ALIGN_LEFT|wx.EXPAND|wx.ALL, 5)
4174        btnsizer = wx.BoxSizer(wx.HORIZONTAL)
4175        self.saveBtn = wx.Button(self,-1,"Save current settings")
4176        btnsizer.Add(self.saveBtn, 0, wx.ALL, 2) 
4177        self.saveBtn.Bind(wx.EVT_BUTTON, self.OnSave)
4178        self.saveBtn.Enable(False)
4179        self.applyBtn = wx.Button(self,-1,"Use current (no save)")
4180        btnsizer.Add(self.applyBtn, 0, wx.ALL, 2) 
4181        self.applyBtn.Bind(wx.EVT_BUTTON, self.OnApplyChanges)
4182        self.applyBtn.Enable(False)
4183       
4184        btn = wx.Button(self,wx.ID_CANCEL)
4185        btnsizer.Add(btn, 0, wx.ALL, 2) 
4186        self.sizer.Add(btnsizer, 0, wx.ALIGN_CENTRE|wx.ALL, 5) 
4187               
4188        self.SetSizer(self.sizer)
4189        self.sizer.Fit(self)
4190        self.CenterOnParent()
4191       
4192    def OnChange(self,event=None):
4193        ''' Check if anything been changed. Turn the save button on/off.
4194        '''
4195        for var in self.vars:
4196            if self.vars[var][0] is None and self.vars[var][1] is not None:
4197                # make blank strings into None, if that is the default
4198                if self.vars[var][1].strip() == '': self.vars[var][1] = None
4199            if self.vars[var][1] != self.vars[var][2]:
4200                #print 'changed',var,self.vars[var][:3]
4201                self.saveBtn.Enable(True)
4202                self.applyBtn.Enable(True)
4203                break
4204        else:
4205            self.saveBtn.Enable(False)
4206            self.applyBtn.Enable(False)
4207        try:
4208            self.resetBtn.Enable(True)
4209        except:
4210            pass
4211       
4212    def OnApplyChanges(self,event=None):
4213        'Set config variables to match the current settings'
4214        GSASIIpath.SetConfigValue(self.vars)
4215        self.EndModal(wx.ID_OK)
4216        import GSASIImpsubs as G2mp
4217        G2mp.ResetMP()
4218       
4219    def OnSave(self,event):
4220        '''Write the config variables to config.py and then set them
4221        as the current settings
4222        '''
4223        if not SaveConfigVars(self.vars,parent=self):
4224            self.OnApplyChanges() # force a reload of the config settings
4225        else:
4226            self.EndModal(wx.ID_OK)
4227
4228    def OnBoolSelect(self,event):
4229        'Respond to a change in a True/False variable'
4230        rb = event.GetEventObject()
4231        var = self.choice[0]
4232        self.vars[var][1] = (rb.GetSelection() == 0)
4233        self.OnChange()
4234        wx.CallAfter(self.OnSelection)
4235       
4236    def onSelDir(self,event):
4237        'Select a directory from a menu'
4238        dlg = wx.DirDialog(self, "Choose a directory:",style=wx.DD_DEFAULT_STYLE)
4239        if dlg.ShowModal() == wx.ID_OK:
4240            var = self.choice[0]
4241            self.vars[var][1] = dlg.GetPath()
4242            self.strEd.SetValue(self.vars[var][1])
4243            self.OnChange()
4244        dlg.Destroy()
4245       
4246    def OnSelection(self):
4247        'show a selected variable'
4248        def OnNewColorBar(event):
4249            self.vars['Contour_color'][1] = self.colSel.GetValue()
4250            self.OnChange(event)
4251
4252        if 'phoenix' in wx.version():
4253            self.varsizer.Clear(True)
4254        else:
4255            self.varsizer.DeleteWindows()
4256        var = self.choice[0]
4257        showdef = True
4258        if var not in self.vars:
4259            raise Exception("How did this happen?")
4260        if type(self.vars[var][0]) is int:
4261            ed = ValidatedTxtCtrl(self,self.vars[var],1,typeHint=int,OKcontrol=self.OnChange)
4262            self.varsizer.Add(ed, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
4263        elif type(self.vars[var][0]) is float:
4264            ed = ValidatedTxtCtrl(self,self.vars[var],1,typeHint=float,OKcontrol=self.OnChange)
4265            self.varsizer.Add(ed, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
4266        elif type(self.vars[var][0]) is bool:
4267            showdef = False
4268            lbl = "value for "+var
4269            ch = []
4270            for i,v in enumerate((True,False)):
4271                s = str(v)
4272                if v == self.vars[var][0]:
4273                    defopt = i
4274                    s += ' (default)'
4275                ch += [s]
4276            rb = wx.RadioBox(self, wx.ID_ANY, lbl, wx.DefaultPosition, wx.DefaultSize,
4277                ch, 1, wx.RA_SPECIFY_COLS)
4278            # set initial value
4279            if self.vars[var][1] is None:
4280                rb.SetSelection(defopt)
4281            elif self.vars[var][1]:
4282                rb.SetSelection(0)
4283            else:
4284                rb.SetSelection(1)
4285            rb.Bind(wx.EVT_RADIOBOX,self.OnBoolSelect)
4286            self.varsizer.Add(rb, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
4287        else:
4288            if var.endswith('_directory') or var.endswith('_location'):
4289                btn = wx.Button(self,wx.ID_ANY,'Select from dialog...')
4290                sz = (400,-1)
4291            else:
4292                btn = None
4293                sz = (250,-1)
4294            if var == 'Contour_color':
4295                if self.vars[var][1] is None:
4296                    self.vars[var][1] = 'Paired'
4297                colorList = sorted([m for m in mpl.cm.datad.keys() ],key=lambda s: s.lower())   #if not m.endswith("_r")
4298                self.colSel = wx.ComboBox(self,value=self.vars[var][1],choices=colorList,
4299                    style=wx.CB_READONLY|wx.CB_DROPDOWN)
4300                self.colSel.Bind(wx.EVT_COMBOBOX, OnNewColorBar)
4301                self.varsizer.Add(self.colSel, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
4302            else:
4303                self.strEd = ValidatedTxtCtrl(self,self.vars[var],1,typeHint=str,
4304                    OKcontrol=self.OnChange,size=sz)
4305                if self.vars[var][1] is not None:
4306                    self.strEd.SetValue(self.vars[var][1])
4307                self.varsizer.Add(self.strEd, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
4308            if btn:
4309                btn.Bind(wx.EVT_BUTTON,self.onSelDir)
4310                self.varsizer.Add(btn, 0, wx.ALIGN_CENTRE|wx.ALL, 5) 
4311        # button for reset to default value
4312        lbl = "Reset to Default"
4313        if showdef: # spell out default when needed
4314            lbl += ' (='+str(self.vars[var][0])+')'
4315            #label = wx.StaticText(self,  wx.ID_ANY, 'Default value = '+str(self.vars[var][0]))
4316            #self.varsizer.Add(label, 0, wx.ALIGN_LEFT|wx.ALL, 5)
4317        self.resetBtn = wx.Button(self,-1,lbl)
4318        self.resetBtn.Bind(wx.EVT_BUTTON, self.OnClear)
4319        if self.vars[var][1] is not None and self.vars[var][1] != '': # show current value, if one
4320            #label = wx.StaticText(self,  wx.ID_ANY, 'Current value = '+str(self.vars[var][1]))
4321            #self.varsizer.Add(label, 0, wx.ALIGN_LEFT|wx.ALL, 5)
4322            self.resetBtn.Enable(True)
4323        else:
4324            self.resetBtn.Enable(False)
4325        self.varsizer.Add(self.resetBtn, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
4326        # show meaning, if defined
4327        self.doclbl.SetLabel("Description of "+str(var)) 
4328        if self.vars[var][3]:
4329            self.docinfo.SetLabel(self.vars[var][3])
4330        else:
4331            self.docinfo.SetLabel("(not documented)")
4332        self.sizer.Fit(self)
4333        self.CenterOnParent()
4334        wx.CallAfter(self.SendSizeEvent)
4335
4336    def OnClear(self, event):
4337        var = self.choice[0]
4338        self.vars[var][1] = self.vars[var][0]
4339        self.OnChange()
4340        wx.CallAfter(self.OnSelection)
4341       
4342################################################################################
4343class downdate(wx.Dialog):
4344    '''Dialog to allow a user to select a version of GSAS-II to install
4345    '''
4346    def __init__(self,parent=None):
4347        style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER
4348        wx.Dialog.__init__(self, parent, wx.ID_ANY, 'Select Version', style=style)
4349        pnl = wx.Panel(self)
4350        sizer = wx.BoxSizer(wx.VERTICAL)
4351        insver = GSASIIpath.svnGetRev(local=True)
4352        curver = int(GSASIIpath.svnGetRev(local=False))
4353        label = wx.StaticText(
4354            pnl,  wx.ID_ANY,
4355            'Select a specific GSAS-II version to install'
4356            )
4357        sizer.Add(label, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
4358        sizer1 = wx.BoxSizer(wx.HORIZONTAL)
4359        sizer1.Add(
4360            wx.StaticText(pnl,  wx.ID_ANY,
4361                          'Currently installed version: '+str(insver)),
4362            0, wx.ALIGN_CENTRE|wx.ALL, 5)
4363        sizer.Add(sizer1)
4364        sizer1 = wx.BoxSizer(wx.HORIZONTAL)
4365        sizer1.Add(
4366            wx.StaticText(pnl,  wx.ID_ANY,
4367                          'Select GSAS-II version to install: '),
4368            0, wx.ALIGN_CENTRE|wx.ALL, 5)
4369        self.spin = wx.SpinCtrl(pnl, wx.ID_ANY,size=(150,-1))
4370        self.spin.SetRange(1, curver)
4371        self.spin.SetValue(curver)
4372        self.Bind(wx.EVT_SPINCTRL, self._onSpin, self.spin)
4373        self.Bind(wx.EVT_KILL_FOCUS, self._onSpin, self.spin)
4374        sizer1.Add(self.spin)
4375        sizer.Add(sizer1)
4376
4377        line = wx.StaticLine(pnl,-1, size=(-1,3), style=wx.LI_HORIZONTAL)
4378        sizer.Add(line, 0, wx.EXPAND|wx.ALIGN_CENTER|wx.ALL, 10)
4379
4380        self.text = wx.StaticText(pnl,  wx.ID_ANY, "")
4381        sizer.Add(self.text, 0, wx.ALIGN_LEFT|wx.EXPAND|wx.ALL, 5)
4382
4383        line = wx.StaticLine(pnl,-1, size=(-1,3), style=wx.LI_HORIZONTAL)
4384        sizer.Add(line, 0, wx.EXPAND|wx.ALIGN_CENTER|wx.ALL, 10)
4385        sizer.Add(
4386            wx.StaticText(
4387                pnl,  wx.ID_ANY,
4388                'If "Install" is pressed, your project will be saved;\n'
4389                'GSAS-II will exit; The specified version will be loaded\n'
4390                'and GSAS-II will restart. Press "Cancel" to abort.'),
4391            0, wx.EXPAND|wx.ALL, 10)
4392        btnsizer = wx.StdDialogButtonSizer()
4393        btn = wx.Button(pnl, wx.ID_OK, "Install")
4394        btn.SetDefault()
4395        btnsizer.AddButton(btn)
4396        btn = wx.Button(pnl, wx.ID_CANCEL)
4397        btnsizer.AddButton(btn)
4398        btnsizer.Realize()
4399        sizer.Add(btnsizer, 0, wx.ALIGN_CENTER|wx.ALL, 5)
4400        pnl.SetSizer(sizer)
4401        sizer.Fit(self)
4402        self.topsizer=sizer
4403        self.CenterOnParent()
4404        self._onSpin(None)
4405
4406    def _onSpin(self,event):
4407        'Called to load info about the selected version in the dialog'
4408        if event: event.Skip()
4409        ver = self.spin.GetValue()
4410        d = GSASIIpath.svnGetLog(version=ver)
4411        date = d.get('date','?').split('T')[0]
4412        s = '(Version '+str(ver)+' created '+date
4413        s += ' by '+d.get('author','?')+')'
4414        msg = d.get('msg')
4415        if msg: s += '\n\nComment: '+msg
4416        self.text.SetLabel(s)
4417        self.topsizer.Fit(self)
4418
4419    def getVersion(self):
4420        'Get the version number in the dialog'
4421        return self.spin.GetValue()
4422
4423################################################################################
4424#### Display Help information
4425################################################################################
4426# define some globals
4427htmlPanel = None
4428htmlFrame = None
4429htmlFirstUse = True
4430#helpLocDict = {}  # to be implemented if we ever split gsasii.html over multiple files
4431path2GSAS2 = os.path.dirname(os.path.realpath(__file__)) # save location of this file
4432def ShowHelp(helpType,frame):
4433    '''Called to bring up a web page for documentation.'''
4434    global htmlFirstUse,htmlPanel,htmlFrame
4435    # no defined link to use, create a default based on key
4436    helplink = 'gsasII.html'
4437    if helpType:
4438        helplink += '#'+helpType.replace(')','').replace('(','_').replace(' ','_')
4439    # determine if a web browser or the internal viewer should be used for help info
4440    if GSASIIpath.GetConfigValue('Help_mode'):
4441        helpMode = GSASIIpath.GetConfigValue('Help_mode')
4442    else:
4443        helpMode = 'browser'
4444    if helpMode == 'internal':
4445        helplink = os.path.join(path2GSAS2,'help',helplink)
4446        try:
4447            htmlPanel.LoadFile(helplink)
4448            htmlFrame.Raise()
4449        except:
4450            htmlFrame = wx.Frame(frame, -1, size=(610, 510))
4451            htmlFrame.Show(True)
4452            htmlFrame.SetTitle("HTML Window") # N.B. reset later in LoadFile
4453            htmlPanel = MyHtmlPanel(htmlFrame,-1)
4454            htmlPanel.LoadFile(helplink)
4455    else:
4456        if sys.platform == "darwin": # for Mac, force use of safari to preserve anchors on file URLs
4457            wb = webbrowser.MacOSXOSAScript('safari')
4458        else:
4459            wb = webbrowser
4460        helplink = os.path.join(path2GSAS2,'help',helplink)
4461        pfx = "file://"
4462        if sys.platform.lower().startswith('win'):
4463            pfx = ''
4464        #if GSASIIpath.GetConfigValue('debug'): print 'Help link=',pfx+helplink
4465        if htmlFirstUse:
4466            wb.open_new(pfx+helplink)
4467            htmlFirstUse = False
4468        else:
4469            wb.open(pfx+helplink, new=0, autoraise=True)
4470
4471def ShowWebPage(URL,frame):
4472    '''Called to show a tutorial web page.
4473    '''
4474    global htmlFirstUse,htmlPanel,htmlFrame
4475    # determine if a web browser or the internal viewer should be used for help info
4476    if GSASIIpath.GetConfigValue('Help_mode'):
4477        helpMode = GSASIIpath.GetConfigValue('Help_mode')
4478    else:
4479        helpMode = 'browser'
4480    if helpMode == 'internal':
4481        try:
4482            htmlPanel.LoadFile(URL)
4483            htmlFrame.Raise()
4484        except:
4485            htmlFrame = wx.Frame(frame, -1, size=(610, 510))
4486            htmlFrame.Show(True)
4487            htmlFrame.SetTitle("HTML Window") # N.B. reset later in LoadFile
4488            htmlPanel = MyHtmlPanel(htmlFrame,-1)
4489            htmlPanel.LoadFile(URL)
4490    else:
4491        if URL.startswith('http'): 
4492            pfx = ''
4493        elif sys.platform.lower().startswith('win'):
4494            pfx = ''
4495        else:
4496            pfx = "file://"
4497        if htmlFirstUse:
4498            webbrowser.open_new(pfx+URL)
4499            htmlFirstUse = False
4500        else:
4501            webbrowser.open(pfx+URL, new=0, autoraise=True)
4502
4503################################################################################
4504#### Tutorials support
4505################################################################################
4506G2BaseURL = "https://subversion.xray.aps.anl.gov/pyGSAS"
4507tutorialIndex = (
4508    # tutorial dir,      web page file name,      title for page
4509    ['Getting started'],
4510    ['StartingGSASII', 'Starting GSAS.htm', 'Starting GSAS-II'],
4511
4512    ['Rietveld fitting'],
4513       
4514    ['LabData', 'Laboratory X.htm', 'Fitting laboratory X-ray powder data for fluoroapatite'],
4515    ['CWNeutron', 'Neutron CW Powder Data.htm', 'CW Neutron Powder fit for Yttrium-Iron Garnet'],
4516    ['CWCombined', 'Combined refinement.htm', 'Combined X-ray/CW-neutron refinement of PbSO4'],
4517    ['TOF-CW Joint Refinement', 'TOF combined XN Rietveld refinement in GSAS.htm', 'Combined X-ray/TOF-neutron Rietveld refinement'],
4518    ['BkgFit', 'FitBkgTut.htm',  'Fitting the Starting Background using Fixed Points'],
4519   
4520    ['Parametric Rietveld fitting'],
4521    ['SeqRefine', 'SequentialTutorial.htm', 'Sequential refinement of multiple datasets'],
4522    ['SeqParametric', 'ParametricFitting.htm', '     Parametric Fitting and Pseudo Variables for Sequential Fits'],
4523
4524    ['Structure solution'],
4525    ['FitPeaks', 'Fit Peaks.htm', 'Fitting individual peaks & autoindexing'],
4526    ['CFjadarite', 'Charge Flipping in GSAS.htm', '     Charge Flipping structure solution for jadarite'],
4527    ['CFsucrose', 'Charge Flipping - sucrose.htm','     Charge Flipping structure solution for sucrose'],
4528    ['CFXraySingleCrystal', 'CFSingleCrystal.htm', 'Charge Flipping structure solution with Xray single crystal data'],       
4529    ['TOF Charge Flipping', 'Charge Flipping with TOF single crystal data in GSASII.htm', 'Charge flipping with neutron TOF single crystal data'],
4530    ['MCsimanneal', 'MCSA in GSAS.htm', 'Monte-Carlo simulated annealing structure determination'],
4531
4532    ['Stacking Fault Modeling'],
4533    ['StackingFaults-I', 'Stacking Faults-I.htm', 'Stacking fault simulations for diamond'],
4534    ['StackingFaults-II', 'Stacking Faults II.htm', 'Stacking fault simulations for Keokuk kaolinite'],
4535    ['StackingFaults-III', 'Stacking Faults-III.htm', 'Stacking fault simulations for Georgia kaolinite'],
4536
4537    ['Image Calibration/Integration'],
4538    ['2DCalibration', 'Calibration of an area detector in GSAS.htm', 'Calibration of an area detector'],
4539    ['2DIntegration', 'Integration of area detector data in GSAS.htm', '     Integration of area detector data'],
4540    ['TOF Calibration', 'Calibration of a TOF powder diffractometer.htm', 'Calibration of a Neutron TOF diffractometer'],
4541
4542    ['Small-Angle Scattering'],       
4543    ['2DStrain', 'Strain fitting of 2D data in GSAS-II.htm', 'Strain fitting of 2D data'],
4544    ['2DTexture', 'Texture analysis of 2D data in GSAS-II.htm', 'Texture analysis of 2D data'],
4545             
4546    ['SAsize', 'Small Angle Size Distribution.htm', 'Small angle x-ray data size distribution (alumina powder)'],
4547    ['SAfit', 'Fitting Small Angle Scattering Data.htm', '     Fitting small angle x-ray data (alumina powder)'],
4548    ['SAimages', 'Small Angle Image Processing.htm', 'Image Processing of small angle x-ray data'],
4549    ['SAseqref', 'Sequential Refinement of Small Angle Scattering Data.htm', 'Sequential refinement with small angle scattering data'],
4550
4551    ['Other'],   
4552    ['MerohedralTwins', 'Merohedral twin refinement in GSAS.htm', 'Merohedral twin refinements'],
4553    ['TOF Single Crystal Refinement', 'TOF single crystal refinement in GSAS.htm', 'Single crystal refinement from TOF data'],
4554    ['PythonScript','Scripting.htm','Scripting a GSAS-II Refinement from Python']
4555   
4556    #['ExampleDir', 'ExamplePage.html', 'Example Tutorial Title'],
4557    )
4558tutorialCatalog = [l for l in tutorialIndex if len(l) == 3]
4559
4560class OpenTutorial(wx.Dialog):
4561    '''Open a tutorial web page, optionally copying the web page, screen images and
4562    data file(s) to the local disk.
4563    '''
4564   
4565    def __init__(self,parent):
4566        style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER
4567        wx.Dialog.__init__(self, parent, wx.ID_ANY, 'Open Tutorial', style=style)
4568        self.G2frame = self.frame = parent
4569        pnl = wx.Panel(self)
4570        sizer = wx.BoxSizer(wx.VERTICAL)
4571        sizer1 = wx.BoxSizer(wx.HORIZONTAL)       
4572        label = wx.StaticText(
4573            pnl,  wx.ID_ANY,
4574            'Select the tutorial to be run and the mode of access'
4575            )
4576        msg = '''GSAS-II tutorials and their sample data files
4577        require a fair amount of storage space; few users will
4578        use all of them. This dialog allows users to load selected
4579        tutorials (along with their sample data) to their computer;
4580        optionally all tutorials can be downloaded.
4581
4582        Downloaded tutorials can be viewed and run without internet
4583        access. Tutorials can also be viewed without download, but
4584        users will need to download the sample data files manually.
4585
4586        The location used to download tutorials is set using the
4587        "Set download location" which is saved as the "Tutorial_location"
4588        configuration option see File/Preference or the
4589        config_example.py file. System managers can select to have
4590        tutorial files installed at a shared location.
4591        '''
4592        self.SetTutorialPath()
4593        hlp = HelpButton(pnl,msg)
4594        sizer1.Add((-1,-1),1, wx.EXPAND, 0)
4595        sizer1.Add(label, 0, wx.ALIGN_CENTRE|wx.ALL, 0)
4596        sizer1.Add((-1,-1),1, wx.EXPAND, 0)
4597        sizer1.Add(hlp,0,wx.ALIGN_RIGHT|wx.ALL)
4598        sizer.Add(sizer1,0,wx.EXPAND|wx.ALL,0)
4599        sizer.Add((10,10))
4600        sizer0 = wx.BoxSizer(wx.HORIZONTAL)       
4601        sizer1a = wx.BoxSizer(wx.VERTICAL)
4602        sizer1b = wx.BoxSizer(wx.VERTICAL)
4603        btn = wx.Button(pnl, wx.ID_ANY, "Download a tutorial and view")
4604        btn.Bind(wx.EVT_BUTTON, self.SelectAndDownload)
4605        sizer1a.Add(btn,0,WACV)
4606        btn = wx.Button(pnl, wx.ID_ANY, "Select from downloaded tutorials")
4607        btn.Bind(wx.EVT_BUTTON, self.onSelectDownloaded)
4608        sizer1a.Add(btn,0,WACV)
4609        btn = wx.Button(pnl, wx.ID_ANY, "Browse tutorial on web")
4610        btn.Bind(wx.EVT_BUTTON, self.onWebBrowse)
4611        sizer1a.Add(btn,0,WACV)
4612        btn = wx.Button(pnl, wx.ID_ANY, "Update downloaded tutorials")
4613        btn.Bind(wx.EVT_BUTTON, self.UpdateDownloaded)
4614        sizer1b.Add(btn,0,WACV)
4615        btn = wx.Button(pnl, wx.ID_ANY, "Download all tutorials")
4616        btn.Bind(wx.EVT_BUTTON, self.DownloadAll)
4617        sizer1b.Add(btn,0,WACV)
4618        sizer0.Add(sizer1a,0,wx.EXPAND|wx.ALL,0)
4619        sizer0.Add(sizer1b,0,wx.EXPAND|wx.ALL,0)
4620        sizer.Add(sizer0,5,wx.EXPAND|wx.ALL,5)
4621       
4622        sizer.Add((10,10))
4623        sizer1 = wx.BoxSizer(wx.HORIZONTAL)
4624        btn = wx.Button(pnl, wx.ID_ANY, "Set download location")
4625        btn.Bind(wx.EVT_BUTTON, self.SelectDownloadLoc)
4626        sizer1.Add(btn,0,WACV)
4627        self.dataLoc = wx.StaticText(pnl, wx.ID_ANY,self.tutorialPath)
4628        sizer1.Add(self.dataLoc,0,WACV)
4629        sizer.Add(sizer1)
4630       
4631        btnsizer = wx.StdDialogButtonSizer()
4632        btn = wx.Button(pnl, wx.ID_CANCEL,"Done")
4633        btnsizer.AddButton(btn)
4634        btnsizer.Realize()
4635        sizer.Add(btnsizer, 0, wx.ALIGN_CENTER|wx.ALL, 5)
4636        pnl.SetSizer(sizer)
4637        sizer.Fit(self)
4638        self.topsizer=sizer
4639        self.CenterOnParent()
4640
4641    def SetTutorialPath(self):
4642        '''Get the tutorial location if set; if not pick a default
4643        directory in a logical place
4644        '''
4645        if GSASIIpath.GetConfigValue('Tutorial_location'):
4646            self.tutorialPath = os.path.abspath(GSASIIpath.GetConfigValue('Tutorial_location'))
4647        elif (sys.platform.lower().startswith('win') and
4648              os.path.exists(os.path.abspath(os.path.expanduser('~/My Documents')))):
4649            self.tutorialPath = os.path.abspath(os.path.expanduser('~/My Documents/G2tutorials'))
4650        elif (sys.platform.lower().startswith('win') and
4651              os.path.exists(os.path.abspath(os.path.expanduser('~/Documents')))):
4652            self.tutorialPath = os.path.abspath(os.path.expanduser('~/Documents/G2tutorials'))
4653        else:
4654            self.tutorialPath = os.path.abspath(os.path.expanduser('~/G2tutorials'))
4655
4656    def SelectAndDownload(self,event):
4657        '''Make a list of all tutorials on web and allow user to choose one to
4658        download and then view
4659        '''
4660        indices = [j for j,i in enumerate(tutorialCatalog)
4661            if not os.path.exists(os.path.join(self.tutorialPath,i[0],i[1]))]
4662        if not indices:
4663            G2MessageBox(self,'All tutorials are downloaded','None to download')
4664            return
4665        choices = [tutorialCatalog[i][2] for i in indices]
4666        selected = self.ChooseTutorial(choices)
4667        if selected is None: return
4668        j = indices[selected]
4669        fullpath = os.path.join(self.tutorialPath,tutorialCatalog[j][0],tutorialCatalog[j][1])
4670        fulldir = os.path.join(self.tutorialPath,tutorialCatalog[j][0])
4671        URL = G2BaseURL+'/Tutorials/'+tutorialCatalog[j][0]+'/'
4672        if GSASIIpath.svnInstallDir(URL,fulldir):
4673            ShowWebPage(fullpath,self.frame)
4674        else:
4675            G2MessageBox(self,'Error downloading tutorial','Download error')
4676        self.EndModal(wx.ID_OK)
4677        self.G2frame.TutorialImportDir = os.path.join(self.tutorialPath,tutorialCatalog[j][0],'data')
4678
4679    def onSelectDownloaded(self,event):
4680        '''Select a previously downloaded tutorial
4681        '''
4682        indices = [j for j,i in enumerate(tutorialCatalog)
4683            if os.path.exists(os.path.join(self.tutorialPath,i[0],i[1]))]
4684        if not indices:
4685            G2MessageBox(self,
4686                         'There are no downloaded tutorials in '+self.tutorialPath,
4687                         'None downloaded')
4688            return
4689        choices = [tutorialCatalog[i][2] for i in indices]
4690        selected = self.ChooseTutorial(choices)
4691        if selected is None: return
4692        j = indices[selected]
4693        fullpath = os.path.join(self.tutorialPath,tutorialCatalog[j][0],tutorialCatalog[j][1])
4694        self.EndModal(wx.ID_OK)
4695        ShowWebPage(fullpath,self.frame)
4696        self.G2frame.TutorialImportDir = os.path.join(self.tutorialPath,tutorialCatalog[j][0],'data')
4697       
4698    def onWebBrowse(self,event):
4699        '''Make a list of all tutorials on web and allow user to view one.
4700        '''
4701        choices = [i[2] for i in tutorialCatalog]
4702        selected = self.ChooseTutorial(choices)
4703        if selected is None: return       
4704        tutdir = tutorialCatalog[selected][0]
4705        tutfil = tutorialCatalog[selected][1]
4706        # open web page remotely, don't worry about data
4707        URL = G2BaseURL+'/Tutorials/'+tutdir+'/'+tutfil
4708        self.EndModal(wx.ID_OK)
4709        ShowWebPage(URL,self.frame)
4710       
4711    def ChooseTutorial(self,choices):
4712        'choose a tutorial from a list'
4713        def onDoubleClick(event):
4714            'double-click closes the dialog'
4715            dlg.EndModal(wx.ID_OK)
4716        dlg = wx.Dialog(self,wx.ID_ANY,
4717                        'Select a tutorial to view. NB: indented ones require prerequisite',
4718                        style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)
4719        pnl = wx.Panel(dlg)
4720        sizer = wx.BoxSizer(wx.VERTICAL)
4721        listbox = wx.ListBox(pnl, wx.ID_ANY, choices=choices,
4722                             size=(450, 100),
4723                             style=wx.LB_SINGLE)
4724        sizer.Add(listbox,1,WACV|wx.EXPAND|wx.ALL,1)
4725        listbox.Bind(wx.EVT_LISTBOX_DCLICK, onDoubleClick)
4726        sizer.Add((10,10))
4727        btnsizer = wx.StdDialogButtonSizer()
4728        btn = wx.Button(pnl, wx.ID_CANCEL)
4729        btnsizer.AddButton(btn)
4730        OKbtn = wx.Button(pnl, wx.ID_OK)
4731        OKbtn.SetDefault()
4732        btnsizer.AddButton(OKbtn)
4733        btnsizer.Realize()
4734        sizer.Add((-1,5))
4735        sizer.Add(btnsizer,0,wx.ALIGN_RIGHT,50)
4736       
4737        pnl.SetSizer(sizer)
4738        sizer.Fit(dlg)
4739        self.CenterOnParent()
4740        if dlg.ShowModal() != wx.ID_OK:
4741            dlg.Destroy()
4742            return
4743        selected = listbox.GetSelection()
4744        dlg.Destroy()
4745        wx.Yield() # close window right away so user sees something happen
4746        if selected < 0: return
4747        return selected
4748
4749    def UpdateDownloaded(self,event):
4750        'Find the downloaded tutorials and run an svn update on them'
4751        updated = 0
4752        for i in tutorialCatalog:
4753            if not os.path.exists(os.path.join(self.tutorialPath,i[0],i[1])): continue
4754            print('Updating '+i[0])
4755            GSASIIpath.svnUpdateDir(os.path.join(self.tutorialPath,i[0]))
4756            updated += 0
4757        if not updated:
4758            G2MessageBox(self,'Warning, you have no downloaded tutorials','None Downloaded')
4759        self.EndModal(wx.ID_OK)
4760       
4761    def DownloadAll(self,event):
4762        'Download or update all tutorials'
4763        fail = ''
4764        for i in tutorialCatalog:
4765            if os.path.exists(os.path.join(self.tutorialPath,i[0],i[1])):
4766                print('Updating '+i[0])
4767                GSASIIpath.svnUpdateDir(os.path.join(self.tutorialPath,i[0]))
4768            else:
4769                fulldir = os.path.join(self.tutorialPath,i[0])
4770                URL = G2BaseURL+'/Tutorials/'+i[0]+'/'
4771                if not GSASIIpath.svnInstallDir(URL,fulldir):
4772                    if fail: fail += ', '
4773                    fail += i[0]
4774        if fail: 
4775            G2MessageBox(self,'Error downloading tutorial(s)\n\t'+fail,'Download error')
4776        self.EndModal(wx.ID_OK)
4777                   
4778    def SelectDownloadLoc(self,event):
4779        '''Select a download location,
4780        Cancel resets to the default
4781        '''
4782        dlg = wx.DirDialog(self, "Choose a directory for tutorial downloads:",
4783                           defaultPath=self.tutorialPath)#,style=wx.DD_DEFAULT_STYLE)
4784                           #)
4785        try:
4786            if dlg.ShowModal() != wx.ID_OK:
4787                return
4788            pth = dlg.GetPath()
4789        finally:
4790            dlg.Destroy()
4791
4792        if not os.path.exists(pth):
4793            try:
4794                os.makedirs(pth)    #failing for no obvious reason
4795            except OSError:
4796                msg = 'The selected directory is not valid.\n\t'
4797                msg += pth
4798                msg += '\n\nAn attempt to create the directory failed'
4799                G2MessageBox(self.frame,msg)
4800                return
4801        if os.path.exists(os.path.join(pth,"help")) and os.path.exists(os.path.join(pth,"Exercises")):
4802            print("Note that you may have old tutorial files in the following directories")
4803            print('\t'+os.path.join(pth,"help"))
4804            print('\t'+os.path.join(pth,"Exercises"))
4805            print('Subdirectories in the above can be deleted to save space\n\n')
4806        self.tutorialPath = pth
4807        self.dataLoc.SetLabel(self.tutorialPath)
4808        if GSASIIpath.GetConfigValue('Tutorial_location') == pth: return
4809        vars = GetConfigValsDocs()
4810        try:
4811            vars['Tutorial_location'][1] = pth
4812            if GSASIIpath.GetConfigValue('debug'): print('Saving Tutorial_location: '+pth)
4813            GSASIIpath.SetConfigValue(vars)
4814            SaveConfigVars(vars)
4815        except KeyError:
4816            pass
4817           
4818if __name__ == '__main__':
4819    app = wx.PySimpleApp()
4820    GSASIIpath.InvokeDebugOpts()
4821    frm = wx.Frame(None) # create a frame
4822    frm.Show(True)
4823   
4824    #======================================================================
4825    # test Grid with GridFractionEditor
4826    #======================================================================
4827    # tbl = [[1.,2.,3.],[1.1,2.1,3.1]]
4828    # colTypes = 3*[wg.GRID_VALUE_FLOAT+':10,5',]
4829    # Gtbl = Table(tbl,types=colTypes,rowLabels=['a','b'],colLabels=['1','2','3'])
4830    # Grid = GSGrid(frm)
4831    # Grid.SetTable(Gtbl,True)
4832    # for i in (0,1,2):
4833    #     attr = wx.grid.GridCellAttr()
4834    #     attr.IncRef()
4835    #     attr.SetEditor(GridFractionEditor(Grid))
4836    #     Grid.SetColAttr(i, attr)
4837    # frm.SetSize((400,200))
4838    # app.MainLoop()
4839    # sys.exit()
4840    #======================================================================
4841    # test Tutorial access
4842    #======================================================================
4843    # dlg = OpenTutorial(frm)
4844    # if dlg.ShowModal() == wx.ID_OK:
4845    #     print "OK"
4846    # else:
4847    #     print "Cancel"
4848    # dlg.Destroy()
4849    # sys.exit()
4850    #======================================================================
4851    # test ScrolledMultiEditor
4852    #======================================================================
4853    # Data1 = {
4854    #      'Order':1,
4855    #      'omega':'string',
4856    #      'chi':2.0,
4857    #      'phi':'',
4858    #      }
4859    # elemlst = sorted(Data1.keys())
4860    # prelbl = sorted(Data1.keys())
4861    # dictlst = len(elemlst)*[Data1,]
4862    #Data2 = [True,False,False,True]
4863    #Checkdictlst = len(Data2)*[Data2,]
4864    #Checkelemlst = range(len(Checkdictlst))
4865    # print 'before',Data1,'\n',Data2
4866    # dlg = ScrolledMultiEditor(
4867    #     frm,dictlst,elemlst,prelbl,
4868    #     checkdictlst=Checkdictlst,checkelemlst=Checkelemlst,
4869    #     checklabel="Refine?",
4870    #     header="test")
4871    # if dlg.ShowModal() == wx.ID_OK:
4872    #     print "OK"
4873    # else:
4874    #     print "Cancel"
4875    # print 'after',Data1,'\n',Data2
4876    # dlg.Destroy()
4877    Data3 = {
4878         'Order':1.0,
4879         'omega':1.1,
4880         'chi':2.0,
4881         'phi':2.3,
4882         'Order1':1.0,
4883         'omega1':1.1,
4884         'chi1':2.0,
4885         'phi1':2.3,
4886         'Order2':1.0,
4887         'omega2':1.1,
4888         'chi2':2.0,
4889         'phi2':2.3,
4890         }
4891    elemlst = sorted(Data3.keys())
4892    dictlst = len(elemlst)*[Data3,]
4893    prelbl = elemlst[:]
4894    prelbl[0]="this is a much longer label to stretch things out"
4895    Data2 = len(elemlst)*[False,]
4896    Data2[1] = Data2[3] = True
4897    Checkdictlst = len(elemlst)*[Data2,]
4898    Checkelemlst = range(len(Checkdictlst))
4899    #print 'before',Data3,'\n',Data2
4900    #print dictlst,"\n",elemlst
4901    #print Checkdictlst,"\n",Checkelemlst
4902    # dlg = ScrolledMultiEditor(
4903    #     frm,dictlst,elemlst,prelbl,
4904    #     checkdictlst=Checkdictlst,checkelemlst=Checkelemlst,
4905    #     checklabel="Refine?",
4906    #     header="test",CopyButton=True)
4907    # if dlg.ShowModal() == wx.ID_OK:
4908    #     print "OK"
4909    # else:
4910    #     print "Cancel"
4911    #print 'after',Data3,'\n',Data2
4912
4913    # Data2 = list(range(100))
4914    # elemlst += range(2,6)
4915    # postlbl += range(2,6)
4916    # dictlst += len(range(2,6))*[Data2,]
4917
4918    # prelbl = range(len(elemlst))
4919    # postlbl[1] = "a very long label for the 2nd item to force a horiz. scrollbar"
4920    # header="""This is a longer\nmultiline and perhaps silly header"""
4921    # dlg = ScrolledMultiEditor(frm,dictlst,elemlst,prelbl,postlbl,
4922    #                           header=header,CopyButton=True)
4923    # print Data1
4924    # if dlg.ShowModal() == wx.ID_OK:
4925    #     for d,k in zip(dictlst,elemlst):
4926    #         print k,d[k]
4927    # dlg.Destroy()
4928    # if CallScrolledMultiEditor(frm,dictlst,elemlst,prelbl,postlbl,
4929    #                            header=header):
4930    #     for d,k in zip(dictlst,elemlst):
4931    #         print k,d[k]
4932
4933    #======================================================================
4934    # test G2MultiChoiceDialog
4935    #======================================================================
4936    choices = []
4937    for i in range(21):
4938        choices.append("option_"+str(i))
4939    od = {
4940        'label_1':'This is a bool','value_1':True,
4941        'label_2':'This is a int','value_2':-1,
4942        'label_3':'This is a float','value_3':1.0,
4943        'label_4':'This is a string','value_4':'test',}
4944    dlg = G2MultiChoiceDialog(frm, 'Sequential refinement',
4945                              'Select dataset to include',
4946                              choices,extraOpts=od)
4947    sel = range(2,11,2)
4948    dlg.SetSelections(sel)
4949    dlg.SetSelections((1,5))
4950    if dlg.ShowModal() == wx.ID_OK:
4951        for sel in dlg.GetSelections():
4952            print (sel,choices[sel])
4953    print (od)
4954    od = {}
4955    dlg = G2MultiChoiceDialog(frm, 'Sequential refinement',
4956                              'Select dataset to include',
4957                              choices,extraOpts=od)
4958    sel = range(2,11,2)
4959    dlg.SetSelections(sel)
4960    dlg.SetSelections((1,5))
4961    if dlg.ShowModal() == wx.ID_OK: pass
4962    #======================================================================
4963    # test wx.MultiChoiceDialog
4964    #======================================================================
4965    # dlg = wx.MultiChoiceDialog(frm, 'Sequential refinement',
4966    #                           'Select dataset to include',
4967    #                           choices)
4968    # sel = range(2,11,2)
4969    # dlg.SetSelections(sel)
4970    # dlg.SetSelections((1,5))
4971    # if dlg.ShowModal() == wx.ID_OK:
4972    #     for sel in dlg.GetSelections():
4973    #         print sel,choices[sel]
4974
4975    # pnl = wx.Panel(frm)
4976    # siz = wx.BoxSizer(wx.VERTICAL)
4977
4978    # td = {'Goni':200.,'a':1.,'calc':1./3.,'string':'s'}
4979    # for key in sorted(td):
4980    #     txt = ValidatedTxtCtrl(pnl,td,key)
4981    #     siz.Add(txt)
4982    # pnl.SetSizer(siz)
4983    # siz.Fit(frm)
4984    # app.MainLoop()
4985    # print td