source: trunk/GSASIIctrls.py @ 1998

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

rebuild docs

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