source: trunk/GSASIIctrlGUI.py @ 3207

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

New command-line tutorial

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