source: trunk/GSASIIctrls.py @ 1881

Last change on this file since 1881 was 1881, checked in by toby, 8 years ago

fix bugs in single crystal editing as well as problems with ValidatedTextCtrl?

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