source: trunk/GSASIIctrls.py @ 2347

Last change on this file since 2347 was 2347, checked in by vondreele, 7 years ago

implement Steven Weigand's fixes for Pilatus 'I' for 'L'
do a 'fix' for powderCif files with value(esd) for powder profile points
add a couple of comments for tutorial stuff

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