source: trunk/GSASIIctrls.py @ 2033

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

change a few imports for RTFD

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