source: trunk/GSASIIctrls.py @ 1988

Last change on this file since 1988 was 1971, checked in by toby, 10 years ago

Add preferences option to edit configuration options

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