source: trunk/GSASIIctrls.py @ 2981

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

fix import & switch msg

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