source: trunk/GSASIIctrls.py @ 2065

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

Allow reading of multiple images from single file

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