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

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

implement restraint tab selection menu; fix restraint scroll bar problem (remove/comment all DestroyChildren?); consolidate menu generation

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