source: trunk/GSASIIctrlGUI.py @ 3195

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

generalize 1ID metadata import to column-oriented; add routine to test .lbls file; recognize Dexala in either orientation

  • 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-18 21:18:15 +0000 (Mon, 18 Dec 2017) $
5# $Author: toby $
6# $Revision: 3195 $
7# $URL: trunk/GSASIIctrlGUI.py $
8# $Id: GSASIIctrlGUI.py 3195 2017-12-18 21:18:15Z 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: 3195 $")
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, 
2220                           pos=wx.DefaultPosition,
2221                           style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)
2222        self.value = value
2223        self.prompt = prompt
2224        self.CenterOnParent()
2225        self.panel = wx.Panel(self)
2226        mainSizer = wx.BoxSizer(wx.VERTICAL)
2227        if help:
2228            sizer1 = wx.BoxSizer(wx.HORIZONTAL)   
2229            sizer1.Add((-1,-1),1, wx.EXPAND, 0)
2230            sizer1.Add(HelpButton(self.panel,help),0,wx.ALIGN_RIGHT|wx.ALL)
2231            mainSizer.Add(sizer1,0,wx.ALIGN_RIGHT|wx.EXPAND)
2232        mainSizer.Add(wx.StaticText(self.panel,-1,self.prompt),0,wx.ALIGN_CENTER)
2233        self.valItem = wx.TextCtrl(self.panel,-1,value=self.value,size=size)
2234        mainSizer.Add(self.valItem,0,wx.ALIGN_CENTER)
2235        btnsizer = wx.StdDialogButtonSizer()
2236        OKbtn = wx.Button(self.panel, wx.ID_OK)
2237        OKbtn.SetDefault()
2238        btnsizer.AddButton(OKbtn)
2239        btn = wx.Button(self.panel, wx.ID_CANCEL)
2240        btnsizer.AddButton(btn)
2241        btnsizer.Realize()
2242        mainSizer.Add(btnsizer,0,wx.ALIGN_CENTER)
2243        self.panel.SetSizer(mainSizer)
2244        self.panel.Fit()
2245        self.Fit()
2246
2247    def Show(self):
2248        '''Use this method after creating the dialog to post it
2249        :returns: True if the user pressed OK; False if the User pressed Cancel
2250        '''
2251        if self.ShowModal() == wx.ID_OK:
2252            self.value = self.valItem.GetValue()
2253            return True
2254        else:
2255            return False
2256
2257    def GetValue(self):
2258        '''Use this method to get the value entered by the user
2259        :returns: string entered by user
2260        '''
2261        return self.value
2262
2263################################################################################
2264class MultiStringDialog(wx.Dialog):
2265    '''Dialog to obtain a multi string values from user
2266   
2267    :param wx.Frame parent: name of parent frame
2268    :param str title: title string for dialog
2269    :param str prompts: strings to tell use what they are inputting
2270    :param str values: default input values, if any
2271    :param int size: length of the input box in pixels
2272    '''
2273    def __init__(self,parent,title,prompts,values=[],size=-1):
2274       
2275        wx.Dialog.__init__(self,parent,wx.ID_ANY,title, 
2276                           pos=wx.DefaultPosition,
2277                           style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)
2278        self.values = values
2279        self.prompts = prompts
2280        self.CenterOnParent()
2281        mainSizer = wx.BoxSizer(wx.VERTICAL)
2282        promptSizer = wx.FlexGridSizer(0,2,5,5)
2283        self.Indx = {}
2284        for prompt,value in zip(prompts,values):
2285            promptSizer.Add(wx.StaticText(self,-1,prompt),0,WACV)
2286            valItem = wx.TextCtrl(self,-1,value=value,style=wx.TE_PROCESS_ENTER,size=(size,-1))
2287            self.Indx[valItem.GetId()] = prompt
2288            valItem.Bind(wx.EVT_TEXT,self.newValue)
2289            promptSizer.Add(valItem,1,WACV|wx.EXPAND,1)
2290        mainSizer.Add(promptSizer,1,wx.ALL|wx.EXPAND,1)
2291        btnsizer = wx.StdDialogButtonSizer()
2292        OKbtn = wx.Button(self, wx.ID_OK)
2293        OKbtn.SetDefault()
2294        btnsizer.AddButton(OKbtn)
2295        btn = wx.Button(self, wx.ID_CANCEL)
2296        btnsizer.AddButton(btn)
2297        btnsizer.Realize()
2298        mainSizer.Add(btnsizer,0,wx.ALIGN_CENTER)
2299        self.SetSizer(mainSizer)
2300        self.Fit()
2301       
2302    def newValue(self,event):
2303        Obj = event.GetEventObject()
2304        item = self.Indx[Obj.GetId()]
2305        id = self.prompts.index(item)
2306        self.values[id] = Obj.GetValue()
2307
2308    def Show(self):
2309        '''Use this method after creating the dialog to post it
2310        :returns: True if the user pressed OK; False if the User pressed Cancel
2311        '''
2312        if self.ShowModal() == wx.ID_OK:
2313            return True
2314        else:
2315            return False
2316
2317    def GetValues(self):
2318        '''Use this method to get the value entered by the user
2319        :returns: string entered by user
2320        '''
2321        return self.values
2322
2323################################################################################
2324class G2ColumnIDDialog(wx.Dialog):
2325    '''A dialog for matching column data to desired items; some columns may be ignored.
2326   
2327    :param wx.Frame ParentFrame: reference to parent frame
2328    :param str title: heading above list of choices
2329    :param str header: Title to place on window frame
2330    :param list ChoiceList: a list of possible choices for the columns
2331    :param list ColumnData: lists of column data to be matched with ChoiceList
2332    :param bool monoFont: If False (default), use a variable-spaced font;
2333      if True use a equally-spaced font.
2334    :param kw: optional keyword parameters for the wx.Dialog may
2335      be included such as size [which defaults to `(320,310)`] and
2336      style (which defaults to ``wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER | wx.CENTRE | wx.OK | wx.CANCEL``);
2337      note that ``wx.OK`` and ``wx.CANCEL`` controls
2338      the presence of the eponymous buttons in the dialog.
2339    :returns: the name of the created dialog
2340   
2341    '''
2342
2343    def __init__(self,parent, title, header,Comments,ChoiceList, ColumnData,
2344                 monoFont=False, **kw):
2345
2346        def OnOk(sevent):
2347            OK = True
2348            selCols = []
2349            for col in self.sel:
2350                item = col.GetValue()
2351                if item != ' ' and item in selCols:
2352                    OK = False
2353                    break
2354                else:
2355                    selCols.append(item)
2356            parent = self.GetParent()
2357            if not OK:
2358                parent.ErrorDialog('Duplicate',item+' selected more than once')
2359                return
2360            parent.Raise()
2361            self.EndModal(wx.ID_OK)
2362           
2363        def OnModify(event):
2364            if event: event.Skip()
2365            Obj = event.GetEventObject()
2366            icol,colData = Indx[Obj.GetId()]
2367            modify = Obj.GetValue()
2368            if not modify:
2369                return
2370            #print 'Modify column',icol,' by', modify
2371            for i,item in enumerate(self.ColumnData[icol]):
2372                self.ColumnData[icol][i] = str(eval(item+modify))
2373            colData.SetValue('\n'.join(self.ColumnData[icol]))
2374            Obj.SetValue('')
2375           
2376        # process keyword parameters, notably style
2377        options = {'size':(600,310), # default Frame keywords
2378                   'style':wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER|wx.CENTRE| wx.OK | wx.CANCEL,
2379                   }
2380        options.update(kw)
2381        self.Comments = ''.join(Comments)
2382        self.ChoiceList = ChoiceList
2383        self.ColumnData = ColumnData
2384        nCol = len(ColumnData)
2385        if options['style'] & wx.OK:
2386            useOK = True
2387            options['style'] ^= wx.OK
2388        else:
2389            useOK = False
2390        if options['style'] & wx.CANCEL:
2391            useCANCEL = True
2392            options['style'] ^= wx.CANCEL
2393        else:
2394            useCANCEL = False       
2395        # create the dialog frame
2396        wx.Dialog.__init__(self,parent,wx.ID_ANY,header,**options)
2397        panel = wxscroll.ScrolledPanel(self)
2398        # fill the dialog
2399        Sizer = wx.BoxSizer(wx.VERTICAL)
2400        Sizer.Add((-1,5))
2401        Sizer.Add(wx.StaticText(panel,label=title),0,WACV)
2402        if self.Comments:
2403            Sizer.Add(wx.StaticText(panel,label=' Header lines:'),0,WACV)
2404            Sizer.Add(wx.TextCtrl(panel,value=self.Comments,size=(200,-1),
2405                style=wx.TE_MULTILINE|wx.TE_READONLY|wx.TE_DONTWRAP),0,wx.ALL|wx.EXPAND|WACV,8)
2406        columnsSizer = wx.FlexGridSizer(0,nCol,5,10)
2407        self.sel = []
2408        self.mod = []
2409        Indx = {}
2410        for icol,col in enumerate(self.ColumnData):
2411            colSizer = wx.BoxSizer(wx.VERTICAL)
2412            colSizer.Add(wx.StaticText(panel,label=' Column #%d Select:'%(icol)),0,WACV)
2413            self.sel.append(wx.ComboBox(panel,value=' ',choices=self.ChoiceList,style=wx.CB_READONLY|wx.CB_DROPDOWN))
2414            colSizer.Add(self.sel[-1])
2415            colData = wx.TextCtrl(panel,value='\n'.join(self.ColumnData[icol]),size=(120,-1),
2416                style=wx.TE_MULTILINE|wx.TE_READONLY|wx.TE_DONTWRAP)
2417            colSizer.Add(colData,0,WACV)
2418            colSizer.Add(wx.StaticText(panel,label=' Modify by:'),0,WACV)
2419            mod = wx.TextCtrl(panel,size=(120,-1),value='',style=wx.TE_PROCESS_ENTER)
2420            mod.Bind(wx.EVT_TEXT_ENTER,OnModify)
2421            mod.Bind(wx.EVT_KILL_FOCUS,OnModify)
2422            Indx[mod.GetId()] = [icol,colData]
2423            colSizer.Add(mod,0,WACV)
2424            columnsSizer.Add(colSizer)
2425        Sizer.Add(columnsSizer)
2426        Sizer.Add(wx.StaticText(panel,label=' For modify by, enter arithmetic string eg. "-12345.67". "+","-","*","/","**" all allowed'),0,WACV) 
2427        Sizer.Add((-1,10))
2428        # OK/Cancel buttons
2429        btnsizer = wx.StdDialogButtonSizer()
2430        if useOK:
2431            self.OKbtn = wx.Button(panel, wx.ID_OK)
2432            self.OKbtn.SetDefault()
2433            btnsizer.AddButton(self.OKbtn)
2434            self.OKbtn.Bind(wx.EVT_BUTTON, OnOk)
2435        if useCANCEL:
2436            btn = wx.Button(panel, wx.ID_CANCEL)
2437            btnsizer.AddButton(btn)
2438        btnsizer.Realize()
2439        Sizer.Add((-1,5))
2440        Sizer.Add(btnsizer,0,wx.ALIGN_LEFT,20)
2441        Sizer.Add((-1,5))
2442        # OK done, let's get outa here
2443        panel.SetSizer(Sizer)
2444        panel.SetAutoLayout(1)
2445        panel.SetupScrolling()
2446        Size = [450,375]
2447        panel.SetSize(Size)
2448        Size[0] += 25; Size[1]+= 25
2449        self.SetSize(Size)
2450       
2451    def GetSelection(self):
2452        'Returns the selected sample parm for each column'
2453        selCols = []
2454        for item in self.sel:
2455            selCols.append(item.GetValue())
2456        return selCols,self.ColumnData
2457   
2458################################################################################
2459class G2HistoDataDialog(wx.Dialog):
2460    '''A dialog for editing histogram data globally.
2461   
2462    :param wx.Frame ParentFrame: reference to parent frame
2463    :param str title: heading above list of choices
2464    :param str header: Title to place on window frame
2465    :param list ParmList: a list of names for the columns
2466    :param list ParmFmt: a list of formatting strings for the columns
2467    :param list: HistoList: a list of histogram names
2468    :param list ParmData: a list of lists of data matched to ParmList; one for each item in HistoList
2469    :param bool monoFont: If False (default), use a variable-spaced font;
2470      if True use a equally-spaced font.
2471    :param kw: optional keyword parameters for the wx.Dialog may
2472      be included such as size [which defaults to `(320,310)`] and
2473      style (which defaults to
2474      ``wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER | wx.CENTRE | wx.OK | wx.CANCEL``);
2475      note that ``wx.OK`` and ``wx.CANCEL`` controls the presence of the eponymous buttons in the dialog.
2476    :returns: the modified ParmData
2477   
2478    '''
2479
2480    def __init__(self,parent, title, header,ParmList,ParmFmt,HistoList,ParmData,
2481                 monoFont=False, **kw):
2482
2483        def OnOk(sevent):
2484            parent.Raise()
2485            self.EndModal(wx.ID_OK)
2486           
2487        def OnModify(event):
2488            Obj = event.GetEventObject()
2489            irow,it = Indx[Obj.GetId()]
2490            try:
2491                val = float(Obj.GetValue())
2492            except ValueError:
2493                val = self.ParmData[irow][it]
2494            self.ParmData[irow][it] = val
2495            Obj.SetValue(self.ParmFmt[it]%val)
2496                       
2497        # process keyword parameters, notably style
2498        options = {'size':(600,310), # default Frame keywords
2499                   'style':wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER|wx.CENTRE| wx.OK | wx.CANCEL,
2500                   }
2501        options.update(kw)
2502        self.ParmList = ParmList
2503        self.ParmFmt = ParmFmt
2504        self.HistoList = HistoList
2505        self.ParmData = ParmData
2506        nCol = len(ParmList)
2507        if options['style'] & wx.OK:
2508            useOK = True
2509            options['style'] ^= wx.OK
2510        else:
2511            useOK = False
2512        if options['style'] & wx.CANCEL:
2513            useCANCEL = True
2514            options['style'] ^= wx.CANCEL
2515        else:
2516            useCANCEL = False       
2517        # create the dialog frame
2518        wx.Dialog.__init__(self,parent,wx.ID_ANY,header,**options)
2519        panel = wxscroll.ScrolledPanel(self)
2520        # fill the dialog
2521        Sizer = wx.BoxSizer(wx.VERTICAL)
2522        Sizer.Add((-1,5))
2523        Sizer.Add(wx.StaticText(panel,label=title),0,WACV)
2524        dataSizer = wx.FlexGridSizer(0,nCol+1,0,0)
2525        self.sel = []
2526        self.mod = []
2527        Indx = {}
2528        for item in ['Histogram',]+self.ParmList:
2529            dataSizer.Add(wx.StaticText(panel,-1,label=' %10s '%(item)),0,WACV)
2530        for irow,name in enumerate(self.HistoList):
2531            dataSizer.Add(wx.StaticText(panel,label=name),0,WACV|wx.LEFT|wx.RIGHT,10)
2532            for it,item in enumerate(self.ParmData[irow]):
2533                dat = wx.TextCtrl(panel,-1,value=self.ParmFmt[it]%(item),style=wx.TE_PROCESS_ENTER)
2534                dataSizer.Add(dat,0,WACV)
2535                dat.Bind(wx.EVT_TEXT_ENTER,OnModify)
2536                dat.Bind(wx.EVT_KILL_FOCUS,OnModify)
2537                Indx[dat.GetId()] = [irow,it]
2538        Sizer.Add(dataSizer)
2539        Sizer.Add((-1,10))
2540        # OK/Cancel buttons
2541        btnsizer = wx.StdDialogButtonSizer()
2542        if useOK:
2543            self.OKbtn = wx.Button(panel, wx.ID_OK)
2544            self.OKbtn.SetDefault()
2545            btnsizer.AddButton(self.OKbtn)
2546            self.OKbtn.Bind(wx.EVT_BUTTON, OnOk)
2547        if useCANCEL:
2548            btn = wx.Button(panel, wx.ID_CANCEL)
2549            btnsizer.AddButton(btn)
2550        btnsizer.Realize()
2551        Sizer.Add((-1,5))
2552        Sizer.Add(btnsizer,0,wx.ALIGN_LEFT,20)
2553        Sizer.Add((-1,5))
2554        # OK done, let's get outa here
2555        panel.SetSizer(Sizer)
2556        panel.SetAutoLayout(1)
2557        panel.SetupScrolling()
2558        Size = [450,375]
2559        panel.SetSize(Size)
2560        Size[0] += 25; Size[1]+= 25
2561        self.SetSize(Size)
2562       
2563    def GetData(self):
2564        'Returns the modified ParmData'
2565        return self.ParmData
2566   
2567################################################################################
2568def ItemSelector(ChoiceList, ParentFrame=None,
2569                 title='Select an item',
2570                 size=None, header='Item Selector',
2571                 useCancel=True,multiple=False):
2572    ''' Provide a wx dialog to select a single item or multiple items from list of choices
2573
2574    :param list ChoiceList: a list of choices where one will be selected
2575    :param wx.Frame ParentFrame: Name of parent frame (default None)
2576    :param str title: heading above list of choices (default 'Select an item')
2577    :param wx.Size size: Size for dialog to be created (default None -- size as needed)
2578    :param str header: Title to place on window frame (default 'Item Selector')
2579    :param bool useCancel: If True (default) both the OK and Cancel buttons are offered
2580    :param bool multiple: If True then multiple items can be selected (default False)
2581   
2582    :returns: the selection index or None or a selection list if multiple is true
2583
2584    Called by GSASIIdataGUI.OnReOrgSelSeq() Which is not fully implemented.
2585    '''
2586    if multiple:
2587        if useCancel:
2588            dlg = G2MultiChoiceDialog(
2589                ParentFrame,title, header, ChoiceList)
2590        else:
2591            dlg = G2MultiChoiceDialog(
2592                ParentFrame,title, header, ChoiceList,
2593                style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER|wx.OK|wx.CENTRE)
2594    else:
2595        if useCancel:
2596            dlg = wx.SingleChoiceDialog(
2597                ParentFrame,title, header, ChoiceList)
2598        else:
2599            dlg = wx.SingleChoiceDialog(
2600                ParentFrame,title, header,ChoiceList,
2601                style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER|wx.OK|wx.CENTRE)
2602    if size: dlg.SetSize(size)
2603    if dlg.ShowModal() == wx.ID_OK:
2604        if multiple:
2605            dlg.Destroy()
2606            return dlg.GetSelections()
2607        else:
2608            dlg.Destroy()
2609            return dlg.GetSelection()
2610    else:
2611        dlg.Destroy()
2612        return None
2613    dlg.Destroy()
2614
2615######################################################### Column-order selection dialog
2616def GetItemOrder(parent,keylist,vallookup,posdict):
2617    '''Creates a panel where items can be ordered into columns
2618   
2619    :param list keylist: is a list of keys for column assignments
2620    :param dict vallookup: is a dict keyed by names in keylist where each item is a dict.
2621       Each inner dict contains variable names as keys and their associated values
2622    :param dict posdict: is a dict keyed by names in keylist where each item is a dict.
2623       Each inner dict contains column numbers as keys and their associated
2624       variable name as a value. This is used for both input and output.
2625       
2626    '''
2627    dlg = wx.Dialog(parent,style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)
2628    sizer = wx.BoxSizer(wx.VERTICAL)
2629    spanel = OrderBox(dlg,keylist,vallookup,posdict)
2630    spanel.Fit()
2631    sizer.Add(spanel,1,wx.EXPAND)
2632    btnsizer = wx.StdDialogButtonSizer()
2633    btn = wx.Button(dlg, wx.ID_OK)
2634    btn.SetDefault()
2635    btnsizer.AddButton(btn)
2636    #btn = wx.Button(dlg, wx.ID_CANCEL)
2637    #btnsizer.AddButton(btn)
2638    btnsizer.Realize()
2639    sizer.Add(btnsizer, 0, wx.ALIGN_CENTER_VERTICAL|wx.EXPAND|wx.ALL, 5)
2640    dlg.SetSizer(sizer)
2641    sizer.Fit(dlg)
2642    dlg.ShowModal()
2643
2644################################################################################
2645class MultiIntegerDialog(wx.Dialog):
2646    '''Input a series of integers based on prompts
2647    '''
2648    def __init__(self,parent,title,prompts,values):
2649        wx.Dialog.__init__(self,parent,-1,title, 
2650            pos=wx.DefaultPosition,style=wx.DEFAULT_DIALOG_STYLE)
2651        self.panel = wx.Panel(self)         #just a dummy - gets destroyed in Draw!
2652        self.values = values
2653        self.prompts = prompts
2654        self.Draw()
2655       
2656    def Draw(self):
2657       
2658        def OnValItem(event):
2659            event.Skip()
2660            Obj = event.GetEventObject()
2661            ind = Indx[Obj.GetId()]
2662            try:
2663                val = int(Obj.GetValue())
2664                if val <= 0:
2665                    raise ValueError
2666            except ValueError:
2667                val = self.values[ind]
2668            self.values[ind] = val
2669            Obj.SetValue('%d'%(val))
2670           
2671        self.panel.Destroy()
2672        self.panel = wx.Panel(self)
2673        mainSizer = wx.BoxSizer(wx.VERTICAL)
2674        Indx = {}
2675        for ival,[prompt,value] in enumerate(zip(self.prompts,self.values)):
2676            mainSizer.Add(wx.StaticText(self.panel,-1,prompt),0,wx.ALIGN_CENTER)
2677            valItem = wx.TextCtrl(self.panel,-1,value='%d'%(value),style=wx.TE_PROCESS_ENTER)
2678            mainSizer.Add(valItem,0,wx.ALIGN_CENTER)
2679            Indx[valItem.GetId()] = ival
2680            valItem.Bind(wx.EVT_TEXT_ENTER,OnValItem)
2681            valItem.Bind(wx.EVT_KILL_FOCUS,OnValItem)
2682        OkBtn = wx.Button(self.panel,-1,"Ok")
2683        OkBtn.Bind(wx.EVT_BUTTON, self.OnOk)
2684        CancelBtn = wx.Button(self.panel,-1,'Cancel')
2685        CancelBtn.Bind(wx.EVT_BUTTON, self.OnCancel)
2686        btnSizer = wx.BoxSizer(wx.HORIZONTAL)
2687        btnSizer.Add((20,20),1)
2688        btnSizer.Add(OkBtn)
2689        btnSizer.Add(CancelBtn)
2690        btnSizer.Add((20,20),1)
2691        mainSizer.Add(btnSizer,0,wx.EXPAND|wx.BOTTOM|wx.TOP, 10)
2692        self.panel.SetSizer(mainSizer)
2693        self.panel.Fit()
2694        self.Fit()
2695
2696    def GetValues(self):
2697        return self.values
2698       
2699    def OnOk(self,event):
2700        parent = self.GetParent()
2701        parent.Raise()
2702        self.EndModal(wx.ID_OK)             
2703       
2704    def OnCancel(self,event):
2705        parent = self.GetParent()
2706        parent.Raise()
2707        self.EndModal(wx.ID_CANCEL)
2708
2709################################################################################
2710class OrderBox(wxscroll.ScrolledPanel):
2711    '''Creates a panel with scrollbars where items can be ordered into columns
2712   
2713    :param list keylist: is a list of keys for column assignments
2714    :param dict vallookup: is a dict keyed by names in keylist where each item is a dict.
2715      Each inner dict contains variable names as keys and their associated values
2716    :param dict posdict: is a dict keyed by names in keylist where each item is a dict.
2717      Each inner dict contains column numbers as keys and their associated
2718      variable name as a value. This is used for both input and output.
2719     
2720    '''
2721    def __init__(self,parent,keylist,vallookup,posdict,*arg,**kw):
2722        self.keylist = keylist
2723        self.vallookup = vallookup
2724        self.posdict = posdict
2725        self.maxcol = 0
2726        for nam in keylist:
2727            posdict = self.posdict[nam]
2728            if posdict.keys():
2729                self.maxcol = max(self.maxcol, max(posdict))
2730        wxscroll.ScrolledPanel.__init__(self,parent,wx.ID_ANY,*arg,**kw)
2731        self.GBsizer = wx.GridBagSizer(4,4)
2732        self.SetBackgroundColour(WHITE)
2733        self.SetSizer(self.GBsizer)
2734        colList = [str(i) for i in range(self.maxcol+2)]
2735        for i in range(self.maxcol+1):
2736            wid = wx.StaticText(self,wx.ID_ANY,str(i),style=wx.ALIGN_CENTER)
2737            wid.SetBackgroundColour(DULL_YELLOW)
2738            wid.SetMinSize((50,-1))
2739            self.GBsizer.Add(wid,(0,i),flag=wx.ALIGN_CENTER|wx.EXPAND)
2740        self.chceDict = {}
2741        for row,nam in enumerate(self.keylist):
2742            posdict = self.posdict[nam]
2743            for col in posdict:
2744                lbl = posdict[col]
2745                pnl = wx.Panel(self,wx.ID_ANY)
2746                pnl.SetBackgroundColour(VERY_LIGHT_GREY)
2747                insize = wx.BoxSizer(wx.VERTICAL)
2748                wid = wx.Choice(pnl,wx.ID_ANY,choices=colList)
2749                insize.Add(wid,0,wx.EXPAND|wx.BOTTOM,3)
2750                wid.SetSelection(col)
2751                self.chceDict[wid] = (row,col)
2752                wid.Bind(wx.EVT_CHOICE,self.OnChoice)
2753                wid = wx.StaticText(pnl,wx.ID_ANY,lbl)
2754                insize.Add(wid,0,flag=wx.EXPAND)
2755                try:
2756                    val = G2py3.FormatSigFigs(self.vallookup[nam][lbl],maxdigits=8)
2757                except KeyError:
2758                    val = '?'
2759                wid = wx.StaticText(pnl,wx.ID_ANY,'('+val+')')
2760                insize.Add(wid,0,flag=wx.EXPAND)
2761                pnl.SetSizer(insize)
2762                self.GBsizer.Add(pnl,(row+1,col),flag=wx.EXPAND)
2763        self.SetAutoLayout(1)
2764        self.SetupScrolling()
2765        self.SetMinSize((
2766            min(700,self.GBsizer.GetSize()[0]),
2767            self.GBsizer.GetSize()[1]+20))
2768    def OnChoice(self,event):
2769        '''Called when a column is assigned to a variable
2770        '''
2771        row,col = self.chceDict[event.EventObject] # which variable was this?
2772        newcol = event.Selection # where will it be moved?
2773        if newcol == col:
2774            return # no change: nothing to do!
2775        prevmaxcol = self.maxcol # save current table size
2776        key = self.keylist[row] # get the key for the current row
2777        lbl = self.posdict[key][col] # selected variable name
2778        lbl1 = self.posdict[key].get(col+1,'') # next variable name, if any
2779        # if a posXXX variable is selected, and the next variable is posXXX, move them together
2780        repeat = 1
2781        if lbl[:3] == 'pos' and lbl1[:3] == 'int' and lbl[3:] == lbl1[3:]:
2782            repeat = 2
2783        for i in range(repeat): # process the posXXX and then the intXXX (or a single variable)
2784            col += i
2785            newcol += i
2786            if newcol in self.posdict[key]:
2787                # find first non-blank after newcol
2788                for mtcol in range(newcol+1,self.maxcol+2):
2789                    if mtcol not in self.posdict[key]: break
2790                l1 = range(mtcol,newcol,-1)+[newcol]
2791                l = range(mtcol-1,newcol-1,-1)+[col]
2792            else:
2793                l1 = [newcol]
2794                l = [col]
2795            # move all of the items, starting from the last column
2796            for newcol,col in zip(l1,l):
2797                #print 'moving',col,'to',newcol
2798                self.posdict[key][newcol] = self.posdict[key][col]
2799                del self.posdict[key][col]
2800                self.maxcol = max(self.maxcol,newcol)
2801                obj = self.GBsizer.FindItemAtPosition((row+1,col))
2802                self.GBsizer.SetItemPosition(obj.GetWindow(),(row+1,newcol))
2803                for wid in obj.GetWindow().Children:
2804                    if wid in self.chceDict:
2805                        self.chceDict[wid] = (row,newcol)
2806                        wid.SetSelection(self.chceDict[wid][1])
2807        # has the table gotten larger? If so we need new column heading(s)
2808        if prevmaxcol != self.maxcol:
2809            for i in range(prevmaxcol+1,self.maxcol+1):
2810                wid = wx.StaticText(self,wx.ID_ANY,str(i),style=wx.ALIGN_CENTER)
2811                wid.SetBackgroundColour(DULL_YELLOW)
2812                wid.SetMinSize((50,-1))
2813                self.GBsizer.Add(wid,(0,i),flag=wx.ALIGN_CENTER|wx.EXPAND)
2814            colList = [str(i) for i in range(self.maxcol+2)]
2815            for wid in self.chceDict:
2816                wid.SetItems(colList)
2817                wid.SetSelection(self.chceDict[wid][1])
2818        self.GBsizer.Layout()
2819        self.FitInside()
2820       
2821################################################################################
2822def GetImportFile(G2frame, message, defaultDir="", defaultFile="", style=wx.FD_OPEN,
2823                  *args, **kwargs):
2824    '''Uses a customized dialog that gets files from the appropriate import directory.
2825    Arguments are used the same as in :func:`wx.FileDialog`. Selection of
2826    multiple files is allowed if argument style includes wx.FD_MULTIPLE.
2827
2828    The default initial directory (unless overridden with argument defaultDir)
2829    is found in G2frame.TutorialImportDir, config setting Import_directory or
2830    G2frame.LastImportDir, see :func:`GetImportPath`.
2831
2832    The path of the first file entered is used to set G2frame.LastImportDir
2833    and optionally config setting Import_directory.
2834
2835    :returns: a list of files or an empty list
2836    '''
2837    dlg = wx.FileDialog(G2frame, message, defaultDir, defaultFile, *args,
2838                        style=style, **kwargs)
2839    pth = GetImportPath(G2frame)
2840    if not defaultDir and pth: dlg.SetDirectory(pth)
2841    try:
2842        if dlg.ShowModal() == wx.ID_OK:
2843            if style & wx.FD_MULTIPLE:
2844                filelist = dlg.GetPaths()
2845                if len(filelist) == 0: return []
2846            else:
2847                filelist = [dlg.GetPath(),]
2848            # not sure if we want to do this (why use wx.CHANGE_DIR?)
2849            if style & wx.FD_CHANGE_DIR: # to get Mac/Linux to change directory like windows!
2850                os.chdir(dlg.GetDirectory())
2851        else: # cancel was pressed
2852            return []
2853    finally:
2854        dlg.Destroy()
2855    # save the path of the first file and reset the TutorialImportDir variable
2856    pth = os.path.split(os.path.abspath(filelist[0]))[0]
2857    if GSASIIpath.GetConfigValue('Save_paths'): SaveImportDirectory(pth)
2858    G2frame.LastImportDir = pth
2859    G2frame.TutorialImportDir = None
2860    return filelist
2861
2862def GetImportPath(G2frame):
2863    '''Determines the default location to use for importing files. Tries sequentially
2864    G2frame.TutorialImportDir, config var Import_directory and G2frame.LastImportDir.
2865   
2866    :returns: a string containing the path to be used when reading files or None
2867      if none of the above are specified.
2868    '''
2869    if G2frame.TutorialImportDir:
2870        if os.path.exists(G2frame.TutorialImportDir):
2871            return G2frame.TutorialImportDir
2872        elif GSASIIpath.GetConfigValue('debug'):
2873            print('Tutorial location (TutorialImportDir) not found: '+G2frame.TutorialImportDir)
2874    pth = GSASIIpath.GetConfigValue('Import_directory')
2875    if pth:
2876        pth = os.path.expanduser(pth)
2877        if os.path.exists(pth):
2878            return pth
2879        elif GSASIIpath.GetConfigValue('debug'):
2880            print('Ignoring Config Import_directory value: '+
2881                      GSASIIpath.GetConfigValue('Import_directory'))
2882    if G2frame.LastImportDir:
2883        if os.path.exists(G2frame.LastImportDir):
2884            return G2frame.LastImportDir
2885        elif GSASIIpath.GetConfigValue('debug'):
2886            print('Warning: G2frame.LastImportDir not found = '+G2frame.LastImportDir)
2887    return None
2888
2889def GetExportPath(G2frame):
2890    '''Determines the default location to use for writing files. Tries sequentially
2891    G2frame.LastExportDir and G2frame.LastGPXdir.
2892   
2893    :returns: a string containing the path to be used when writing files or '.'
2894      if none of the above are specified.
2895    '''
2896    if G2frame.LastExportDir:
2897        return G2frame.LastExportDir
2898    elif G2frame.LastGPXdir:
2899        return G2frame.LastGPXdir
2900    else:
2901        return '.'
2902
2903################################################################################
2904class SGMessageBox(wx.Dialog):
2905    ''' Special version of MessageBox that displays space group & super space group text
2906    in two blocks
2907    '''
2908    def __init__(self,parent,title,text,table,):
2909        wx.Dialog.__init__(self,parent,wx.ID_ANY,title,pos=wx.DefaultPosition,
2910            style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)
2911        self.text = text
2912        self.table = table
2913        self.panel = wx.Panel(self)
2914        mainSizer = wx.BoxSizer(wx.VERTICAL)
2915        mainSizer.Add((0,10))
2916        for line in text:
2917            mainSizer.Add(wx.StaticText(self.panel,label='     %s     '%(line)),0,WACV)
2918        ncol = self.table[0].count(',')+1
2919        tableSizer = wx.FlexGridSizer(0,2*ncol+3,0,0)
2920        for j,item in enumerate(self.table):
2921            num,flds = item.split(')')
2922            tableSizer.Add(wx.StaticText(self.panel,label='     %s  '%(num+')')),0,WACV|wx.ALIGN_LEFT)           
2923            flds = flds.replace(' ','').split(',')
2924            for i,fld in enumerate(flds):
2925                if i < ncol-1:
2926                    tableSizer.Add(wx.StaticText(self.panel,label='%s, '%(fld)),0,WACV|wx.ALIGN_RIGHT)
2927                else:
2928                    tableSizer.Add(wx.StaticText(self.panel,label='%s'%(fld)),0,WACV|wx.ALIGN_RIGHT)
2929            if not j%2:
2930                tableSizer.Add((20,0))
2931        mainSizer.Add(tableSizer,0,wx.ALIGN_LEFT)
2932        btnsizer = wx.StdDialogButtonSizer()
2933        OKbtn = wx.Button(self.panel, wx.ID_OK)
2934        OKbtn.Bind(wx.EVT_BUTTON, self.OnOk)
2935        OKbtn.SetDefault()
2936        btnsizer.AddButton(OKbtn)
2937        btnsizer.Realize()
2938        mainSizer.Add((0,10))
2939        mainSizer.Add(btnsizer,0,wx.ALIGN_CENTER)
2940        self.panel.SetSizer(mainSizer)
2941        self.panel.Fit()
2942        self.Fit()
2943        size = self.GetSize()
2944        self.SetSize([size[0]+20,size[1]])
2945
2946    def Show(self):
2947        '''Use this method after creating the dialog to post it
2948        '''
2949        self.ShowModal()
2950        return
2951
2952    def OnOk(self,event):
2953        parent = self.GetParent()
2954        parent.Raise()
2955        self.EndModal(wx.ID_OK)
2956
2957################################################################################
2958class DisAglDialog(wx.Dialog):
2959    '''Distance/Angle Controls input dialog. After
2960    :meth:`ShowModal` returns, the results are found in
2961    dict :attr:`self.data`, which is accessed using :meth:`GetData`.
2962
2963    :param wx.Frame parent: reference to parent frame (or None)
2964    :param dict data: a dict containing the current
2965      search ranges or an empty dict, which causes default values
2966      to be used.
2967      Will be used to set element `DisAglCtls` in
2968      :ref:`Phase Tree Item <Phase_table>`
2969    :param dict default:  A dict containing the default
2970      search ranges for each element.
2971    :param bool Reset: if True (default), show Reset button
2972    :param bool Angle: if True (default), show angle radii
2973    '''
2974    def __init__(self,parent,data,default,Reset=True,Angle=True):
2975        text = 'Distance Angle Controls'
2976        if not Angle:
2977            text = 'Distance Controls'
2978        wx.Dialog.__init__(self,parent,wx.ID_ANY,text, 
2979            pos=wx.DefaultPosition,style=wx.DEFAULT_DIALOG_STYLE)
2980        self.default = default
2981        self.Reset = Reset
2982        self.Angle = Angle
2983        self.panel = None
2984        self._default(data,self.default)
2985        self.Draw(self.data)
2986               
2987    def _default(self,data,default):
2988        '''Set starting values for the search values, either from
2989        the input array or from defaults, if input is null
2990        '''
2991        if data:
2992            self.data = copy.deepcopy(data) # don't mess with originals
2993        else:
2994            self.data = {}
2995            self.data['Name'] = default['Name']
2996            self.data['Factors'] = [0.85,0.85]
2997            self.data['AtomTypes'] = default['AtomTypes']
2998            self.data['BondRadii'] = default['BondRadii'][:]
2999            self.data['AngleRadii'] = default['AngleRadii'][:]
3000
3001    def Draw(self,data):
3002        '''Creates the contents of the dialog. Normally called
3003        by :meth:`__init__`.
3004        '''
3005        if self.panel: self.panel.Destroy()
3006        self.panel = wx.Panel(self)
3007        mainSizer = wx.BoxSizer(wx.VERTICAL)
3008        mainSizer.Add(wx.StaticText(self.panel,-1,'Controls for phase '+data['Name']),
3009            0,WACV|wx.LEFT,10)
3010        mainSizer.Add((10,10),1)
3011       
3012        ncol = 3
3013        if not self.Angle:
3014            ncol=2
3015        radiiSizer = wx.FlexGridSizer(0,ncol,5,5)
3016        radiiSizer.Add(wx.StaticText(self.panel,-1,' Type'),0,WACV)
3017        radiiSizer.Add(wx.StaticText(self.panel,-1,'Bond radii'),0,WACV)
3018        if self.Angle:
3019            radiiSizer.Add(wx.StaticText(self.panel,-1,'Angle radii'),0,WACV)
3020        self.objList = {}
3021        for id,item in enumerate(self.data['AtomTypes']):
3022            radiiSizer.Add(wx.StaticText(self.panel,-1,' '+item),0,WACV)
3023            bRadii = ValidatedTxtCtrl(self.panel,data['BondRadii'],id,nDig=(10,3))
3024            radiiSizer.Add(bRadii,0,WACV)
3025            if self.Angle:
3026                aRadii = ValidatedTxtCtrl(self.panel,data['AngleRadii'],id,nDig=(10,3))
3027                radiiSizer.Add(aRadii,0,WACV)
3028        mainSizer.Add(radiiSizer,0,wx.EXPAND)
3029        if self.Angle:
3030            factorSizer = wx.FlexGridSizer(0,2,5,5)
3031            Names = ['Bond','Angle']
3032            for i,name in enumerate(Names):
3033                factorSizer.Add(wx.StaticText(self.panel,-1,name+' search factor'),0,WACV)
3034                bondFact = ValidatedTxtCtrl(self.panel,data['Factors'],i,nDig=(10,3))
3035                factorSizer.Add(bondFact)
3036            mainSizer.Add(factorSizer,0,wx.EXPAND)
3037       
3038        OkBtn = wx.Button(self.panel,-1,"Ok")
3039        OkBtn.Bind(wx.EVT_BUTTON, self.OnOk)
3040        btnSizer = wx.BoxSizer(wx.HORIZONTAL)
3041        btnSizer.Add((20,20),1)
3042        btnSizer.Add(OkBtn)
3043        if self.Reset:
3044            ResetBtn = wx.Button(self.panel,-1,'Reset')
3045            ResetBtn.Bind(wx.EVT_BUTTON, self.OnReset)
3046            btnSizer.Add(ResetBtn)
3047        btnSizer.Add((20,20),1)
3048        mainSizer.Add(btnSizer,0,wx.EXPAND|wx.BOTTOM|wx.TOP, 10)
3049        self.panel.SetSizer(mainSizer)
3050        self.panel.Fit()
3051        self.Fit()
3052   
3053    def GetData(self):
3054        'Returns the values from the dialog'
3055        return self.data
3056       
3057    def OnOk(self,event):
3058        'Called when the OK button is pressed'
3059        parent = self.GetParent()
3060        parent.Raise()
3061        self.EndModal(wx.ID_OK)             
3062       
3063    def OnReset(self,event):
3064        'Called when the Reset button is pressed'
3065        data = {}
3066        self._default(data,self.default)
3067        wx.CallAfter(self.Draw,self.data)
3068               
3069################################################################################
3070class ShowLSParms(wx.Dialog):
3071    '''Create frame to show least-squares parameters
3072    '''
3073    def __init__(self,parent,title,parmDict,varyList,fullVaryList,
3074                 size=(375,430)):
3075       
3076        wx.Dialog.__init__(self,parent,wx.ID_ANY,title,size=size,
3077                           style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)
3078        self.panel = wxscroll.ScrolledPanel(self)
3079        self.parmChoice = 'Phase'
3080        self.parmDict = parmDict
3081        self.varyList = varyList
3082        self.fullVaryList = fullVaryList
3083
3084        self.parmNames = list(parmDict.keys())
3085        self.parmNames.sort()
3086        splitNames = [item.split(':') for item in self.parmNames if len(item) > 3 and not isinstance(self.parmDict[item],basestring)]
3087        self.globNames = [':'.join(item) for item in splitNames if not item[0] and not item[1]]
3088        self.globVars = list(set([' ',]+[item[2] for item in splitNames if not item[0] and not item[1]]))
3089        self.globVars.sort()
3090        self.hisNames = [':'.join(item) for item in splitNames if not item[0] and item[1]]
3091        self.hisNums = list(set([int(item.split(':')[1]) for item in self.hisNames]))
3092        self.hisNums.sort()
3093        self.hisNums = [' ',]+[str(item) for item in self.hisNums]
3094        self.hisVars = list(set([' ',]+[item[2] for item in splitNames if not item[0]]))
3095        self.hisVars.sort()
3096        self.phasNames = [':'.join(item) for item in splitNames if not item[1] and 'is' not in item[2]]
3097        self.phasNums = [' ',]+list(set([item.split(':')[0] for item in self.phasNames]))
3098        if '' in self.phasNums: self.phasNums.remove('')
3099        self.phasVars = list(set([' ',]+[item[2] for item in splitNames if not item[1] and 'is' not in item[2]]))
3100        self.phasVars.sort()
3101        self.phasNums.sort()
3102        self.hapNames = [':'.join(item) for item in splitNames if item[0] and item[1]]
3103        self.hapVars = list(set([' ',]+[item[2] for item in splitNames if item[0] and item[1]]))
3104        self.hapVars.sort()
3105        self.hisNum = ' '
3106        self.phasNum = ' '
3107        self.varName = ' '
3108        self.listSel = 'Refined'
3109        self.DrawPanel()
3110       
3111           
3112    def DrawPanel(self):
3113           
3114        def _OnParmSel(event):
3115            self.parmChoice = parmSel.GetStringSelection()
3116            self.varName = ' '
3117            wx.CallLater(100,self.DrawPanel)
3118           
3119        def OnPhasSel(event):
3120            event.Skip()
3121            self.phasNum = phasSel.GetValue()
3122            self.varName = ' '
3123            wx.CallLater(100,self.DrawPanel)
3124
3125        def OnHistSel(event):
3126            event.Skip()
3127            self.hisNum = histSel.GetValue()
3128            self.varName = ' '
3129            wx.CallLater(100,self.DrawPanel)
3130           
3131        def OnVarSel(event):
3132            self.varName = varSel.GetValue()
3133            self.phasNum = ' '
3134            self.hisNum = ' '
3135            wx.CallLater(100,self.DrawPanel)
3136           
3137        def OnListSel(event):
3138            self.listSel = listSel.GetStringSelection()
3139            wx.CallLater(100,self.DrawPanel)
3140
3141        if self.panel.GetSizer(): self.panel.GetSizer().Clear(True)
3142        mainSizer = wx.BoxSizer(wx.VERTICAL)
3143        num = len(self.varyList)
3144        mainSizer.Add(wx.StaticText(self.panel,label=' Number of refined variables: '+str(num)),0)
3145        if len(self.varyList) != len(self.fullVaryList):
3146            num = len(self.fullVaryList) - len(self.varyList)
3147            mainSizer.Add(wx.StaticText(self.panel,label=' + '+str(num)+' parameters are varied via constraints'))
3148        choiceDict = {'Global':self.globNames,'Phase':self.phasNames,'Phase/Histo':self.hapNames,'Histogram':self.hisNames}
3149        choice = ['Phase','Phase/Histo','Histogram']
3150        if len(self.globNames):
3151            choice += ['Global',]
3152        parmSizer = wx.FlexGridSizer(0,3,5,5)
3153        parmSel = wx.RadioBox(self.panel,wx.ID_ANY,'Parameter type:',choices=choice,
3154            majorDimension=1,style=wx.RA_SPECIFY_COLS)
3155        parmSel.Bind(wx.EVT_RADIOBOX,_OnParmSel)
3156        parmSel.SetStringSelection(self.parmChoice)
3157        parmSizer.Add(parmSel,0)
3158        numSizer = wx.BoxSizer(wx.VERTICAL)
3159        numSizer.Add((5,25),0)
3160        if self.parmChoice in ['Phase','Phase/Histo'] and len(self.phasNums) > 1:
3161            numSizer.Add(wx.StaticText(self.panel,label='Phase'),0)
3162            phasSel = wx.ComboBox(self.panel,choices=self.phasNums,value=self.phasNum,
3163                style=wx.CB_READONLY|wx.CB_DROPDOWN)
3164            phasSel.Bind(wx.EVT_COMBOBOX,OnPhasSel)
3165            numSizer.Add(phasSel,0)
3166        if self.parmChoice in ['Histogram','Phase/Histo'] and len(self.hisNums) > 1:
3167            numSizer.Add(wx.StaticText(self.panel,label='Histogram'),0)
3168            histSel = wx.ComboBox(self.panel,choices=self.hisNums,value=self.hisNum,
3169                style=wx.CB_READONLY|wx.CB_DROPDOWN)
3170            histSel.Bind(wx.EVT_COMBOBOX,OnHistSel)
3171#            histSel = wx.TextCtrl(self.panel,size=(50,25),value='0',style=wx.TE_PROCESS_ENTER)
3172#            histSel.Bind(wx.EVT_TEXT_ENTER,OnHistSel)
3173#            histSel.Bind(wx.EVT_KILL_FOCUS,OnHistSel)
3174            numSizer.Add(histSel,0)
3175        parmSizer.Add(numSizer)
3176        varSizer = wx.BoxSizer(wx.VERTICAL)
3177        if self.parmChoice in ['Phase',]:
3178            varSel = wx.ComboBox(self.panel,choices=self.phasVars,value=self.varName,
3179                style=wx.CB_READONLY|wx.CB_DROPDOWN)
3180            varSel.Bind(wx.EVT_COMBOBOX,OnVarSel)
3181        elif self.parmChoice in ['Histogram',]:
3182            varSel = wx.ComboBox(self.panel,choices=self.hisVars,value=self.varName,
3183                style=wx.CB_READONLY|wx.CB_DROPDOWN)
3184            varSel.Bind(wx.EVT_COMBOBOX,OnVarSel)
3185        elif self.parmChoice in ['Phase/Histo',]:
3186            varSel = wx.ComboBox(self.panel,choices=self.hapVars,value=self.varName,
3187                style=wx.CB_READONLY|wx.CB_DROPDOWN)
3188            varSel.Bind(wx.EVT_COMBOBOX,OnVarSel)
3189        if self.parmChoice != 'Global': 
3190            varSizer.Add(wx.StaticText(self.panel,label='Parameter'))
3191            varSizer.Add(varSel,0)
3192        parmSizer.Add(varSizer,0)
3193        mainSizer.Add(parmSizer,0)
3194        listChoice = ['All','Refined']
3195        listSel = wx.RadioBox(self.panel,wx.ID_ANY,'Parameter type:',choices=listChoice,
3196            majorDimension=0,style=wx.RA_SPECIFY_COLS)
3197        listSel.SetStringSelection(self.listSel)
3198        listSel.Bind(wx.EVT_RADIOBOX,OnListSel)
3199        mainSizer.Add(listSel,0)
3200        subSizer = wx.FlexGridSizer(cols=4,hgap=2,vgap=2)
3201        subSizer.Add((-1,-1))
3202        subSizer.Add(wx.StaticText(self.panel,wx.ID_ANY,'Parameter name  '))
3203        subSizer.Add(wx.StaticText(self.panel,wx.ID_ANY,'refine?'))
3204        subSizer.Add(wx.StaticText(self.panel,wx.ID_ANY,'value'),0,wx.ALIGN_RIGHT)
3205        explainRefine = False
3206        for name in choiceDict[self.parmChoice]:
3207            # skip entries without numerical values
3208            if isinstance(self.parmDict[name],basestring): continue
3209            if 'Refined' in self.listSel and (name not in self.fullVaryList
3210                                              ) and (name not in self.varyList):
3211                continue
3212            if 'Phase' in self.parmChoice:
3213                if self.phasNum != ' ' and name.split(':')[0] != self.phasNum: continue
3214            if 'Histo' in self.parmChoice:
3215                if self.hisNum != ' ' and name.split(':')[1] != self.hisNum: continue
3216            if (self.varName != ' ') and (self.varName not in name): continue
3217            try:
3218                value = G2py3.FormatSigFigs(self.parmDict[name])
3219            except TypeError:
3220                value = str(self.parmDict[name])+' -?' # unexpected
3221                #continue
3222            v = G2obj.getVarDescr(name)
3223            if v is None or v[-1] is None:
3224                subSizer.Add((-1,-1))
3225            else:               
3226                ch = HelpButton(self.panel,G2obj.fmtVarDescr(name))
3227                subSizer.Add(ch,0,wx.LEFT|wx.RIGHT|WACV|wx.ALIGN_CENTER,1)
3228            subSizer.Add(wx.StaticText(self.panel,wx.ID_ANY,str(name)))
3229            if name in self.varyList:
3230                subSizer.Add(wx.StaticText(self.panel,label='R'))   #TODO? maybe a checkbox for one stop refinemnt flag setting?
3231            elif name in self.fullVaryList:
3232                subSizer.Add(wx.StaticText(self.panel,label='C'))
3233                explainRefine = True
3234            else:
3235                subSizer.Add((-1,-1))
3236            subSizer.Add(wx.StaticText(self.panel,label=value),0,wx.ALIGN_RIGHT)
3237
3238        mainSizer.Add(subSizer,0)
3239        if explainRefine:
3240            mainSizer.Add(
3241                wx.StaticText(self.panel,label='"R" indicates a refined variable\n'+
3242                    '"C" indicates generated from a constraint'),0, wx.ALL,0)
3243        # make OK button
3244        btnsizer = wx.BoxSizer(wx.HORIZONTAL)
3245        btn = wx.Button(self.panel, wx.ID_CLOSE,"Close") 
3246        btn.Bind(wx.EVT_BUTTON,self._onClose)
3247        btnsizer.Add(btn)
3248        mainSizer.Add(btnsizer, 0, wx.ALIGN_CENTER|wx.ALL, 5)
3249        # Allow window to be enlarged but not made smaller
3250        self.panel.SetSizer(mainSizer)
3251        self.panel.SetAutoLayout(1)
3252        self.panel.SetupScrolling()
3253        self.panel.SetMinSize(self.GetSize())
3254
3255    def _onClose(self,event):
3256        self.EndModal(wx.ID_CANCEL)
3257
3258################################################################################
3259#####  Customized Grid Support
3260################################################################################           
3261class GSGrid(wg.Grid):
3262    '''Basic wx.Grid implementation
3263    '''
3264    def __init__(self, parent, name=''):
3265        wg.Grid.__init__(self,parent,-1,name=name)
3266        if hasattr(parent.TopLevelParent,'currentGrids'):
3267            parent.TopLevelParent.currentGrids.append(self)      # save a reference to the grid in the Frame
3268        self.SetScrollRate(0,0)         #GSAS-II grids have no scroll bars by default
3269           
3270    def Clear(self):
3271        wg.Grid.ClearGrid(self)
3272       
3273    def SetCellReadOnly(self,r,c,readonly=True):
3274        self.SetReadOnly(r,c,isReadOnly=readonly)
3275       
3276    def SetCellStyle(self,r,c,color="white",readonly=True):
3277        self.SetCellBackgroundColour(r,c,color)
3278        self.SetReadOnly(r,c,isReadOnly=readonly)
3279       
3280    def GetSelection(self):
3281        #this is to satisfy structure drawing stuff in G2plt when focus changes
3282        return None
3283
3284    def InstallGridToolTip(self, rowcolhintcallback,
3285                           colLblCallback=None,rowLblCallback=None):
3286        '''code to display a tooltip for each item on a grid
3287        from http://wiki.wxpython.org/wxGrid%20ToolTips (buggy!), expanded to
3288        column and row labels using hints from
3289        https://groups.google.com/forum/#!topic/wxPython-users/bm8OARRVDCs
3290
3291        :param function rowcolhintcallback: a routine that returns a text
3292          string depending on the selected row and column, to be used in
3293          explaining grid entries.
3294        :param function colLblCallback: a routine that returns a text
3295          string depending on the selected column, to be used in
3296          explaining grid columns (if None, the default), column labels
3297          do not get a tooltip.
3298        :param function rowLblCallback: a routine that returns a text
3299          string depending on the selected row, to be used in
3300          explaining grid rows (if None, the default), row labels
3301          do not get a tooltip.
3302        '''
3303        prev_rowcol = [None,None,None]
3304        def OnMouseMotion(event):
3305            # event.GetRow() and event.GetCol() would be nice to have here,
3306            # but as this is a mouse event, not a grid event, they are not
3307            # available and we need to compute them by hand.
3308            x, y = self.CalcUnscrolledPosition(event.GetPosition())
3309            row = self.YToRow(y)
3310            col = self.XToCol(x)
3311            hinttext = ''
3312            win = event.GetEventObject()
3313            if [row,col,win] == prev_rowcol: # no change from last position
3314                if event: event.Skip()
3315                return
3316            if win == self.GetGridWindow() and row >= 0 and col >= 0:
3317                hinttext = rowcolhintcallback(row, col)
3318            elif win == self.GetGridColLabelWindow() and col >= 0:
3319                if colLblCallback: hinttext = colLblCallback(col)
3320            elif win == self.GetGridRowLabelWindow() and row >= 0:
3321                if rowLblCallback: hinttext = rowLblCallback(row)
3322            else: # this should be the upper left corner, which is empty
3323                if event: event.Skip()
3324                return
3325            if hinttext is None: hinttext = ''
3326            win.SetToolTipString(hinttext)
3327            prev_rowcol[:] = [row,col,win]
3328            if event: event.Skip()
3329
3330        wx.EVT_MOTION(self.GetGridWindow(), OnMouseMotion)
3331        if colLblCallback: wx.EVT_MOTION(self.GetGridColLabelWindow(), OnMouseMotion)
3332        if rowLblCallback: wx.EVT_MOTION(self.GetGridRowLabelWindow(), OnMouseMotion)
3333                                                   
3334################################################################################           
3335class Table(wg.PyGridTableBase):
3336    '''Basic data table for use with GSgrid
3337    '''
3338    def __init__(self, data=[], rowLabels=None, colLabels=None, types = None):
3339        if 'phoenix' in wx.version():
3340            wg.GridTableBase.__init__(self)
3341        else:
3342            wg.PyGridTableBase.__init__(self)
3343        self.colLabels = colLabels
3344        self.rowLabels = rowLabels
3345        self.dataTypes = types
3346        self.data = data
3347       
3348    def AppendRows(self, numRows=1):
3349        self.data.append([])
3350        return True
3351       
3352    def CanGetValueAs(self, row, col, typeName):
3353        if self.dataTypes:
3354            colType = self.dataTypes[col].split(':')[0]
3355            if typeName == colType:
3356                return True
3357            else:
3358                return False
3359        else:
3360            return False
3361
3362    def CanSetValueAs(self, row, col, typeName):
3363        return self.CanGetValueAs(row, col, typeName)
3364
3365    def DeleteRow(self,pos):
3366        data = self.GetData()
3367        self.SetData([])
3368        new = []
3369        for irow,row in enumerate(data):
3370            if irow != pos:
3371                new.append(row)
3372        self.SetData(new)
3373       
3374    def GetColLabelValue(self, col):
3375        if self.colLabels:
3376            return self.colLabels[col]
3377           
3378    def GetData(self):
3379        data = []
3380        for row in range(self.GetNumberRows()):
3381            data.append(self.GetRowValues(row))
3382        return data
3383       
3384    def GetNumberCols(self):
3385        try:
3386            return len(self.colLabels)
3387        except TypeError:
3388            return None
3389       
3390    def GetNumberRows(self):
3391        return len(self.data)
3392       
3393    def GetRowLabelValue(self, row):
3394        if self.rowLabels:
3395            return self.rowLabels[row]
3396       
3397    def GetColValues(self, col):
3398        data = []
3399        for row in range(self.GetNumberRows()):
3400            data.append(self.GetValue(row, col))
3401        return data
3402       
3403    def GetRowValues(self, row):
3404        data = []
3405        for col in range(self.GetNumberCols()):
3406            data.append(self.GetValue(row, col))
3407        return data
3408       
3409    def GetTypeName(self, row, col):
3410        try:
3411            if self.data[row][col] is None: return None
3412            return self.dataTypes[col]
3413        except (TypeError,IndexError):
3414            return None
3415
3416    def GetValue(self, row, col):
3417        try:
3418            if self.data[row][col] is None: return ""
3419            return self.data[row][col]
3420        except IndexError:
3421            return None
3422           
3423    def InsertRows(self, pos, rows):
3424        for row in range(rows):
3425            self.data.insert(pos,[])
3426            pos += 1
3427       
3428    def IsEmptyCell(self,row,col):
3429        try:
3430            return not self.data[row][col]
3431        except IndexError:
3432            return True
3433       
3434    def OnKeyPress(self, event):
3435        dellist = self.GetSelectedRows()
3436        if event.GetKeyCode() == wx.WXK_DELETE and dellist:
3437            grid = self.GetView()
3438            for i in dellist: grid.DeleteRow(i)
3439               
3440    def SetColLabelValue(self, col, label):
3441        numcols = self.GetNumberCols()
3442        if col > numcols-1:
3443            self.colLabels.append(label)
3444        else:
3445            self.colLabels[col]=label
3446       
3447    def SetData(self,data):
3448        for row in range(len(data)):
3449            self.SetRowValues(row,data[row])
3450               
3451    def SetRowLabelValue(self, row, label):
3452        self.rowLabels[row]=label
3453           
3454    def SetRowValues(self,row,data):
3455        self.data[row] = data
3456           
3457    def SetValue(self, row, col, value):
3458        def innerSetValue(row, col, value):
3459            try:
3460                self.data[row][col] = value
3461            except TypeError:
3462                return
3463            except IndexError: # has this been tested?
3464                #print row,col,value
3465                # add a new row
3466                if row > self.GetNumberRows():
3467                    self.data.append([''] * self.GetNumberCols())
3468                elif col > self.GetNumberCols():
3469                    for row in range(self.GetNumberRows()): # bug fixed here
3470                        self.data[row].append('')
3471                #print self.data
3472                self.data[row][col] = value
3473        innerSetValue(row, col, value)
3474
3475################################################################################
3476class GridFractionEditor(wg.PyGridCellEditor):
3477    '''A grid cell editor class that allows entry of values as fractions as well
3478    as sine and cosine values [as s() and c()]
3479    '''
3480    def __init__(self,grid):
3481        if 'phoenix' in wx.version():
3482            wg.GridCellEditor.__init__(self)
3483        else:
3484            wg.PyGridCellEditor.__init__(self)
3485
3486    def Create(self, parent, id, evtHandler):
3487        self._tc = wx.TextCtrl(parent, id, "")
3488        self._tc.SetInsertionPoint(0)
3489        self.SetControl(self._tc)
3490
3491        if evtHandler:
3492            self._tc.PushEventHandler(evtHandler)
3493
3494        self._tc.Bind(wx.EVT_CHAR, self.OnChar)
3495
3496    def SetSize(self, rect):
3497        self._tc.SetDimensions(rect.x, rect.y, rect.width+2, rect.height+2,
3498                               wx.SIZE_ALLOW_MINUS_ONE)
3499
3500    def BeginEdit(self, row, col, grid):
3501        self.startValue = grid.GetTable().GetValue(row, col)
3502        self._tc.SetValue(str(self.startValue))
3503        self._tc.SetInsertionPointEnd()
3504        self._tc.SetFocus()
3505        self._tc.SetSelection(0, self._tc.GetLastPosition())
3506
3507    def EndEdit(self, row, col, grid, oldVal=None):
3508        changed = False
3509
3510        self.nextval = self.startValue
3511        val = self._tc.GetValue().lower().strip()
3512        if val != self.startValue:
3513            changed = True
3514            neg = False
3515            if val.startswith('-'):
3516                neg = True
3517                val = val[1:]
3518            # allow old GSAS s20 and c20 etc for sind(20) and cosd(20)
3519            if val.startswith('s') and '(' not in val:
3520                val = 'sind('+val.strip('s')+')'
3521            elif val.startswith('c') and '(' not in val:
3522                val = 'cosd('+val.strip('c')+')'
3523            if neg:
3524                val = '-' + val
3525            val = G2py3.FormulaEval(val)
3526            if val is not None:
3527                self.nextval = val
3528            else:
3529                return None
3530            if oldVal is None: # this arg appears in 2.9+; before, we should go ahead & change the table
3531                grid.GetTable().SetValue(row, col, val) # update the table
3532            # otherwise self.ApplyEdit gets called
3533
3534        self.startValue = ''
3535        self._tc.SetValue('')
3536        return changed
3537   
3538    def ApplyEdit(self, row, col, grid):
3539        """ Called only in wx >= 2.9
3540        Save the value of the control into the grid if EndEdit() returns as True
3541        """
3542        grid.GetTable().SetValue(row, col, self.nextval) # update the table
3543
3544    def Reset(self):
3545        self._tc.SetValue(self.startValue)
3546        self._tc.SetInsertionPointEnd()
3547
3548    def Clone(self,grid):
3549        return GridFractionEditor(grid)
3550
3551    def StartingKey(self, evt):
3552        self.OnChar(evt)
3553        if evt.GetSkipped():
3554            self._tc.EmulateKeyPress(evt)
3555
3556    def OnChar(self, evt):
3557        key = evt.GetKeyCode()
3558        if key < 32 or key >= 127:
3559            evt.Skip()
3560        elif chr(key).lower() in '.+-*/0123456789cosind()':
3561            evt.Skip()
3562        else:
3563            evt.StopPropagation()
3564           
3565################################################################################
3566#####  Customized Notebook
3567################################################################################           
3568class GSNoteBook(wx.aui.AuiNotebook):
3569    '''Notebook used in various locations; implemented with wx.aui extension
3570    '''
3571    def __init__(self, parent, name='',size = None,style=wx.aui.AUI_NB_TOP |
3572        wx.aui.AUI_NB_SCROLL_BUTTONS):
3573        wx.aui.AuiNotebook.__init__(self, parent, style=style)
3574        if size: self.SetSize(size)
3575        self.parent = parent
3576        self.PageChangeHandler = None
3577       
3578    def PageChangeEvent(self,event):
3579        pass
3580#        G2frame = self.parent.G2frame
3581#        page = event.GetSelection()
3582#        if self.PageChangeHandler:
3583#            if log.LogInfo['Logging']:
3584#                log.MakeTabLog(
3585#                    G2frame.dataWindow.GetTitle(),
3586#                    G2frame.dataDisplay.GetPageText(page)
3587#                    )
3588#            self.PageChangeHandler(event)
3589           
3590#    def Bind(self,eventtype,handler,*args,**kwargs):
3591#        '''Override the Bind() function so that page change events can be trapped
3592#        '''
3593#        if eventtype == wx.aui.EVT_AUINOTEBOOK_PAGE_CHANGED:
3594#            self.PageChangeHandler = handler
3595#            wx.aui.AuiNotebook.Bind(self,eventtype,self.PageChangeEvent)
3596#            return
3597#        wx.aui.AuiNotebook.Bind(self,eventtype,handler,*args,**kwargs)
3598                                                     
3599    def Clear(self):       
3600        GSNoteBook.DeleteAllPages(self)
3601       
3602    def FindPage(self,name):
3603        numPage = self.GetPageCount()
3604        for page in range(numPage):
3605            if self.GetPageText(page) == name:
3606                return page
3607        return None
3608
3609    def ChangeSelection(self,page):
3610        # in wx.Notebook ChangeSelection is like SetSelection, but it
3611        # does not invoke the event related to pressing the tab button
3612        # I don't see a way to do that in aui.
3613        oldPage = self.GetSelection()
3614        self.SetSelection(page)
3615        return oldPage
3616
3617    # def __getattribute__(self,name):
3618    #     '''This method provides a way to print out a message every time
3619    #     that a method in a class is called -- to see what all the calls
3620    #     might be, or where they might be coming from.
3621    #     Cute trick for debugging!
3622    #     '''
3623    #     attr = object.__getattribute__(self, name)
3624    #     if hasattr(attr, '__call__'):
3625    #         def newfunc(*args, **kwargs):
3626    #             print('GSauiNoteBook calling %s' %attr.__name__)
3627    #             result = attr(*args, **kwargs)
3628    #             return result
3629    #         return newfunc
3630    #     else:
3631    #         return attr
3632           
3633################################################################################
3634#### Help support routines
3635################################################################################
3636class MyHelp(wx.Menu):
3637    '''
3638    A class that creates the contents of a help menu.
3639    The menu will start with two entries:
3640
3641    * 'Help on <helpType>': where helpType is a reference to an HTML page to
3642      be opened
3643    * About: opens an About dialog using OnHelpAbout. N.B. on the Mac this
3644      gets moved to the App menu to be consistent with Apple style.
3645
3646    NOTE: for this to work properly with respect to system menus, the title
3647    for the menu must be &Help, or it will not be processed properly:
3648
3649    ::
3650
3651       menu.Append(menu=MyHelp(self,...),title="&Help")
3652
3653    '''
3654    def __init__(self,frame,includeTree=False,morehelpitems=[]):
3655        wx.Menu.__init__(self,'')
3656        self.HelpById = {}
3657        self.frame = frame
3658        self.Append(wx.ID_ABOUT,'&About GSAS-II','')
3659        frame.Bind(wx.EVT_MENU, self.OnHelpAbout, id=wx.ID_ABOUT)
3660        if GSASIIpath.whichsvn():
3661            helpobj = self.Append(wx.ID_ANY,'&Check for updates','')
3662            frame.Bind(wx.EVT_MENU, self.OnCheckUpdates, helpobj)
3663            helpobj = self.Append(wx.ID_ANY,'&Regress to an old GSAS-II version','')
3664            frame.Bind(wx.EVT_MENU, self.OnSelectVersion, helpobj)
3665            # if GSASIIpath.svnTestBranch():
3666            #     msg = "&Switch back to standard GSAS-II version"
3667            # else:
3668            #     msg = "&Switch to test (2frame) GSAS-II version"
3669            # helpobj = self.Append(
3670            #     help='', id=wx.ID_ANY, kind=wx.ITEM_NORMAL,text=msg)
3671            # frame.Bind(wx.EVT_MENU, self.OnSelectBranch, helpobj)
3672        # provide special help topic names for extra items in help menu
3673        for lbl,indx in morehelpitems:
3674            helpobj = self.Append(wx.ID_ANY,lbl,'')
3675            frame.Bind(wx.EVT_MENU, self.OnHelpById, helpobj)
3676            self.HelpById[helpobj.GetId()] = indx
3677        # add help lookup(s) in gsasii.html
3678        self.AppendSeparator()
3679        if includeTree:
3680            helpobj = self.Append(wx.ID_ANY,'Help on Data tree','')
3681            frame.Bind(wx.EVT_MENU, self.OnHelpById, id=helpobj.GetId())
3682            self.HelpById[helpobj.GetId()] = 'Data tree'
3683        helpobj = self.Append(wx.ID_ANY,'Help on current data tree item','')
3684        frame.Bind(wx.EVT_MENU, self.OnHelpById, id=helpobj.GetId())
3685       
3686    def OnHelpById(self,event):
3687        '''Called when Help on... is pressed in a menu. Brings up a web page
3688        for documentation. Uses the helpKey value from the dataWindow window
3689        unless a special help key value has been defined for this menu id in
3690        self.HelpById
3691
3692        Note that self should now (2frame) be child of the main window (G2frame)
3693        '''
3694        if hasattr(self.frame,'dataWindow'):  # Debug code: check this is called from menu in G2frame
3695            # should always be true in 2 Frame version
3696            dW = self.frame.dataWindow
3697        else:
3698            print('help error: not called from standard menu?')
3699            print (self)
3700            return           
3701        try:
3702            helpKey = dW.helpKey # look up help from helpKey in data window
3703            #if GSASIIpath.GetConfigValue('debug'): print 'dataWindow help: key=',helpKey
3704        except AttributeError:
3705            helpKey = ''
3706            if GSASIIpath.GetConfigValue('debug'): print('No helpKey for current dataWindow!')
3707        helpType = self.HelpById.get(event.GetId(),helpKey) # see if there is a special help topic
3708        #if GSASIIpath.GetConfigValue('debug'): print 'helpKey=',helpKey,'  helpType=',helpType
3709        if helpType == 'Tutorials':
3710            dlg = OpenTutorial(self.frame)
3711            dlg.ShowModal()
3712            dlg.Destroy()
3713            return
3714        else:
3715            ShowHelp(helpType,self.frame)
3716
3717    def OnHelpAbout(self, event):
3718        "Display an 'About GSAS-II' box"
3719        import GSASII
3720        try:
3721            import wx.adv as wxadv  # AboutBox moved here in Phoenix
3722        except:
3723            wxadv = wx
3724        info = wxadv.AboutDialogInfo()
3725        info.Name = 'GSAS-II'
3726        ver = GSASIIpath.svnGetRev()
3727        if not ver:
3728            ver = GSASIIpath.GetVersionNumber()
3729        info.SetVersion(ver)
3730        #info.Developers = ['Robert B. Von Dreele','Brian H. Toby']
3731        info.Copyright = ('(c) ' + time.strftime('%Y') +
3732''' Argonne National Laboratory
3733This product includes software developed
3734by the UChicago Argonne, LLC, as
3735Operator of Argonne National Laboratory.''')
3736        info.Description = '''General Structure Analysis System-II (GSAS-II)
3737Robert B. Von Dreele and Brian H. Toby
3738
3739Please cite as:
3740  B.H. Toby & R.B. Von Dreele, J. Appl. Cryst. 46, 544-549 (2013)
3741For small angle use cite:
3742  R.B. Von Dreele, J. Appl. Cryst. 47, 1748-9 (2014)
3743For DIFFaX use cite:
3744  M.M.J. Treacy, J.M. Newsam & M.W. Deem,
3745  Proc. Roy. Soc. Lond. A 433, 499-520 (1991)
3746'''
3747        info.WebSite = ("https://subversion.xray.aps.anl.gov/trac/pyGSAS","GSAS-II home page")
3748        wxadv.AboutBox(info)
3749
3750    def OnCheckUpdates(self,event):
3751        '''Check if the GSAS-II repository has an update for the current source files
3752        and perform that update if requested.
3753        '''           
3754        if not GSASIIpath.whichsvn():
3755            dlg = wx.MessageDialog(self.frame,
3756                                   'No Subversion','Cannot update GSAS-II because subversion (svn) was not found.',
3757                                   wx.OK)
3758            dlg.ShowModal()
3759            dlg.Destroy()
3760            return
3761        wx.BeginBusyCursor()
3762        local = GSASIIpath.svnGetRev()
3763        if local is None: 
3764            wx.EndBusyCursor()
3765            dlg = wx.MessageDialog(self.frame,
3766                                   'Unable to run subversion on the GSAS-II current directory. Is GSAS-II installed correctly?',
3767                                   'Subversion error',
3768                                   wx.OK)
3769            dlg.ShowModal()
3770            dlg.Destroy()
3771            return
3772        print ('Installed GSAS-II version: '+local)
3773        repos = GSASIIpath.svnGetRev(local=False)
3774        wx.EndBusyCursor()
3775        # has the current branch disappeared? If so, switch to the trunk -- not fully tested
3776        if (repos is None and "not found" in GSASIIpath.svnLastError.lower()
3777            and "path" in GSASIIpath.svnLastError.lower()):
3778            print('Repository is gone, will switch to trunk')
3779            GSASIIpath.svnSwitch2branch()
3780            return
3781        elif repos is None: 
3782            dlg = wx.MessageDialog(self.frame,
3783                                   'Unable to access the GSAS-II server. Is this computer on the internet?',
3784                                   'Server unavailable',
3785                                   wx.OK)
3786            dlg.ShowModal()
3787            dlg.Destroy()
3788            return
3789        print ('GSAS-II version on server: '+repos)
3790        if local == repos:
3791            dlg = wx.MessageDialog(self.frame,
3792                                   'GSAS-II is up-to-date. Version '+local+' is already loaded.',
3793                                   'GSAS-II Up-to-date',
3794                                   wx.OK)
3795            dlg.ShowModal()
3796            dlg.Destroy()
3797            return
3798        mods = GSASIIpath.svnFindLocalChanges()
3799        if mods:
3800            dlg = wx.MessageDialog(self.frame,
3801                                   'You have version '+local+
3802                                   ' of GSAS-II installed, but the current version is '+repos+
3803                                   '. However, '+str(len(mods))+
3804                                   ' file(s) on your local computer have been modified.'
3805                                   ' Updating will attempt to merge your local changes with '
3806                                   'the latest GSAS-II version, but if '
3807                                   'conflicts arise, local changes will be '
3808                                   'discarded. It is also possible that the '
3809                                   'local changes my prevent GSAS-II from running. '
3810                                   'Press OK to start an update if this is acceptable:',
3811                                   'Local GSAS-II Mods',
3812                                   wx.OK|wx.CANCEL)
3813            if dlg.ShowModal() != wx.ID_OK:
3814                dlg.Destroy()
3815                return
3816            else:
3817                dlg.Destroy()
3818        else:
3819            dlg = wx.MessageDialog(self.frame,
3820                                   'You have version '+local+
3821                                   ' of GSAS-II installed, but the current version is '+repos+
3822                                   '. Press OK to start an update:',
3823                                   'GSAS-II Updates',
3824                                   wx.OK|wx.CANCEL)
3825            if dlg.ShowModal() != wx.ID_OK:
3826                dlg.Destroy()
3827                return
3828            dlg.Destroy()
3829        print ('start updates')
3830        dlg = wx.MessageDialog(self.frame,
3831                               'Your project will now be saved, GSAS-II will exit and an update '
3832                               'will be performed and GSAS-II will restart. Press Cancel to '
3833                               'abort the update',
3834                               'Start update?',
3835                               wx.OK|wx.CANCEL)
3836        if dlg.ShowModal() != wx.ID_OK:
3837            dlg.Destroy()
3838            return
3839        dlg.Destroy()
3840        self.frame.OnFileSave(event)
3841        GPX = self.frame.GSASprojectfile
3842        GSASIIpath.svnUpdateProcess(projectfile=GPX)
3843        return
3844
3845    def OnSelectVersion(self,event):
3846        '''Allow the user to select a specific version of GSAS-II
3847        '''
3848        if not GSASIIpath.whichsvn():
3849            dlg = wx.MessageDialog(self,'No Subversion','Cannot update GSAS-II because subversion (svn) '+
3850                                   'was not found.'
3851                                   ,wx.OK)
3852            dlg.ShowModal()
3853            return
3854        local = GSASIIpath.svnGetRev()
3855        if local is None: 
3856            dlg = wx.MessageDialog(self.frame,
3857                                   'Unable to run subversion on the GSAS-II current directory. Is GSAS-II installed correctly?',
3858                                   'Subversion error',
3859                                   wx.OK)
3860            dlg.ShowModal()
3861            dlg.Destroy()
3862            return
3863        mods = GSASIIpath.svnFindLocalChanges()
3864        if mods:
3865            dlg = wx.MessageDialog(self.frame,
3866                                   'You have version '+local+
3867                                   ' of GSAS-II installed'
3868                                   '. However, '+str(len(mods))+
3869                                   ' file(s) on your local computer have been modified.'
3870                                   ' Downdating will attempt to merge your local changes with '
3871                                   'the selected GSAS-II version. '
3872                                   'Downdating is not encouraged because '
3873                                   'if merging is not possible, your local changes will be '
3874                                   'discarded. It is also possible that the '
3875                                   'local changes my prevent GSAS-II from running. '
3876                                   'Press OK to continue anyway.',
3877                                   'Local GSAS-II Mods',
3878                                   wx.OK|wx.CANCEL)
3879            if dlg.ShowModal() != wx.ID_OK:
3880                dlg.Destroy()
3881                return
3882            dlg.Destroy()
3883        if GSASIIpath.svnGetRev(local=False) is None:
3884            dlg = wx.MessageDialog(self.frame,
3885                                   'Error obtaining current GSAS-II version. Is internet access working correctly?',
3886                                   'Subversion error',
3887                                   wx.OK)
3888            dlg.ShowModal()
3889            dlg.Destroy()
3890            return
3891        dlg = downdate(parent=self.frame)
3892        if dlg.ShowModal() == wx.ID_OK:
3893            ver = dlg.getVersion()
3894        else:
3895            dlg.Destroy()
3896            return
3897        dlg.Destroy()
3898        print('start regress to '+str(ver))
3899        self.frame.OnFileSave(event)
3900        GPX = self.frame.GSASprojectfile
3901        GSASIIpath.svnUpdateProcess(projectfile=GPX,version=str(ver))
3902        return
3903
3904    # def OnSelectBranch(self,event):
3905    #     '''Allow the user to select branch of GSAS-II or return to trunk
3906    #     N.B. Name of branch to use is hard-coded here. Must contain a slash
3907    #     '''
3908    #     testbranch = '/branch/2frame'
3909    #     if not GSASIIpath.svnTestBranch():
3910    #         dlg = wx.MessageDialog(self.frame,
3911    #                                'Switching to test GSAS-II version',
3912    #                                'Confirm Switch',
3913    #                                wx.OK|wx.CANCEL)
3914    #         if dlg.ShowModal() != wx.ID_OK: return
3915    #         branch = testbranch
3916    #     else:
3917    #         dlg = wx.MessageDialog(self.frame,
3918    #                                'Switching back to standard GSAS-II version',
3919    #                                'Confirm Switch',
3920    #                                wx.OK|wx.CANCEL)
3921    #         if dlg.ShowModal() != wx.ID_OK: return
3922    #         branch = 'trunk'
3923    #     print('start switch')
3924    #     self.frame.OnFileSave(event)
3925    #     GPX = self.frame.GSASprojectfile
3926    #     GSASIIpath.svnUpdateProcess(projectfile=GPX,branch=branch)
3927
3928################################################################################
3929class HelpButton(wx.Button):
3930    '''Create a help button that displays help information.
3931    The text is displayed in a modal message window.
3932
3933    TODO: it might be nice if it were non-modal: e.g. it stays around until
3934    the parent is deleted or the user closes it, but this did not work for
3935    me.
3936
3937    :param parent: the panel which will be the parent of the button
3938    :param str msg: the help text to be displayed
3939    '''
3940    def __init__(self,parent,msg):
3941        if sys.platform == "darwin": 
3942            wx.Button.__init__(self,parent,wx.ID_HELP)
3943        else:
3944            wx.Button.__init__(self,parent,wx.ID_ANY,'?',style=wx.BU_EXACTFIT)
3945        self.Bind(wx.EVT_BUTTON,self._onPress)
3946        self.msg=StripIndents(msg)
3947        self.parent = parent
3948    def _onClose(self,event):
3949        self.dlg.EndModal(wx.ID_CANCEL)
3950    def _onPress(self,event):
3951        'Respond to a button press by displaying the requested text'
3952        #dlg = wx.MessageDialog(self.parent,self.msg,'Help info',wx.OK)
3953        self.dlg = wx.Dialog(self.parent,wx.ID_ANY,'Help information', 
3954                        style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)
3955        #self.dlg.SetBackgroundColour(wx.WHITE)
3956        mainSizer = wx.BoxSizer(wx.VERTICAL)
3957        txt = wx.StaticText(self.dlg,wx.ID_ANY,self.msg)
3958        mainSizer.Add(txt,1,wx.ALL|wx.EXPAND,10)
3959        txt.SetBackgroundColour(wx.WHITE)
3960
3961        btnsizer = wx.BoxSizer(wx.HORIZONTAL)
3962        btn = wx.Button(self.dlg, wx.ID_CLOSE) 
3963        btn.Bind(wx.EVT_BUTTON,self._onClose)
3964        btnsizer.Add(btn)
3965        mainSizer.Add(btnsizer, 0, wx.ALIGN_CENTER|wx.ALL, 5)
3966        self.dlg.SetSizer(mainSizer)
3967        mainSizer.Fit(self.dlg)
3968        self.dlg.CenterOnParent()
3969        self.dlg.ShowModal()
3970        self.dlg.Destroy()
3971################################################################################
3972class MyHtmlPanel(wx.Panel):
3973    '''Defines a panel to display HTML help information, as an alternative to
3974    displaying help information in a web browser.
3975    '''
3976    def __init__(self, frame, id):
3977        self.frame = frame
3978        wx.Panel.__init__(self, frame, id)
3979        sizer = wx.BoxSizer(wx.VERTICAL)
3980        back = wx.Button(self, -1, "Back")
3981        back.Bind(wx.EVT_BUTTON, self.OnBack)
3982        self.htmlwin = G2HtmlWindow(self, id, size=(750,450))
3983        sizer.Add(self.htmlwin, 1,wx.EXPAND)
3984        sizer.Add(back, 0, wx.ALIGN_LEFT, 0)
3985        self.SetSizer(sizer)
3986        sizer.Fit(frame)       
3987        self.Bind(wx.EVT_SIZE,self.OnHelpSize)
3988    def OnHelpSize(self,event):         #does the job but weirdly!!
3989        anchor = self.htmlwin.GetOpenedAnchor()
3990        if anchor:           
3991            self.htmlwin.ScrollToAnchor(anchor)
3992            wx.CallAfter(self.htmlwin.ScrollToAnchor,anchor)
3993            if event: event.Skip()
3994    def OnBack(self, event):
3995        self.htmlwin.HistoryBack()
3996    def LoadFile(self,file):
3997        pos = file.rfind('#')
3998        if pos != -1:
3999            helpfile = file[:pos]
4000            helpanchor = file[pos+1:]
4001        else:
4002            helpfile = file
4003            helpanchor = None
4004        self.htmlwin.LoadPage(helpfile)
4005        if helpanchor is not None:
4006            self.htmlwin.ScrollToAnchor(helpanchor)
4007            xs,ys = self.htmlwin.GetViewStart()
4008            self.htmlwin.Scroll(xs,ys-1)
4009################################################################################
4010class G2HtmlWindow(wx.html.HtmlWindow):
4011    '''Displays help information in a primitive HTML browser type window
4012    '''
4013    def __init__(self, parent, *args, **kwargs):
4014        self.parent = parent
4015        wx.html.HtmlWindow.__init__(self, parent, *args, **kwargs)
4016    def LoadPage(self, *args, **kwargs):
4017        wx.html.HtmlWindow.LoadPage(self, *args, **kwargs)
4018        self.TitlePage()
4019    def OnLinkClicked(self, *args, **kwargs):
4020        wx.html.HtmlWindow.OnLinkClicked(self, *args, **kwargs)
4021        xs,ys = self.GetViewStart()
4022        self.Scroll(xs,ys-1)
4023        self.TitlePage()
4024    def HistoryBack(self, *args, **kwargs):
4025        wx.html.HtmlWindow.HistoryBack(self, *args, **kwargs)
4026        self.TitlePage()
4027    def TitlePage(self):
4028        self.parent.frame.SetTitle(self.GetOpenedPage() + ' -- ' + 
4029            self.GetOpenedPageTitle())
4030
4031################################################################################
4032def StripIndents(msg):
4033    'Strip indentation from multiline strings'
4034    msg1 = msg.replace('\n ','\n')
4035    while msg != msg1:
4036        msg = msg1
4037        msg1 = msg.replace('\n ','\n')
4038    return msg.replace('\n\t','\n')
4039
4040def StripUnicode(string,subs='.'):
4041    '''Strip non-ASCII characters from strings
4042   
4043    :param str string: string to strip Unicode characters from
4044    :param str subs: character(s) to place into string in place of each
4045      Unicode character. Defaults to '.'
4046
4047    :returns: a new string with only ASCII characters
4048    '''
4049    s = ''
4050    for c in string:
4051        if ord(c) < 128:
4052            s += c
4053        else:
4054            s += subs
4055    return s.encode('ascii','replace')
4056       
4057################################################################################
4058# configuration routines (for editing config.py)
4059def SaveGPXdirectory(path):
4060    if GSASIIpath.GetConfigValue('Starting_directory') == path: return
4061    vars = GetConfigValsDocs()
4062    try:
4063        vars['Starting_directory'][1] = path
4064        if GSASIIpath.GetConfigValue('debug'): print('Saving GPX path: '+path)
4065        SaveConfigVars(vars)
4066    except KeyError:
4067        pass
4068
4069def SaveImportDirectory(path):
4070    if GSASIIpath.GetConfigValue('Import_directory') == path: return
4071    vars = GetConfigValsDocs()
4072    try:
4073        vars['Import_directory'][1] = path
4074        if GSASIIpath.GetConfigValue('debug'): print('Saving Import path: '+path)
4075        SaveConfigVars(vars)
4076    except KeyError:
4077        pass
4078
4079def GetConfigValsDocs():
4080    '''Reads the module referenced in fname (often <module>.__file__) and
4081    return a dict with names of global variables as keys.
4082    For each global variable, the value contains four items:
4083
4084    :returns: a dict where keys are names defined in module config_example.py
4085      where the value is a list of four items, as follows:
4086
4087         * item 0: the default value
4088         * item 1: the current value
4089         * item 2: the initial value (starts same as item 1)
4090         * item 3: the "docstring" that follows variable definition
4091
4092    '''
4093    import config_example
4094    import ast
4095    fname = os.path.splitext(config_example.__file__)[0]+'.py' # convert .pyc to .py
4096    with open(fname, 'r') as f:
4097        fstr = f.read()
4098    fstr = fstr.replace('\r\n', '\n').replace('\r', '\n')
4099    if not fstr.endswith('\n'):
4100        fstr += '\n'
4101    tree = ast.parse(fstr)
4102    d = {}
4103    key = None
4104    for node in ast.walk(tree):
4105        if isinstance(node,ast.Assign):
4106            key = node.targets[0].id
4107            d[key] = [config_example.__dict__.get(key),
4108                      GSASIIpath.configDict.get(key),
4109                      GSASIIpath.configDict.get(key),'']
4110        elif isinstance(node,ast.Expr) and key:
4111            d[key][3] = node.value.s.strip()
4112        else:
4113            key = None
4114    return d
4115
4116def SaveConfigVars(vars,parent=None):
4117    '''Write the current config variable values to config.py
4118
4119    :params dict vars: a dictionary of variable settings and meanings as
4120      created in :func:`GetConfigValsDocs`.
4121    :param parent: wx.Frame object or None (default) for parent
4122      of error message if no file can be written.
4123    :returns: True if unable to write the file, None otherwise
4124    '''
4125    # try to write to where an old config file is located
4126    try:
4127        import config
4128        savefile = config.__file__
4129    except ImportError: # no config.py file yet
4130        savefile = os.path.join(GSASIIpath.path2GSAS2,'config.py')
4131    # try to open file for write
4132    try:
4133        savefile = os.path.splitext(savefile)[0]+'.py' # convert .pyc to .py
4134        fp = open(savefile,'w')
4135    except IOError:  # can't write there, write in local mods directory
4136        # create a local mods directory, if needed
4137        g2local = os.path.expanduser('~/.G2local/')
4138        if not os.path.exists(g2local):
4139            try:
4140                print(u'Creating directory '+g2local)
4141                os.mkdir(g2local)
4142            except:
4143                if parent:
4144                    G2MessageBox(parent,u'Error trying to create directory '+g2local,
4145                        'Unable to save')
4146                else:
4147                    print(u'Error trying to create directory '+g2local)
4148                return True
4149            sys.path.insert(0,os.path.expanduser('~/.G2local/'))
4150        savefile = os.path.join(os.path.expanduser('~/.G2local/'),'config.py')
4151        try:
4152            fp = open(savefile,'w')
4153        except IOError:
4154            if parent:
4155                G2MessageBox(parent,'Error trying to write configuration to '+savefile,
4156                    'Unable to save')
4157            else:
4158                print('Error trying to write configuration to '+savefile)
4159            return True
4160    import datetime
4161    fp.write("'''\n")
4162    fp.write("*config.py: Configuration options*\n----------------------------------\n")
4163    fp.write("This file created in SelectConfigSetting on {:%d %b %Y %H:%M}\n".
4164             format(datetime.datetime.now()))
4165    fp.write("'''\n\n")
4166    fp.write("import os.path\n")
4167    fp.write("import GSASIIpath\n\n")
4168    for var in sorted(vars.keys(),key=lambda s: s.lower()):
4169        if vars[var][1] is None: continue
4170        if vars[var][1] == '': continue
4171        if vars[var][0] == vars[var][1]: continue
4172        try:
4173            float(vars[var][1]) # test for number
4174            fp.write(var + ' = ' + str(vars[var][1])+'\n')
4175        except:
4176            try:
4177                eval(vars[var][1]) # test for an expression
4178                fp.write(var + ' = ' + str(vars[var][1])+'\n')
4179            except: # must be a string
4180                varstr = vars[var][1]
4181                if '\\' in varstr:
4182                    fp.write(var + ' = os.path.normpath("' + varstr.replace('\\','/') +'")\n')
4183                else:
4184                    fp.write(var + ' = "' + str(varstr)+'"\n')
4185        if vars[var][3]:
4186            fp.write("'''" + str(vars[var][3]) + "\n'''\n\n")
4187    fp.close()
4188    print('wrote file '+savefile)
4189
4190class SelectConfigSetting(wx.Dialog):
4191    '''Dialog to select configuration variables and set associated values.
4192    '''
4193    def __init__(self,parent=None):
4194        style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER
4195        wx.Dialog.__init__(self, parent, wx.ID_ANY, 'Set Config Variable', style=style)
4196        self.sizer = wx.BoxSizer(wx.VERTICAL)
4197        self.vars = GetConfigValsDocs()
4198       
4199        label = wx.StaticText(
4200            self,  wx.ID_ANY,
4201            'Select a GSAS-II configuration variable to change'
4202            )
4203        self.sizer.Add(label, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
4204        self.choice = {}
4205        btn = G2ChoiceButton(self, sorted(self.vars.keys(),key=lambda s: s.lower()),
4206            strLoc=self.choice,strKey=0,onChoice=self.OnSelection)
4207        btn.SetLabel("")
4208        self.sizer.Add(btn)
4209
4210        self.varsizer = wx.BoxSizer(wx.VERTICAL)
4211        self.sizer.Add(self.varsizer,1,wx.ALL|wx.EXPAND,1)
4212       
4213        self.doclbl = wx.StaticBox(self, wx.ID_ANY, "")
4214        self.doclblsizr = wx.StaticBoxSizer(self.doclbl)
4215        self.docinfo = wx.StaticText(self,  wx.ID_ANY, "")
4216        self.doclblsizr.Add(self.docinfo, 0, wx.ALIGN_LEFT|wx.ALL, 5)
4217        self.sizer.Add(self.doclblsizr, 0, wx.ALIGN_LEFT|wx.EXPAND|wx.ALL, 5)
4218        btnsizer = wx.BoxSizer(wx.HORIZONTAL)
4219        self.saveBtn = wx.Button(self,-1,"Save current settings")
4220        btnsizer.Add(self.saveBtn, 0, wx.ALL, 2) 
4221        self.saveBtn.Bind(wx.EVT_BUTTON, self.OnSave)
4222        self.saveBtn.Enable(False)
4223        self.applyBtn = wx.Button(self,-1,"Use current (no save)")
4224        btnsizer.Add(self.applyBtn, 0, wx.ALL, 2) 
4225        self.applyBtn.Bind(wx.EVT_BUTTON, self.OnApplyChanges)
4226        self.applyBtn.Enable(False)
4227       
4228        btn = wx.Button(self,wx.ID_CANCEL)
4229        btnsizer.Add(btn, 0, wx.ALL, 2) 
4230        self.sizer.Add(btnsizer, 0, wx.ALIGN_CENTRE|wx.ALL, 5) 
4231               
4232        self.SetSizer(self.sizer)
4233        self.sizer.Fit(self)
4234        self.CenterOnParent()
4235       
4236    def OnChange(self,event=None):
4237        ''' Check if anything been changed. Turn the save button on/off.
4238        '''
4239        for var in self.vars:
4240            if self.vars[var][0] is None and self.vars[var][1] is not None:
4241                # make blank strings into None, if that is the default
4242                if self.vars[var][1].strip() == '': self.vars[var][1] = None
4243            if self.vars[var][1] != self.vars[var][2]:
4244                #print 'changed',var,self.vars[var][:3]
4245                self.saveBtn.Enable(True)
4246                self.applyBtn.Enable(True)
4247                break
4248        else:
4249            self.saveBtn.Enable(False)
4250            self.applyBtn.Enable(False)
4251        try:
4252            self.resetBtn.Enable(True)
4253        except:
4254            pass
4255       
4256    def OnApplyChanges(self,event=None):
4257        'Set config variables to match the current settings'
4258        GSASIIpath.SetConfigValue(self.vars)
4259        self.EndModal(wx.ID_OK)
4260        import GSASIImpsubs as G2mp
4261        G2mp.ResetMP()
4262       
4263    def OnSave(self,event):
4264        '''Write the config variables to config.py and then set them
4265        as the current settings
4266        '''
4267        if not SaveConfigVars(self.vars,parent=self):
4268            self.OnApplyChanges() # force a reload of the config settings
4269        else:
4270            self.EndModal(wx.ID_OK)
4271
4272    def OnBoolSelect(self,event):
4273        'Respond to a change in a True/False variable'
4274        rb = event.GetEventObject()
4275        var = self.choice[0]
4276        self.vars[var][1] = (rb.GetSelection() == 0)
4277        self.OnChange()
4278        wx.CallAfter(self.OnSelection)
4279       
4280    def onSelDir(self,event):
4281        'Select a directory from a menu'
4282        dlg = wx.DirDialog(self, "Choose a directory:",style=wx.DD_DEFAULT_STYLE)
4283        if dlg.ShowModal() == wx.ID_OK:
4284            var = self.choice[0]
4285            self.vars[var][1] = dlg.GetPath()
4286            self.strEd.SetValue(self.vars[var][1])
4287            self.OnChange()
4288        dlg.Destroy()
4289       
4290    def OnSelection(self):
4291        'show a selected variable'
4292        def OnNewColorBar(event):
4293            self.vars['Contour_color'][1] = self.colSel.GetValue()
4294            self.OnChange(event)
4295
4296        if 'phoenix' in wx.version():
4297            self.varsizer.Clear(True)
4298        else:
4299            self.varsizer.DeleteWindows()
4300        var = self.choice[0]
4301        showdef = True
4302        if var not in self.vars:
4303            raise Exception("How did this happen?")
4304        if type(self.vars[var][0]) is int:
4305            ed = ValidatedTxtCtrl(self,self.vars[var],1,typeHint=int,OKcontrol=self.OnChange)
4306            self.varsizer.Add(ed, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
4307        elif type(self.vars[var][0]) is float:
4308            ed = ValidatedTxtCtrl(self,self.vars[var],1,typeHint=float,OKcontrol=self.OnChange)
4309            self.varsizer.Add(ed, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
4310        elif type(self.vars[var][0]) is bool:
4311            showdef = False
4312            lbl = "value for "+var
4313            ch = []
4314            for i,v in enumerate((True,False)):
4315                s = str(v)
4316                if v == self.vars[var][0]:
4317                    defopt = i
4318                    s += ' (default)'
4319                ch += [s]
4320            rb = wx.RadioBox(self, wx.ID_ANY, lbl, wx.DefaultPosition, wx.DefaultSize,
4321                ch, 1, wx.RA_SPECIFY_COLS)
4322            # set initial value
4323            if self.vars[var][1] is None:
4324                rb.SetSelection(defopt)
4325            elif self.vars[var][1]:
4326                rb.SetSelection(0)
4327            else:
4328                rb.SetSelection(1)
4329            rb.Bind(wx.EVT_RADIOBOX,self.OnBoolSelect)
4330            self.varsizer.Add(rb, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
4331        else:
4332            if var.endswith('_directory') or var.endswith('_location'):
4333                btn = wx.Button(self,wx.ID_ANY,'Select from dialog...')
4334                sz = (400,-1)
4335            else:
4336                btn = None
4337                sz = (250,-1)
4338            if var == 'Contour_color':
4339                if self.vars[var][1] is None:
4340                    self.vars[var][1] = 'Paired'
4341                colorList = sorted([m for m in mpl.cm.datad.keys() ],key=lambda s: s.lower())   #if not m.endswith("_r")
4342                self.colSel = wx.ComboBox(self,value=self.vars[var][1],choices=colorList,
4343                    style=wx.CB_READONLY|wx.CB_DROPDOWN)
4344                self.colSel.Bind(wx.EVT_COMBOBOX, OnNewColorBar)
4345                self.varsizer.Add(self.colSel, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
4346            else:
4347                self.strEd = ValidatedTxtCtrl(self,self.vars[var],1,typeHint=str,
4348                    OKcontrol=self.OnChange,size=sz)
4349                if self.vars[var][1] is not None:
4350                    self.strEd.SetValue(self.vars[var][1])
4351                self.varsizer.Add(self.strEd, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
4352            if btn:
4353                btn.Bind(wx.EVT_BUTTON,self.onSelDir)
4354                self.varsizer.Add(btn, 0, wx.ALIGN_CENTRE|wx.ALL, 5) 
4355        # button for reset to default value
4356        lbl = "Reset to Default"
4357        if showdef: # spell out default when needed
4358            lbl += ' (='+str(self.vars[var][0])+')'
4359            #label = wx.StaticText(self,  wx.ID_ANY, 'Default value = '+str(self.vars[var][0]))
4360            #self.varsizer.Add(label, 0, wx.ALIGN_LEFT|wx.ALL, 5)
4361        self.resetBtn = wx.Button(self,-1,lbl)
4362        self.resetBtn.Bind(wx.EVT_BUTTON, self.OnClear)
4363        if self.vars[var][1] is not None and self.vars[var][1] != '': # show current value, if one
4364            #label = wx.StaticText(self,  wx.ID_ANY, 'Current value = '+str(self.vars[var][1]))
4365            #self.varsizer.Add(label, 0, wx.ALIGN_LEFT|wx.ALL, 5)
4366            self.resetBtn.Enable(True)
4367        else:
4368            self.resetBtn.Enable(False)
4369        self.varsizer.Add(self.resetBtn, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
4370        # show meaning, if defined
4371        self.doclbl.SetLabel("Description of "+str(var)) 
4372        if self.vars[var][3]:
4373            self.docinfo.SetLabel(self.vars[var][3])
4374        else:
4375            self.docinfo.SetLabel("(not documented)")
4376        self.sizer.Fit(self)
4377        self.CenterOnParent()
4378        wx.CallAfter(self.SendSizeEvent)
4379
4380    def OnClear(self, event):
4381        var = self.choice[0]
4382        self.vars[var][1] = self.vars[var][0]
4383        self.OnChange()
4384        wx.CallAfter(self.OnSelection)
4385       
4386################################################################################
4387class downdate(wx.Dialog):
4388    '''Dialog to allow a user to select a version of GSAS-II to install
4389    '''
4390    def __init__(self,parent=None):
4391        style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER
4392        wx.Dialog.__init__(self, parent, wx.ID_ANY, 'Select Version', style=style)
4393        pnl = wx.Panel(self)
4394        sizer = wx.BoxSizer(wx.VERTICAL)
4395        insver = GSASIIpath.svnGetRev(local=True)
4396        curver = int(GSASIIpath.svnGetRev(local=False))
4397        label = wx.StaticText(
4398            pnl,  wx.ID_ANY,
4399            'Select a specific GSAS-II version to install'
4400            )
4401        sizer.Add(label, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
4402        sizer1 = wx.BoxSizer(wx.HORIZONTAL)
4403        sizer1.Add(
4404            wx.StaticText(pnl,  wx.ID_ANY,
4405                          'Currently installed version: '+str(insver)),
4406            0, wx.ALIGN_CENTRE|wx.ALL, 5)
4407        sizer.Add(sizer1)
4408        sizer1 = wx.BoxSizer(wx.HORIZONTAL)
4409        sizer1.Add(
4410            wx.StaticText(pnl,  wx.ID_ANY,
4411                          'Select GSAS-II version to install: '),
4412            0, wx.ALIGN_CENTRE|wx.ALL, 5)
4413        self.spin = wx.SpinCtrl(pnl, wx.ID_ANY,size=(150,-1))
4414        self.spin.SetRange(1, curver)
4415        self.spin.SetValue(curver)
4416        self.Bind(wx.EVT_SPINCTRL, self._onSpin, self.spin)
4417        self.Bind(wx.EVT_KILL_FOCUS, self._onSpin, self.spin)
4418        sizer1.Add(self.spin)
4419        sizer.Add(sizer1)
4420
4421        line = wx.StaticLine(pnl,-1, size=(-1,3), style=wx.LI_HORIZONTAL)
4422        sizer.Add(line, 0, wx.EXPAND|wx.ALIGN_CENTER|wx.ALL, 10)
4423
4424        self.text = wx.StaticText(pnl,  wx.ID_ANY, "")
4425        sizer.Add(self.text, 0, wx.ALIGN_LEFT|wx.EXPAND|wx.ALL, 5)
4426
4427        line = wx.StaticLine(pnl,-1, size=(-1,3), style=wx.LI_HORIZONTAL)
4428        sizer.Add(line, 0, wx.EXPAND|wx.ALIGN_CENTER|wx.ALL, 10)
4429        sizer.Add(
4430            wx.StaticText(
4431                pnl,  wx.ID_ANY,
4432                'If "Install" is pressed, your project will be saved;\n'
4433                'GSAS-II will exit; The specified version will be loaded\n'
4434                'and GSAS-II will restart. Press "Cancel" to abort.'),
4435            0, wx.EXPAND|wx.ALL, 10)
4436        btnsizer = wx.StdDialogButtonSizer()
4437        btn = wx.Button(pnl, wx.ID_OK, "Install")
4438        btn.SetDefault()
4439        btnsizer.AddButton(btn)
4440        btn = wx.Button(pnl, wx.ID_CANCEL)
4441        btnsizer.AddButton(btn)
4442        btnsizer.Realize()
4443        sizer.Add(btnsizer, 0, wx.ALIGN_CENTER|wx.ALL, 5)
4444        pnl.SetSizer(sizer)
4445        sizer.Fit(self)
4446        self.topsizer=sizer
4447        self.CenterOnParent()
4448        self._onSpin(None)
4449
4450    def _onSpin(self,event):
4451        'Called to load info about the selected version in the dialog'
4452        if event: event.Skip()
4453        ver = self.spin.GetValue()
4454        d = GSASIIpath.svnGetLog(version=ver)
4455        date = d.get('date','?').split('T')[0]
4456        s = '(Version '+str(ver)+' created '+date
4457        s += ' by '+d.get('author','?')+')'
4458        msg = d.get('msg')
4459        if msg: s += '\n\nComment: '+msg
4460        self.text.SetLabel(s)
4461        self.topsizer.Fit(self)
4462
4463    def getVersion(self):
4464        'Get the version number in the dialog'
4465        return self.spin.GetValue()
4466
4467################################################################################
4468#### Display Help information
4469################################################################################
4470# define some globals
4471htmlPanel = None
4472htmlFrame = None
4473htmlFirstUse = True
4474#helpLocDict = {}  # to be implemented if we ever split gsasii.html over multiple files
4475path2GSAS2 = os.path.dirname(os.path.realpath(__file__)) # save location of this file
4476def ShowHelp(helpType,frame):
4477    '''Called to bring up a web page for documentation.'''
4478    global htmlFirstUse,htmlPanel,htmlFrame
4479    # no defined link to use, create a default based on key
4480    helplink = 'gsasII.html'
4481    if helpType:
4482        helplink += '#'+helpType.replace(')','').replace('(','_').replace(' ','_')
4483    # determine if a web browser or the internal viewer should be used for help info
4484    if GSASIIpath.GetConfigValue('Help_mode'):
4485        helpMode = GSASIIpath.GetConfigValue('Help_mode')
4486    else:
4487        helpMode = 'browser'
4488    if helpMode == 'internal':
4489        helplink = os.path.join(path2GSAS2,'help',helplink)
4490        try:
4491            htmlPanel.LoadFile(helplink)
4492            htmlFrame.Raise()
4493        except:
4494            htmlFrame = wx.Frame(frame, -1, size=(610, 510))
4495            htmlFrame.Show(True)
4496            htmlFrame.SetTitle("HTML Window") # N.B. reset later in LoadFile
4497            htmlPanel = MyHtmlPanel(htmlFrame,-1)
4498            htmlPanel.LoadFile(helplink)
4499    else:
4500        if sys.platform == "darwin": # for Mac, force use of safari to preserve anchors on file URLs
4501            wb = webbrowser.MacOSXOSAScript('safari')
4502        else:
4503            wb = webbrowser
4504        helplink = os.path.join(path2GSAS2,'help',helplink)
4505        pfx = "file://"
4506        if sys.platform.lower().startswith('win'):
4507            pfx = ''
4508        #if GSASIIpath.GetConfigValue('debug'): print 'Help link=',pfx+helplink
4509        if htmlFirstUse:
4510            wb.open_new(pfx+helplink)
4511            htmlFirstUse = False
4512        else:
4513            wb.open(pfx+helplink, new=0, autoraise=True)
4514
4515def ShowWebPage(URL,frame):
4516    '''Called to show a tutorial web page.
4517    '''
4518    global htmlFirstUse,htmlPanel,htmlFrame
4519    # determine if a web browser or the internal viewer should be used for help info
4520    if GSASIIpath.GetConfigValue('Help_mode'):
4521        helpMode = GSASIIpath.GetConfigValue('Help_mode')
4522    else:
4523        helpMode = 'browser'
4524    if helpMode == 'internal':
4525        try:
4526            htmlPanel.LoadFile(URL)
4527            htmlFrame.Raise()
4528        except:
4529            htmlFrame = wx.Frame(frame, -1, size=(610, 510))
4530            htmlFrame.Show(True)
4531            htmlFrame.SetTitle("HTML Window") # N.B. reset later in LoadFile
4532            htmlPanel = MyHtmlPanel(htmlFrame,-1)
4533            htmlPanel.LoadFile(URL)
4534    else:
4535        if URL.startswith('http'): 
4536            pfx = ''
4537        elif sys.platform.lower().startswith('win'):
4538            pfx = ''
4539        else:
4540            pfx = "file://"
4541        if htmlFirstUse:
4542            webbrowser.open_new(pfx+URL)
4543            htmlFirstUse = False
4544        else:
4545            webbrowser.open(pfx+URL, new=0, autoraise=True)
4546
4547################################################################################
4548#### Tutorials support
4549################################################################################
4550G2BaseURL = "https://subversion.xray.aps.anl.gov/pyGSAS"
4551tutorialIndex = (
4552    # tutorial dir,      web page file name,      title for page
4553    ['Getting started'],
4554    ['StartingGSASII', 'Starting GSAS.htm', 'Starting GSAS-II'],
4555
4556    ['Rietveld fitting'],
4557       
4558    ['LabData', 'Laboratory X.htm', 'Fitting laboratory X-ray powder data for fluoroapatite'],
4559    ['CWNeutron', 'Neutron CW Powder Data.htm', 'CW Neutron Powder fit for Yttrium-Iron Garnet'],
4560    ['CWCombined', 'Combined refinement.htm', 'Combined X-ray/CW-neutron refinement of PbSO4'],
4561    ['TOF-CW Joint Refinement', 'TOF combined XN Rietveld refinement in GSAS.htm', 'Combined X-ray/TOF-neutron Rietveld refinement'],
4562    ['BkgFit', 'FitBkgTut.htm',  'Fitting the Starting Background using Fixed Points'],
4563   
4564    ['Parametric Rietveld fitting'],
4565    ['SeqRefine', 'SequentialTutorial.htm', 'Sequential refinement of multiple datasets'],
4566    ['SeqParametric', 'ParametricFitting.htm', '     Parametric Fitting and Pseudo Variables for Sequential Fits'],
4567
4568    ['Structure solution'],
4569    ['FitPeaks', 'Fit Peaks.htm', 'Fitting individual peaks & autoindexing'],
4570    ['CFjadarite', 'Charge Flipping in GSAS.htm', '     Charge Flipping structure solution for jadarite'],
4571    ['CFsucrose', 'Charge Flipping - sucrose.htm','     Charge Flipping structure solution for sucrose'],
4572    ['CFXraySingleCrystal', 'CFSingleCrystal.htm', 'Charge Flipping structure solution with Xray single crystal data'],       
4573    ['TOF Charge Flipping', 'Charge Flipping with TOF single crystal data in GSASII.htm', 'Charge flipping with neutron TOF single crystal data'],
4574    ['MCsimanneal', 'MCSA in GSAS.htm', 'Monte-Carlo simulated annealing structure determination'],
4575
4576    ['Stacking Fault Modeling'],
4577    ['StackingFaults-I', 'Stacking Faults-I.htm', 'Stacking fault simulations for diamond'],
4578    ['StackingFaults-II', 'Stacking Faults II.htm', 'Stacking fault simulations for Keokuk kaolinite'],
4579    ['StackingFaults-III', 'Stacking Faults-III.htm', 'Stacking fault simulations for Georgia kaolinite'],
4580
4581    ['Image Calibration/Integration'],
4582    ['2DCalibration', 'Calibration of an area detector in GSAS.htm', 'Calibration of an area detector'],
4583    ['2DIntegration', 'Integration of area detector data in GSAS.htm', '     Integration of area detector data'],
4584    ['TOF Calibration', 'Calibration of a TOF powder diffractometer.htm', 'Calibration of a Neutron TOF diffractometer'],
4585
4586    ['Small-Angle Scattering'],       
4587    ['2DStrain', 'Strain fitting of 2D data in GSAS-II.htm', 'Strain fitting of 2D data'],
4588    ['2DTexture', 'Texture analysis of 2D data in GSAS-II.htm', 'Texture analysis of 2D data'],
4589             
4590    ['SAsize', 'Small Angle Size Distribution.htm', 'Small angle x-ray data size distribution (alumina powder)'],
4591    ['SAfit', 'Fitting Small Angle Scattering Data.htm', '     Fitting small angle x-ray data (alumina powder)'],
4592    ['SAimages', 'Small Angle Image Processing.htm', 'Image Processing of small angle x-ray data'],
4593    ['SAseqref', 'Sequential Refinement of Small Angle Scattering Data.htm', 'Sequential refinement with small angle scattering data'],
4594
4595    ['Other'],   
4596    ['MerohedralTwins', 'Merohedral twin refinement in GSAS.htm', 'Merohedral twin refinements'],
4597    ['TOF Single Crystal Refinement', 'TOF single crystal refinement in GSAS.htm', 'Single crystal refinement from TOF data'],
4598    ['PythonScript','Scripting.htm','Scripting a GSAS-II Refinement from Python']
4599   
4600    #['ExampleDir', 'ExamplePage.html', 'Example Tutorial Title'],
4601    )
4602'''A catalog of GSAS-II tutorials with headings. This is the master list of GSAS-II tutorials and must be updated when tutorials are
4603added. Each item has either one or three items.
4604Titles are single item in a list or tuple. Tutorials have three items: (a) the name of the directory,
4605(b) the name of the web page and (c) a title for the tutorial.
4606Tutorials that depend on a previous tutorial being completed should have the title for
4607the tutorial indented by five spaces.
4608
4609Note that :data:`tutorialCatalog` is generated from this tuple.
4610Also see :mod:`makeTutorial` which is used to read this and create a web page.
4611'''
4612
4613#A catalog of GSAS-II tutorials generated from the table in :data:`tutorialIndex`
4614tutorialCatalog = [l for l in tutorialIndex if len(l) == 3]
4615
4616class OpenTutorial(wx.Dialog):
4617    '''Open a tutorial web page, optionally copying the web page, screen images and
4618    data file(s) to the local disk.
4619    '''
4620   
4621    def __init__(self,parent):
4622        style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER
4623        wx.Dialog.__init__(self, parent, wx.ID_ANY, 'Open Tutorial', style=style)
4624        self.G2frame = self.frame = parent
4625        pnl = wx.Panel(self)
4626        sizer = wx.BoxSizer(wx.VERTICAL)
4627        sizer1 = wx.BoxSizer(wx.HORIZONTAL)       
4628        label = wx.StaticText(
4629            pnl,  wx.ID_ANY,
4630            'Select the tutorial to be run and the mode of access'
4631            )
4632        msg = '''GSAS-II tutorials and their sample data files
4633        require a fair amount of storage space; few users will
4634        use all of them. This dialog allows users to load selected
4635        tutorials (along with their sample data) to their computer;
4636        optionally all tutorials can be downloaded.
4637
4638        Downloaded tutorials can be viewed and run without internet
4639        access. Tutorials can also be viewed without download, but
4640        users will need to download the sample data files manually.
4641
4642        The location used to download tutorials is set using the
4643        "Set download location" which is saved as the "Tutorial_location"
4644        configuration option see File/Preference or the
4645        config_example.py file. System managers can select to have
4646        tutorial files installed at a shared location.
4647        '''
4648        self.SetTutorialPath()
4649        hlp = HelpButton(pnl,msg)
4650        sizer1.Add((-1,-1),1, wx.EXPAND, 0)
4651        sizer1.Add(label, 0, wx.ALIGN_CENTRE|wx.ALL, 0)
4652        sizer1.Add((-1,-1),1, wx.EXPAND, 0)
4653        sizer1.Add(hlp,0,wx.ALIGN_RIGHT|wx.ALL)
4654        sizer.Add(sizer1,0,wx.EXPAND|wx.ALL,0)
4655        sizer.Add((10,10))
4656        sizer0 = wx.BoxSizer(wx.HORIZONTAL)       
4657        sizer1a = wx.BoxSizer(wx.VERTICAL)
4658        sizer1b = wx.BoxSizer(wx.VERTICAL)
4659        btn = wx.Button(pnl, wx.ID_ANY, "Download a tutorial and view")
4660        btn.Bind(wx.EVT_BUTTON, self.SelectAndDownload)
4661        sizer1a.Add(btn,0,WACV)
4662        btn = wx.Button(pnl, wx.ID_ANY, "Select from downloaded tutorials")
4663        btn.Bind(wx.EVT_BUTTON, self.onSelectDownloaded)
4664        sizer1a.Add(btn,0,WACV)
4665        btn = wx.Button(pnl, wx.ID_ANY, "Browse tutorial on web")
4666        btn.Bind(wx.EVT_BUTTON, self.onWebBrowse)
4667        sizer1a.Add(btn,0,WACV)
4668        btn = wx.Button(pnl, wx.ID_ANY, "Update downloaded tutorials")
4669        btn.Bind(wx.EVT_BUTTON, self.UpdateDownloaded)
4670        sizer1b.Add(btn,0,WACV)
4671        btn = wx.Button(pnl, wx.ID_ANY, "Download all tutorials")
4672        btn.Bind(wx.EVT_BUTTON, self.DownloadAll)
4673        sizer1b.Add(btn,0,WACV)
4674        sizer0.Add(sizer1a,0,wx.EXPAND|wx.ALL,0)
4675        sizer0.Add(sizer1b,0,wx.EXPAND|wx.ALL,0)
4676        sizer.Add(sizer0,5,wx.EXPAND|wx.ALL,5)
4677       
4678        sizer.Add((10,10))
4679        sizer1 = wx.BoxSizer(wx.HORIZONTAL)
4680        btn = wx.Button(pnl, wx.ID_ANY, "Set download location")
4681        btn.Bind(wx.EVT_BUTTON, self.SelectDownloadLoc)
4682        sizer1.Add(btn,0,WACV)
4683        self.dataLoc = wx.StaticText(pnl, wx.ID_ANY,self.tutorialPath)
4684        sizer1.Add(self.dataLoc,0,WACV)
4685        sizer.Add(sizer1)
4686       
4687        btnsizer = wx.StdDialogButtonSizer()
4688        btn = wx.Button(pnl, wx.ID_CANCEL,"Done")
4689        btnsizer.AddButton(btn)
4690        btnsizer.Realize()
4691        sizer.Add(btnsizer, 0, wx.ALIGN_CENTER|wx.ALL, 5)
4692        pnl.SetSizer(sizer)
4693        sizer.Fit(self)
4694        self.topsizer=sizer
4695        self.CenterOnParent()
4696
4697    def SetTutorialPath(self):
4698        '''Get the tutorial location if set; if not pick a default
4699        directory in a logical place
4700        '''
4701        if GSASIIpath.GetConfigValue('Tutorial_location'):
4702            self.tutorialPath = os.path.abspath(GSASIIpath.GetConfigValue('Tutorial_location'))
4703        elif (sys.platform.lower().startswith('win') and
4704              os.path.exists(os.path.abspath(os.path.expanduser('~/My Documents')))):
4705            self.tutorialPath = os.path.abspath(os.path.expanduser('~/My Documents/G2tutorials'))
4706        elif (sys.platform.lower().startswith('win') and
4707              os.path.exists(os.path.abspath(os.path.expanduser('~/Documents')))):
4708            self.tutorialPath = os.path.abspath(os.path.expanduser('~/Documents/G2tutorials'))
4709        else:
4710            self.tutorialPath = os.path.abspath(os.path.expanduser('~/G2tutorials'))
4711
4712    def SelectAndDownload(self,event):
4713        '''Make a list of all tutorials on web and allow user to choose one to
4714        download and then view
4715        '''
4716        indices = [j for j,i in enumerate(tutorialCatalog)
4717            if not os.path.exists(os.path.join(self.tutorialPath,i[0],i[1]))]
4718        if not indices:
4719            G2MessageBox(self,'All tutorials are downloaded','None to download')
4720            return
4721        choices = [tutorialCatalog[i][2] for i in indices]
4722        selected = self.ChooseTutorial(choices)
4723        if selected is None: return
4724        j = indices[selected]
4725        fullpath = os.path.join(self.tutorialPath,tutorialCatalog[j][0],tutorialCatalog[j][1])
4726        fulldir = os.path.join(self.tutorialPath,tutorialCatalog[j][0])
4727        URL = G2BaseURL+'/Tutorials/'+tutorialCatalog[j][0]+'/'
4728        if GSASIIpath.svnInstallDir(URL,fulldir):
4729            ShowWebPage(fullpath,self.frame)
4730        else:
4731            G2MessageBox(self,'Error downloading tutorial','Download error')
4732        self.EndModal(wx.ID_OK)
4733        self.G2frame.TutorialImportDir = os.path.join(self.tutorialPath,tutorialCatalog[j][0],'data')
4734
4735    def onSelectDownloaded(self,event):
4736        '''Select a previously downloaded tutorial
4737        '''
4738        indices = [j for j,i in enumerate(tutorialCatalog)
4739            if os.path.exists(os.path.join(self.tutorialPath,i[0],i[1]))]
4740        if not indices:
4741            G2MessageBox(self,
4742                         'There are no downloaded tutorials in '+self.tutorialPath,
4743                         'None downloaded')
4744            return
4745        choices = [tutorialCatalog[i][2] for i in indices]
4746        selected = self.ChooseTutorial(choices)
4747        if selected is None: return
4748        j = indices[selected]
4749        fullpath = os.path.join(self.tutorialPath,tutorialCatalog[j][0],tutorialCatalog[j][1])
4750        self.EndModal(wx.ID_OK)
4751        ShowWebPage(fullpath,self.frame)
4752        self.G2frame.TutorialImportDir = os.path.join(self.tutorialPath,tutorialCatalog[j][0],'data')
4753       
4754    def onWebBrowse(self,event):
4755        '''Make a list of all tutorials on web and allow user to view one.
4756        '''
4757        choices = [i[2] for i in tutorialCatalog]
4758        selected = self.ChooseTutorial(choices)
4759        if selected is None: return       
4760        tutdir = tutorialCatalog[selected][0]
4761        tutfil = tutorialCatalog[selected][1]
4762        # open web page remotely, don't worry about data
4763        URL = G2BaseURL+'/Tutorials/'+tutdir+'/'+tutfil
4764        self.EndModal(wx.ID_OK)
4765        ShowWebPage(URL,self.frame)
4766       
4767    def ChooseTutorial(self,choices):
4768        'choose a tutorial from a list'
4769        def onDoubleClick(event):
4770            'double-click closes the dialog'
4771            dlg.EndModal(wx.ID_OK)
4772        dlg = wx.Dialog(self,wx.ID_ANY,
4773                        'Select a tutorial to view. NB: indented ones require prerequisite',
4774                        style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)
4775        pnl = wx.Panel(dlg)
4776        sizer = wx.BoxSizer(wx.VERTICAL)
4777        listbox = wx.ListBox(pnl, wx.ID_ANY, choices=choices,
4778                             size=(450, 100),
4779                             style=wx.LB_SINGLE)
4780        sizer.Add(listbox,1,WACV|wx.EXPAND|wx.ALL,1)
4781        listbox.Bind(wx.EVT_LISTBOX_DCLICK, onDoubleClick)
4782        sizer.Add((10,10))
4783        btnsizer = wx.StdDialogButtonSizer()
4784        btn = wx.Button(pnl, wx.ID_CANCEL)
4785        btnsizer.AddButton(btn)
4786        OKbtn = wx.Button(pnl, wx.ID_OK)
4787        OKbtn.SetDefault()
4788        btnsizer.AddButton(OKbtn)
4789        btnsizer.Realize()
4790        sizer.Add((-1,5))
4791        sizer.Add(btnsizer,0,wx.ALIGN_RIGHT,50)
4792       
4793        pnl.SetSizer(sizer)
4794        sizer.Fit(dlg)
4795        self.CenterOnParent()
4796        if dlg.ShowModal() != wx.ID_OK:
4797            dlg.Destroy()
4798            return
4799        selected = listbox.GetSelection()
4800        dlg.Destroy()
4801        wx.Yield() # close window right away so user sees something happen
4802        if selected < 0: return
4803        return selected
4804
4805    def UpdateDownloaded(self,event):
4806        'Find the downloaded tutorials and run an svn update on them'
4807        updated = 0
4808        for i in tutorialCatalog:
4809            if not os.path.exists(os.path.join(self.tutorialPath,i[0],i[1])): continue
4810            print('Updating '+i[0])
4811            GSASIIpath.svnUpdateDir(os.path.join(self.tutorialPath,i[0]))
4812            updated += 0
4813        if not updated:
4814            G2MessageBox(self,'Warning, you have no downloaded tutorials','None Downloaded')
4815        self.EndModal(wx.ID_OK)
4816       
4817    def DownloadAll(self,event):
4818        'Download or update all tutorials'
4819        fail = ''
4820        for i in tutorialCatalog:
4821            if os.path.exists(os.path.join(self.tutorialPath,i[0],i[1])):
4822                print('Updating '+i[0])
4823                GSASIIpath.svnUpdateDir(os.path.join(self.tutorialPath,i[0]))
4824            else:
4825                fulldir = os.path.join(self.tutorialPath,i[0])
4826                URL = G2BaseURL+'/Tutorials/'+i[0]+'/'
4827                if not GSASIIpath.svnInstallDir(URL,fulldir):
4828                    if fail: fail += ', '
4829                    fail += i[0]
4830        if fail: 
4831            G2MessageBox(self,'Error downloading tutorial(s)\n\t'+fail,'Download error')
4832        self.EndModal(wx.ID_OK)
4833                   
4834    def SelectDownloadLoc(self,event):
4835        '''Select a download location,
4836        Cancel resets to the default
4837        '''
4838        dlg = wx.DirDialog(self, "Choose a directory for tutorial downloads:",
4839                           defaultPath=self.tutorialPath)#,style=wx.DD_DEFAULT_STYLE)
4840                           #)
4841        try:
4842            if dlg.ShowModal() != wx.ID_OK:
4843                return
4844            pth = dlg.GetPath()
4845        finally:
4846            dlg.Destroy()
4847
4848        if not os.path.exists(pth):
4849            try:
4850                os.makedirs(pth)    #failing for no obvious reason
4851            except OSError:
4852                msg = 'The selected directory is not valid.\n\t'
4853                msg += pth
4854                msg += '\n\nAn attempt to create the directory failed'
4855                G2MessageBox(self.frame,msg)
4856                return
4857        if os.path.exists(os.path.join(pth,"help")) and os.path.exists(os.path.join(pth,"Exercises")):
4858            print("Note that you may have old tutorial files in the following directories")
4859            print('\t'+os.path.join(pth,"help"))
4860            print('\t'+os.path.join(pth,"Exercises"))
4861            print('Subdirectories in the above can be deleted to save space\n\n')
4862        self.tutorialPath = pth
4863        self.dataLoc.SetLabel(self.tutorialPath)
4864        if GSASIIpath.GetConfigValue('Tutorial_location') == pth: return
4865        vars = GetConfigValsDocs()
4866        try:
4867            vars['Tutorial_location'][1] = pth
4868            if GSASIIpath.GetConfigValue('debug'): print('Saving Tutorial_location: '+pth)
4869            GSASIIpath.SetConfigValue(vars)
4870            SaveConfigVars(vars)
4871        except KeyError:
4872            pass
4873           
4874if __name__ == '__main__':
4875    app = wx.PySimpleApp()
4876    GSASIIpath.InvokeDebugOpts()
4877    frm = wx.Frame(None) # create a frame
4878    frm.Show(True)
4879   
4880    #======================================================================
4881    # test Grid with GridFractionEditor
4882    #======================================================================
4883    # tbl = [[1.,2.,3.],[1.1,2.1,3.1]]
4884    # colTypes = 3*[wg.GRID_VALUE_FLOAT+':10,5',]
4885    # Gtbl = Table(tbl,types=colTypes,rowLabels=['a','b'],colLabels=['1','2','3'])
4886    # Grid = GSGrid(frm)
4887    # Grid.SetTable(Gtbl,True)
4888    # for i in (0,1,2):
4889    #     attr = wx.grid.GridCellAttr()
4890    #     attr.IncRef()
4891    #     attr.SetEditor(GridFractionEditor(Grid))
4892    #     Grid.SetColAttr(i, attr)
4893    # frm.SetSize((400,200))
4894    # app.MainLoop()
4895    # sys.exit()
4896    #======================================================================
4897    # test Tutorial access
4898    #======================================================================
4899    # dlg = OpenTutorial(frm)
4900    # if dlg.ShowModal() == wx.ID_OK:
4901    #     print "OK"
4902    # else:
4903    #     print "Cancel"
4904    # dlg.Destroy()
4905    # sys.exit()
4906    #======================================================================
4907    # test ScrolledMultiEditor
4908    #======================================================================
4909    # Data1 = {
4910    #      'Order':1,
4911    #      'omega':'string',
4912    #      'chi':2.0,
4913    #      'phi':'',
4914    #      }
4915    # elemlst = sorted(Data1.keys())
4916    # prelbl = sorted(Data1.keys())
4917    # dictlst = len(elemlst)*[Data1,]
4918    #Data2 = [True,False,False,True]
4919    #Checkdictlst = len(Data2)*[Data2,]
4920    #Checkelemlst = range(len(Checkdictlst))
4921    # print 'before',Data1,'\n',Data2
4922    # dlg = ScrolledMultiEditor(
4923    #     frm,dictlst,elemlst,prelbl,
4924    #     checkdictlst=Checkdictlst,checkelemlst=Checkelemlst,
4925    #     checklabel="Refine?",
4926    #     header="test")
4927    # if dlg.ShowModal() == wx.ID_OK:
4928    #     print "OK"
4929    # else:
4930    #     print "Cancel"
4931    # print 'after',Data1,'\n',Data2
4932    # dlg.Destroy()
4933    Data3 = {
4934         'Order':1.0,
4935         'omega':1.1,
4936         'chi':2.0,
4937         'phi':2.3,
4938         'Order1':1.0,
4939         'omega1':1.1,
4940         'chi1':2.0,
4941         'phi1':2.3,
4942         'Order2':1.0,
4943         'omega2':1.1,
4944         'chi2':2.0,
4945         'phi2':2.3,
4946         }
4947    elemlst = sorted(Data3.keys())
4948    dictlst = len(elemlst)*[Data3,]
4949    prelbl = elemlst[:]
4950    prelbl[0]="this is a much longer label to stretch things out"
4951    Data2 = len(elemlst)*[False,]
4952    Data2[1] = Data2[3] = True
4953    Checkdictlst = len(elemlst)*[Data2,]
4954    Checkelemlst = range(len(Checkdictlst))
4955    #print 'before',Data3,'\n',Data2
4956    #print dictlst,"\n",elemlst
4957    #print Checkdictlst,"\n",Checkelemlst
4958    # dlg = ScrolledMultiEditor(
4959    #     frm,dictlst,elemlst,prelbl,
4960    #     checkdictlst=Checkdictlst,checkelemlst=Checkelemlst,
4961    #     checklabel="Refine?",
4962    #     header="test",CopyButton=True)
4963    # if dlg.ShowModal() == wx.ID_OK:
4964    #     print "OK"
4965    # else:
4966    #     print "Cancel"
4967    #print 'after',Data3,'\n',Data2
4968
4969    # Data2 = list(range(100))
4970    # elemlst += range(2,6)
4971    # postlbl += range(2,6)
4972    # dictlst += len(range(2,6))*[Data2,]
4973
4974    # prelbl = range(len(elemlst))
4975    # postlbl[1] = "a very long label for the 2nd item to force a horiz. scrollbar"
4976    # header="""This is a longer\nmultiline and perhaps silly header"""
4977    # dlg = ScrolledMultiEditor(frm,dictlst,elemlst,prelbl,postlbl,
4978    #                           header=header,CopyButton=True)
4979    # print Data1
4980    # if dlg.ShowModal() == wx.ID_OK:
4981    #     for d,k in zip(dictlst,elemlst):
4982    #         print k,d[k]
4983    # dlg.Destroy()
4984    # if CallScrolledMultiEditor(frm,dictlst,elemlst,prelbl,postlbl,
4985    #                            header=header):
4986    #     for d,k in zip(dictlst,elemlst):
4987    #         print k,d[k]