source: branch/2frame/GSASIIctrls.py @ 2895

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

merge trunk changes to 2984

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