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

Last change on this file since 2900 was 2900, checked in by toby, 5 years ago

reorg for GUI separation pretty much complete (GSASIIIO.py still needs some thought)

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