source: trunk/GSASIIctrlGUI.py @ 3075

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

Allow editing of proxy info

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