source: trunk/GSASIIctrls.py @ 1866

Last change on this file since 1866 was 1866, checked in by vondreele, 9 years ago

Add TOF single crystal tutorial to list

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