source: trunk/GSASIIctrls.py @ 2112

Last change on this file since 2112 was 2112, checked in by toby, 7 years ago

missing import

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