source: branch/2frame/GSASIIctrlGUI.py @ 2907

Last change on this file since 2907 was 2907, checked in by toby, 6 years ago

tabbed phase panel done. More to go...

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