source: trunk/GSASIIctrls.py @ 2865

Last change on this file since 2865 was 2865, checked in by vondreele, 4 years ago

fix problem with contour color preference setting

  • Property svn:eol-style set to native
  • Property svn:keywords set to Date Author Revision URL Id
File size: 182.8 KB
Line 
1# -*- coding: utf-8 -*-
2#GSASIIctrls - Custom GSAS-II GUI controls
3########### SVN repository information ###################
4# $Date: 2017-06-16 17:59:40 +0000 (Fri, 16 Jun 2017) $
5# $Author: vondreele $
6# $Revision: 2865 $
7# $URL: trunk/GSASIIctrls.py $
8# $Id: GSASIIctrls.py 2865 2017-06-16 17:59:40Z 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: 2865 $")
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 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    if multiple:
2474        if useCancel:
2475            dlg = G2MultiChoiceDialog(
2476                ParentFrame,title, header, ChoiceList)
2477        else:
2478            dlg = G2MultiChoiceDialog(
2479                ParentFrame,title, header, ChoiceList,
2480                style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER|wx.OK|wx.CENTRE)
2481    else:
2482        if useCancel:
2483            dlg = wx.SingleChoiceDialog(
2484                ParentFrame,title, header, ChoiceList)
2485        else:
2486            dlg = wx.SingleChoiceDialog(
2487                ParentFrame,title, header,ChoiceList,
2488                style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER|wx.OK|wx.CENTRE)
2489    if size: dlg.SetSize(size)
2490    if dlg.ShowModal() == wx.ID_OK:
2491        if multiple:
2492            dlg.Destroy()
2493            return dlg.GetSelections()
2494        else:
2495            dlg.Destroy()
2496            return dlg.GetSelection()
2497    else:
2498        dlg.Destroy()
2499        return None
2500    dlg.Destroy()
2501
2502######################################################### Column-order selection dialog
2503def GetItemOrder(parent,keylist,vallookup,posdict):
2504    '''Creates a panel where items can be ordered into columns
2505   
2506    :param list keylist: is a list of keys for column assignments
2507    :param dict vallookup: is a dict keyed by names in keylist where each item is a dict.
2508       Each inner dict contains variable names as keys and their associated values
2509    :param dict posdict: is a dict keyed by names in keylist where each item is a dict.
2510       Each inner dict contains column numbers as keys and their associated
2511       variable name as a value. This is used for both input and output.
2512       
2513    '''
2514    dlg = wx.Dialog(parent,style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)
2515    sizer = wx.BoxSizer(wx.VERTICAL)
2516    spanel = OrderBox(dlg,keylist,vallookup,posdict)
2517    spanel.Fit()
2518    sizer.Add(spanel,1,wx.EXPAND)
2519    btnsizer = wx.StdDialogButtonSizer()
2520    btn = wx.Button(dlg, wx.ID_OK)
2521    btn.SetDefault()
2522    btnsizer.AddButton(btn)
2523    #btn = wx.Button(dlg, wx.ID_CANCEL)
2524    #btnsizer.AddButton(btn)
2525    btnsizer.Realize()
2526    sizer.Add(btnsizer, 0, wx.ALIGN_CENTER_VERTICAL|wx.EXPAND|wx.ALL, 5)
2527    dlg.SetSizer(sizer)
2528    sizer.Fit(dlg)
2529    dlg.ShowModal()
2530
2531################################################################################
2532class OrderBox(wxscroll.ScrolledPanel):
2533    '''Creates a panel with scrollbars where items can be ordered into columns
2534   
2535    :param list keylist: is a list of keys for column assignments
2536    :param dict vallookup: is a dict keyed by names in keylist where each item is a dict.
2537      Each inner dict contains variable names as keys and their associated values
2538    :param dict posdict: is a dict keyed by names in keylist where each item is a dict.
2539      Each inner dict contains column numbers as keys and their associated
2540      variable name as a value. This is used for both input and output.
2541     
2542    '''
2543    def __init__(self,parent,keylist,vallookup,posdict,*arg,**kw):
2544        self.keylist = keylist
2545        self.vallookup = vallookup
2546        self.posdict = posdict
2547        self.maxcol = 0
2548        for nam in keylist:
2549            posdict = self.posdict[nam]
2550            if posdict.keys():
2551                self.maxcol = max(self.maxcol, max(posdict))
2552        wxscroll.ScrolledPanel.__init__(self,parent,wx.ID_ANY,*arg,**kw)
2553        self.GBsizer = wx.GridBagSizer(4,4)
2554        self.SetBackgroundColour(WHITE)
2555        self.SetSizer(self.GBsizer)
2556        colList = [str(i) for i in range(self.maxcol+2)]
2557        for i in range(self.maxcol+1):
2558            wid = wx.StaticText(self,wx.ID_ANY,str(i),style=wx.ALIGN_CENTER)
2559            wid.SetBackgroundColour(DULL_YELLOW)
2560            wid.SetMinSize((50,-1))
2561            self.GBsizer.Add(wid,(0,i),flag=wx.ALIGN_CENTER|wx.EXPAND)
2562        self.chceDict = {}
2563        for row,nam in enumerate(self.keylist):
2564            posdict = self.posdict[nam]
2565            for col in posdict:
2566                lbl = posdict[col]
2567                pnl = wx.Panel(self,wx.ID_ANY)
2568                pnl.SetBackgroundColour(VERY_LIGHT_GREY)
2569                insize = wx.BoxSizer(wx.VERTICAL)
2570                wid = wx.Choice(pnl,wx.ID_ANY,choices=colList)
2571                insize.Add(wid,0,wx.EXPAND|wx.BOTTOM,3)
2572                wid.SetSelection(col)
2573                self.chceDict[wid] = (row,col)
2574                wid.Bind(wx.EVT_CHOICE,self.OnChoice)
2575                wid = wx.StaticText(pnl,wx.ID_ANY,lbl)
2576                insize.Add(wid,0,flag=wx.EXPAND)
2577                val = G2py3.FormatSigFigs(self.vallookup[nam][lbl],maxdigits=8)
2578                wid = wx.StaticText(pnl,wx.ID_ANY,'('+val+')')
2579                insize.Add(wid,0,flag=wx.EXPAND)
2580                pnl.SetSizer(insize)
2581                self.GBsizer.Add(pnl,(row+1,col),flag=wx.EXPAND)
2582        self.SetAutoLayout(1)
2583        self.SetupScrolling()
2584        self.SetMinSize((
2585            min(700,self.GBsizer.GetSize()[0]),
2586            self.GBsizer.GetSize()[1]+20))
2587    def OnChoice(self,event):
2588        '''Called when a column is assigned to a variable
2589        '''
2590        row,col = self.chceDict[event.EventObject] # which variable was this?
2591        newcol = event.Selection # where will it be moved?
2592        if newcol == col:
2593            return # no change: nothing to do!
2594        prevmaxcol = self.maxcol # save current table size
2595        key = self.keylist[row] # get the key for the current row
2596        lbl = self.posdict[key][col] # selected variable name
2597        lbl1 = self.posdict[key].get(col+1,'') # next variable name, if any
2598        # if a posXXX variable is selected, and the next variable is posXXX, move them together
2599        repeat = 1
2600        if lbl[:3] == 'pos' and lbl1[:3] == 'int' and lbl[3:] == lbl1[3:]:
2601            repeat = 2
2602        for i in range(repeat): # process the posXXX and then the intXXX (or a single variable)
2603            col += i
2604            newcol += i
2605            if newcol in self.posdict[key]:
2606                # find first non-blank after newcol
2607                for mtcol in range(newcol+1,self.maxcol+2):
2608                    if mtcol not in self.posdict[key]: break
2609                l1 = range(mtcol,newcol,-1)+[newcol]
2610                l = range(mtcol-1,newcol-1,-1)+[col]
2611            else:
2612                l1 = [newcol]
2613                l = [col]
2614            # move all of the items, starting from the last column
2615            for newcol,col in zip(l1,l):
2616                #print 'moving',col,'to',newcol
2617                self.posdict[key][newcol] = self.posdict[key][col]
2618                del self.posdict[key][col]
2619                self.maxcol = max(self.maxcol,newcol)
2620                obj = self.GBsizer.FindItemAtPosition((row+1,col))
2621                self.GBsizer.SetItemPosition(obj.GetWindow(),(row+1,newcol))
2622                for wid in obj.GetWindow().Children:
2623                    if wid in self.chceDict:
2624                        self.chceDict[wid] = (row,newcol)
2625                        wid.SetSelection(self.chceDict[wid][1])
2626        # has the table gotten larger? If so we need new column heading(s)
2627        if prevmaxcol != self.maxcol:
2628            for i in range(prevmaxcol+1,self.maxcol+1):
2629                wid = wx.StaticText(self,wx.ID_ANY,str(i),style=wx.ALIGN_CENTER)
2630                wid.SetBackgroundColour(DULL_YELLOW)
2631                wid.SetMinSize((50,-1))
2632                self.GBsizer.Add(wid,(0,i),flag=wx.ALIGN_CENTER|wx.EXPAND)
2633            colList = [str(i) for i in range(self.maxcol+2)]
2634            for wid in self.chceDict:
2635                wid.SetItems(colList)
2636                wid.SetSelection(self.chceDict[wid][1])
2637        self.GBsizer.Layout()
2638        self.FitInside()
2639
2640################################################################################
2641def GetImportFile(G2frame, message, defaultDir="", defaultFile="", style=wx.OPEN,
2642                  *args, **kwargs):
2643    '''Uses a customized dialog that gets files from the appropriate import directory.
2644    Arguments are used the same as in :func:`wx.FileDialog`. Selection of
2645    multiple files is allowed if argument style includes wx.MULTIPLE.
2646
2647    The default initial directory (unless overridden with argument defaultDir)
2648    is found in G2frame.TutorialImportDir, config setting Import_directory or
2649    G2frame.LastImportDir, see :func:`GetImportPath`.
2650
2651    The path of the first file entered is used to set G2frame.LastImportDir
2652    and optionally config setting Import_directory.
2653
2654    :returns: a list of files or an empty list
2655    '''
2656    dlg = wx.FileDialog(G2frame, message, defaultDir, defaultFile, *args,
2657                        style=style, **kwargs)
2658    pth = GetImportPath(G2frame)
2659    if not defaultDir and pth: dlg.SetDirectory(pth)
2660    try:
2661        if dlg.ShowModal() == wx.ID_OK:
2662            if style & wx.MULTIPLE:
2663                filelist = dlg.GetPaths()
2664                if len(filelist) == 0: return []
2665            else:
2666                filelist = [dlg.GetPath(),]
2667            # not sure if we want to do this (why use wx.CHANGE_DIR?)
2668            if style & wx.CHANGE_DIR: # to get Mac/Linux to change directory like windows!
2669                os.chdir(dlg.GetDirectory())
2670        else: # cancel was pressed
2671            return []
2672    finally:
2673        dlg.Destroy()
2674    # save the path of the first file and reset the TutorialImportDir variable
2675    pth = os.path.split(os.path.abspath(filelist[0]))[0]
2676    if GSASIIpath.GetConfigValue('Save_paths'): SaveImportDirectory(pth)
2677    G2frame.LastImportDir = pth
2678    G2frame.TutorialImportDir = None
2679    return filelist
2680
2681def GetImportPath(G2frame):
2682    '''Determines the default location to use for importing files. Tries sequentially
2683    G2frame.TutorialImportDir, config var Import_directory and G2frame.LastImportDir.
2684   
2685    :returns: a string containing the path to be used when reading files or None
2686      if none of the above are specified.
2687    '''
2688    if G2frame.TutorialImportDir:
2689        if os.path.exists(G2frame.TutorialImportDir):
2690            return G2frame.TutorialImportDir
2691        elif GSASIIpath.GetConfigValue('debug'):
2692            print('Tutorial location (TutorialImportDir) not found: '+G2frame.TutorialImportDir)
2693    pth = GSASIIpath.GetConfigValue('Import_directory')
2694    if pth:
2695        pth = os.path.expanduser(pth)
2696        if os.path.exists(pth):
2697            return pth
2698        elif GSASIIpath.GetConfigValue('debug'):
2699            print('Ignoring Config Import_directory value: '+
2700                      GSASIIpath.GetConfigValue('Import_directory'))
2701    if G2frame.LastImportDir:
2702        if os.path.exists(G2frame.LastImportDir):
2703            return G2frame.LastImportDir
2704        elif GSASIIpath.GetConfigValue('debug'):
2705            print('Warning: G2frame.LastImportDir not found = '+G2frame.LastImportDir)
2706    return None
2707
2708def GetExportPath(G2frame):
2709    '''Determines the default location to use for writing files. Tries sequentially
2710    G2frame.LastExportDir and G2frame.LastGPXdir.
2711   
2712    :returns: a string containing the path to be used when writing files or '.'
2713      if none of the above are specified.
2714    '''
2715    if G2frame.LastExportDir:
2716        return G2frame.LastExportDir
2717    elif G2frame.LastGPXdir:
2718        return G2frame.LastGPXdir
2719    else:
2720        return '.'
2721
2722################################################################################
2723#####  Customized Grid Support
2724################################################################################           
2725class GSGrid(wg.Grid):
2726    '''Basic wx.Grid implementation
2727    '''
2728    def __init__(self, parent, name=''):
2729        wg.Grid.__init__(self,parent,-1,name=name)
2730        if hasattr(parent.TopLevelParent,'currentGrids'):
2731            parent.TopLevelParent.currentGrids.append(self)      # save a reference to the grid in the Frame
2732           
2733    def Clear(self):
2734        wg.Grid.ClearGrid(self)
2735       
2736    def SetCellReadOnly(self,r,c,readonly=True):
2737        self.SetReadOnly(r,c,isReadOnly=readonly)
2738       
2739    def SetCellStyle(self,r,c,color="white",readonly=True):
2740        self.SetCellBackgroundColour(r,c,color)
2741        self.SetReadOnly(r,c,isReadOnly=readonly)
2742       
2743    def GetSelection(self):
2744        #this is to satisfy structure drawing stuff in G2plt when focus changes
2745        return None
2746
2747    def InstallGridToolTip(self, rowcolhintcallback,
2748                           colLblCallback=None,rowLblCallback=None):
2749        '''code to display a tooltip for each item on a grid
2750        from http://wiki.wxpython.org/wxGrid%20ToolTips (buggy!), expanded to
2751        column and row labels using hints from
2752        https://groups.google.com/forum/#!topic/wxPython-users/bm8OARRVDCs
2753
2754        :param function rowcolhintcallback: a routine that returns a text
2755          string depending on the selected row and column, to be used in
2756          explaining grid entries.
2757        :param function colLblCallback: a routine that returns a text
2758          string depending on the selected column, to be used in
2759          explaining grid columns (if None, the default), column labels
2760          do not get a tooltip.
2761        :param function rowLblCallback: a routine that returns a text
2762          string depending on the selected row, to be used in
2763          explaining grid rows (if None, the default), row labels
2764          do not get a tooltip.
2765        '''
2766        prev_rowcol = [None,None,None]
2767        def OnMouseMotion(event):
2768            # event.GetRow() and event.GetCol() would be nice to have here,
2769            # but as this is a mouse event, not a grid event, they are not
2770            # available and we need to compute them by hand.
2771            x, y = self.CalcUnscrolledPosition(event.GetPosition())
2772            row = self.YToRow(y)
2773            col = self.XToCol(x)
2774            hinttext = ''
2775            win = event.GetEventObject()
2776            if [row,col,win] == prev_rowcol: # no change from last position
2777                if event: event.Skip()
2778                return
2779            if win == self.GetGridWindow() and row >= 0 and col >= 0:
2780                hinttext = rowcolhintcallback(row, col)
2781            elif win == self.GetGridColLabelWindow() and col >= 0:
2782                if colLblCallback: hinttext = colLblCallback(col)
2783            elif win == self.GetGridRowLabelWindow() and row >= 0:
2784                if rowLblCallback: hinttext = rowLblCallback(row)
2785            else: # this should be the upper left corner, which is empty
2786                if event: event.Skip()
2787                return
2788            if hinttext is None: hinttext = ''
2789            win.SetToolTipString(hinttext)
2790            prev_rowcol[:] = [row,col,win]
2791            if event: event.Skip()
2792
2793        wx.EVT_MOTION(self.GetGridWindow(), OnMouseMotion)
2794        if colLblCallback: wx.EVT_MOTION(self.GetGridColLabelWindow(), OnMouseMotion)
2795        if rowLblCallback: wx.EVT_MOTION(self.GetGridRowLabelWindow(), OnMouseMotion)
2796                                                   
2797################################################################################           
2798class Table(wg.PyGridTableBase):
2799    '''Basic data table for use with GSgrid
2800    '''
2801    def __init__(self, data=[], rowLabels=None, colLabels=None, types = None):
2802        wg.PyGridTableBase.__init__(self)
2803        self.colLabels = colLabels
2804        self.rowLabels = rowLabels
2805        self.dataTypes = types
2806        self.data = data
2807       
2808    def AppendRows(self, numRows=1):
2809        self.data.append([])
2810        return True
2811       
2812    def CanGetValueAs(self, row, col, typeName):
2813        if self.dataTypes:
2814            colType = self.dataTypes[col].split(':')[0]
2815            if typeName == colType:
2816                return True
2817            else:
2818                return False
2819        else:
2820            return False
2821
2822    def CanSetValueAs(self, row, col, typeName):
2823        return self.CanGetValueAs(row, col, typeName)
2824
2825    def DeleteRow(self,pos):
2826        data = self.GetData()
2827        self.SetData([])
2828        new = []
2829        for irow,row in enumerate(data):
2830            if irow <> pos:
2831                new.append(row)
2832        self.SetData(new)
2833       
2834    def GetColLabelValue(self, col):
2835        if self.colLabels:
2836            return self.colLabels[col]
2837           
2838    def GetData(self):
2839        data = []
2840        for row in range(self.GetNumberRows()):
2841            data.append(self.GetRowValues(row))
2842        return data
2843       
2844    def GetNumberCols(self):
2845        try:
2846            return len(self.colLabels)
2847        except TypeError:
2848            return None
2849       
2850    def GetNumberRows(self):
2851        return len(self.data)
2852       
2853    def GetRowLabelValue(self, row):
2854        if self.rowLabels:
2855            return self.rowLabels[row]
2856       
2857    def GetColValues(self, col):
2858        data = []
2859        for row in range(self.GetNumberRows()):
2860            data.append(self.GetValue(row, col))
2861        return data
2862       
2863    def GetRowValues(self, row):
2864        data = []
2865        for col in range(self.GetNumberCols()):
2866            data.append(self.GetValue(row, col))
2867        return data
2868       
2869    def GetTypeName(self, row, col):
2870        try:
2871            if self.data[row][col] is None: return None
2872            return self.dataTypes[col]
2873        except (TypeError,IndexError):
2874            return None
2875
2876    def GetValue(self, row, col):
2877        try:
2878            if self.data[row][col] is None: return ""
2879            return self.data[row][col]
2880        except IndexError:
2881            return None
2882           
2883    def InsertRows(self, pos, rows):
2884        for row in range(rows):
2885            self.data.insert(pos,[])
2886            pos += 1
2887       
2888    def IsEmptyCell(self,row,col):
2889        try:
2890            return not self.data[row][col]
2891        except IndexError:
2892            return True
2893       
2894    def OnKeyPress(self, event):
2895        dellist = self.GetSelectedRows()
2896        if event.GetKeyCode() == wx.WXK_DELETE and dellist:
2897            grid = self.GetView()
2898            for i in dellist: grid.DeleteRow(i)
2899               
2900    def SetColLabelValue(self, col, label):
2901        numcols = self.GetNumberCols()
2902        if col > numcols-1:
2903            self.colLabels.append(label)
2904        else:
2905            self.colLabels[col]=label
2906       
2907    def SetData(self,data):
2908        for row in range(len(data)):
2909            self.SetRowValues(row,data[row])
2910               
2911    def SetRowLabelValue(self, row, label):
2912        self.rowLabels[row]=label
2913           
2914    def SetRowValues(self,row,data):
2915        self.data[row] = data
2916           
2917    def SetValue(self, row, col, value):
2918        def innerSetValue(row, col, value):
2919            try:
2920                self.data[row][col] = value
2921            except TypeError:
2922                return
2923            except IndexError: # has this been tested?
2924                #print row,col,value
2925                # add a new row
2926                if row > self.GetNumberRows():
2927                    self.data.append([''] * self.GetNumberCols())
2928                elif col > self.GetNumberCols():
2929                    for row in range(self.GetNumberRows()): # bug fixed here
2930                        self.data[row].append('')
2931                #print self.data
2932                self.data[row][col] = value
2933        innerSetValue(row, col, value)
2934
2935################################################################################
2936class GridFractionEditor(wg.PyGridCellEditor):
2937    '''A grid cell editor class that allows entry of values as fractions as well
2938    as sine and cosine values [as s() and c()]
2939    '''
2940    def __init__(self,grid):
2941        wg.PyGridCellEditor.__init__(self)
2942
2943    def Create(self, parent, id, evtHandler):
2944        self._tc = wx.TextCtrl(parent, id, "")
2945        self._tc.SetInsertionPoint(0)
2946        self.SetControl(self._tc)
2947
2948        if evtHandler:
2949            self._tc.PushEventHandler(evtHandler)
2950
2951        self._tc.Bind(wx.EVT_CHAR, self.OnChar)
2952
2953    def SetSize(self, rect):
2954        self._tc.SetDimensions(rect.x, rect.y, rect.width+2, rect.height+2,
2955                               wx.SIZE_ALLOW_MINUS_ONE)
2956
2957    def BeginEdit(self, row, col, grid):
2958        self.startValue = grid.GetTable().GetValue(row, col)
2959        self._tc.SetValue(str(self.startValue))
2960        self._tc.SetInsertionPointEnd()
2961        self._tc.SetFocus()
2962        self._tc.SetSelection(0, self._tc.GetLastPosition())
2963
2964    def EndEdit(self, row, col, grid, oldVal=None):
2965        changed = False
2966
2967        self.nextval = self.startValue
2968        val = self._tc.GetValue().lower().strip()
2969        if val != self.startValue:
2970            changed = True
2971            neg = False
2972            if val.startswith('-'):
2973                neg = True
2974                val = val[1:]
2975            # allow old GSAS s20 and c20 etc for sind(20) and cosd(20)
2976            if val.startswith('s') and '(' not in val:
2977                val = 'sind('+val.strip('s')+')'
2978            elif val.startswith('c') and '(' not in val:
2979                val = 'cosd('+val.strip('c')+')'
2980            if neg:
2981                val = '-' + val
2982            val = G2py3.FormulaEval(val)
2983            if val is not None:
2984                self.nextval = val
2985            else:
2986                return None
2987            if oldVal is None: # this arg appears in 2.9+; before, we should go ahead & change the table
2988                grid.GetTable().SetValue(row, col, val) # update the table
2989            # otherwise self.ApplyEdit gets called
2990
2991        self.startValue = ''
2992        self._tc.SetValue('')
2993        return changed
2994   
2995    def ApplyEdit(self, row, col, grid):
2996        """ Called only in wx >= 2.9
2997        Save the value of the control into the grid if EndEdit() returns as True
2998        """
2999        grid.GetTable().SetValue(row, col, self.nextval) # update the table
3000
3001    def Reset(self):
3002        self._tc.SetValue(self.startValue)
3003        self._tc.SetInsertionPointEnd()
3004
3005    def Clone(self,grid):
3006        return GridFractionEditor(grid)
3007
3008    def StartingKey(self, evt):
3009        self.OnChar(evt)
3010        if evt.GetSkipped():
3011            self._tc.EmulateKeyPress(evt)
3012
3013    def OnChar(self, evt):
3014        key = evt.GetKeyCode()
3015        if key < 32 or key >= 127:
3016            evt.Skip()
3017        elif chr(key).lower() in '.+-*/0123456789cosind()':
3018            evt.Skip()
3019        else:
3020            evt.StopPropagation()
3021           
3022################################################################################
3023#####  Customized Notebook
3024################################################################################           
3025class GSNoteBook(wx.aui.AuiNotebook):
3026    '''Notebook used in various locations; implemented with wx.aui extension
3027    '''
3028    def __init__(self, parent, name='',size = None,style=wx.aui.AUI_NB_TOP |
3029        wx.aui.AUI_NB_SCROLL_BUTTONS):
3030        wx.aui.AuiNotebook.__init__(self, parent, style=style)
3031        if size: self.SetSize(size)
3032        self.parent = parent
3033        self.PageChangeHandler = None
3034       
3035    def PageChangeEvent(self,event):
3036        G2frame = self.parent.G2frame
3037        page = event.GetSelection()
3038        if self.PageChangeHandler:
3039            if log.LogInfo['Logging']:
3040                log.MakeTabLog(
3041                    G2frame.dataFrame.GetTitle(),
3042                    G2frame.dataDisplay.GetPageText(page)
3043                    )
3044            self.PageChangeHandler(event)
3045           
3046    def Bind(self,eventtype,handler,*args,**kwargs):
3047        '''Override the Bind() function so that page change events can be trapped
3048        '''
3049        if eventtype == wx.aui.EVT_AUINOTEBOOK_PAGE_CHANGED:
3050            self.PageChangeHandler = handler
3051            wx.aui.AuiNotebook.Bind(self,eventtype,self.PageChangeEvent)
3052            return
3053        wx.aui.AuiNotebook.Bind(self,eventtype,handler,*args,**kwargs)
3054                                                     
3055    def Clear(self):       
3056        GSNoteBook.DeleteAllPages(self)
3057       
3058    def FindPage(self,name):
3059        numPage = self.GetPageCount()
3060        for page in range(numPage):
3061            if self.GetPageText(page) == name:
3062                return page
3063
3064    def ChangeSelection(self,page):
3065        # in wx.Notebook ChangeSelection is like SetSelection, but it
3066        # does not invoke the event related to pressing the tab button
3067        # I don't see a way to do that in aui.
3068        oldPage = self.GetSelection()
3069        self.SetSelection(page)
3070        return oldPage
3071
3072    # def __getattribute__(self,name):
3073    #     '''This method provides a way to print out a message every time
3074    #     that a method in a class is called -- to see what all the calls
3075    #     might be, or where they might be coming from.
3076    #     Cute trick for debugging!
3077    #     '''
3078    #     attr = object.__getattribute__(self, name)
3079    #     if hasattr(attr, '__call__'):
3080    #         def newfunc(*args, **kwargs):
3081    #             print('GSauiNoteBook calling %s' %attr.__name__)
3082    #             result = attr(*args, **kwargs)
3083    #             return result
3084    #         return newfunc
3085    #     else:
3086    #         return attr
3087           
3088################################################################################
3089#### Help support routines
3090################################################################################
3091class MyHelp(wx.Menu):
3092    '''
3093    A class that creates the contents of a help menu.
3094    The menu will start with two entries:
3095
3096    * 'Help on <helpType>': where helpType is a reference to an HTML page to
3097      be opened
3098    * About: opens an About dialog using OnHelpAbout. N.B. on the Mac this
3099      gets moved to the App menu to be consistent with Apple style.
3100
3101    NOTE: for this to work properly with respect to system menus, the title
3102    for the menu must be &Help, or it will not be processed properly:
3103
3104    ::
3105
3106       menu.Append(menu=MyHelp(self,...),title="&Help")
3107
3108    '''
3109    def __init__(self,frame,includeTree=False,morehelpitems=[]):
3110        wx.Menu.__init__(self,'')
3111        self.HelpById = {}
3112        self.frame = frame
3113        self.Append(help='', id=wx.ID_ABOUT, kind=wx.ITEM_NORMAL,
3114            text='&About GSAS-II')
3115        frame.Bind(wx.EVT_MENU, self.OnHelpAbout, id=wx.ID_ABOUT)
3116        if GSASIIpath.whichsvn():
3117            helpobj = self.Append(
3118                help='', id=wx.ID_ANY, kind=wx.ITEM_NORMAL,
3119                text='&Check for updates')
3120            frame.Bind(wx.EVT_MENU, self.OnCheckUpdates, helpobj)
3121            helpobj = self.Append(
3122                help='', id=wx.ID_ANY, kind=wx.ITEM_NORMAL,
3123                text='&Regress to an old GSAS-II version')
3124            frame.Bind(wx.EVT_MENU, self.OnSelectVersion, helpobj)
3125        for lbl,indx in morehelpitems:
3126            helpobj = self.Append(text=lbl,
3127                id=wx.ID_ANY, kind=wx.ITEM_NORMAL)
3128            frame.Bind(wx.EVT_MENU, self.OnHelpById, helpobj)
3129            self.HelpById[helpobj.GetId()] = indx
3130        # add help lookup(s) in gsasii.html
3131        self.AppendSeparator()
3132        if includeTree:
3133            helpobj = self.Append(text='Help on Data tree',
3134                                  id=wx.ID_ANY, kind=wx.ITEM_NORMAL)
3135            frame.Bind(wx.EVT_MENU, self.OnHelpById, id=helpobj.GetId())
3136            self.HelpById[helpobj.GetId()] = 'Data tree'
3137        helpobj = self.Append(text='Help on current data tree item',id=wx.ID_ANY, kind=wx.ITEM_NORMAL)
3138        frame.Bind(wx.EVT_MENU, self.OnHelpById, id=helpobj.GetId())
3139       
3140    def OnHelpById(self,event):
3141        '''Called when Help on... is pressed in a menu. Brings up a web page
3142        for documentation. Uses the helpKey value from the dataFrame window
3143        unless a special help key value has been defined for this menu id in
3144        self.HelpById
3145
3146        Note that self may be child of the main window (G2frame) or of the dataFrame
3147        '''
3148        if hasattr(self.frame,'dataFrame'):  # find the dataFrame
3149            dataFrame = self.frame.dataFrame
3150        else:
3151            dataFrame = self.frame
3152           
3153        try:
3154            helpKey = dataFrame.helpKey # BHT: look up help from helpKey in data window
3155            #if GSASIIpath.GetConfigValue('debug'): print 'dataFrame help: key=',helpKey
3156        except AttributeError:
3157            helpKey = ''
3158            if GSASIIpath.GetConfigValue('debug'):
3159                print('No helpKey for current dataFrame!')
3160        helpType = self.HelpById.get(event.GetId(),helpKey)
3161        if helpType == 'Tutorials':
3162            dlg = OpenTutorial(self.frame)
3163            dlg.ShowModal()
3164            dlg.Destroy()
3165            return
3166        else:
3167            ShowHelp(helpType,self.frame)
3168
3169    def OnHelpAbout(self, event):
3170        "Display an 'About GSAS-II' box"
3171        import GSASII
3172        info = wx.AboutDialogInfo()
3173        info.Name = 'GSAS-II'
3174        ver = GSASIIpath.svnGetRev()
3175        if ver: 
3176            info.Version = 'Revision '+str(ver)+' (svn), version '+GSASII.__version__
3177        else:
3178            info.Version = 'Revision '+str(GSASIIpath.GetVersionNumber())+' (.py files), version '+GSASII.__version__
3179        #info.Developers = ['Robert B. Von Dreele','Brian H. Toby']
3180        info.Copyright = ('(c) ' + time.strftime('%Y') +
3181''' Argonne National Laboratory
3182This product includes software developed
3183by the UChicago Argonne, LLC, as
3184Operator of Argonne National Laboratory.''')
3185        info.Description = '''General Structure Analysis System-II (GSAS-II)
3186Robert B. Von Dreele and Brian H. Toby
3187
3188Please cite as:
3189  B.H. Toby & R.B. Von Dreele, J. Appl. Cryst. 46, 544-549 (2013)
3190For small angle use cite:
3191  R.B. Von Dreele, J. Appl. Cryst. 47, 1748-9 (2014)
3192For DIFFaX use cite:
3193  M.M.J. Treacy, J.M. Newsam & M.W. Deem,
3194  Proc. Roy. Soc. Lond. A 433, 499-520 (1991)
3195'''
3196        info.WebSite = ("https://subversion.xray.aps.anl.gov/trac/pyGSAS","GSAS-II home page")
3197        wx.AboutBox(info)
3198
3199    def OnCheckUpdates(self,event):
3200        '''Check if the GSAS-II repository has an update for the current source files
3201        and perform that update if requested.
3202        '''           
3203        if not GSASIIpath.whichsvn():
3204            dlg = wx.MessageDialog(self.frame,
3205                                   'No Subversion','Cannot update GSAS-II because subversion (svn) was not found.',
3206                                   wx.OK)
3207            dlg.ShowModal()
3208            dlg.Destroy()
3209            return
3210        wx.BeginBusyCursor()
3211        local = GSASIIpath.svnGetRev()
3212        if local is None: 
3213            wx.EndBusyCursor()
3214            dlg = wx.MessageDialog(self.frame,
3215                                   'Unable to run subversion on the GSAS-II current directory. Is GSAS-II installed correctly?',
3216                                   'Subversion error',
3217                                   wx.OK)
3218            dlg.ShowModal()
3219            dlg.Destroy()
3220            return
3221        print 'Installed GSAS-II version: '+local
3222        repos = GSASIIpath.svnGetRev(local=False)
3223        wx.EndBusyCursor()
3224        if repos is None: 
3225            dlg = wx.MessageDialog(self.frame,
3226                                   'Unable to access the GSAS-II server. Is this computer on the internet?',
3227                                   'Server unavailable',
3228                                   wx.OK)
3229            dlg.ShowModal()
3230            dlg.Destroy()
3231            return
3232        print 'GSAS-II version on server: '+repos
3233        if local == repos:
3234            dlg = wx.MessageDialog(self.frame,
3235                                   'GSAS-II is up-to-date. Version '+local+' is already loaded.',
3236                                   'GSAS-II Up-to-date',
3237                                   wx.OK)
3238            dlg.ShowModal()
3239            dlg.Destroy()
3240            return
3241        mods = GSASIIpath.svnFindLocalChanges()
3242        if mods:
3243            dlg = wx.MessageDialog(self.frame,
3244                                   'You have version '+local+
3245                                   ' of GSAS-II installed, but the current version is '+repos+
3246                                   '. However, '+str(len(mods))+
3247                                   ' file(s) on your local computer have been modified.'
3248                                   ' Updating will attempt to merge your local changes with '
3249                                   'the latest GSAS-II version, but if '
3250                                   'conflicts arise, local changes will be '
3251                                   'discarded. It is also possible that the '
3252                                   'local changes my prevent GSAS-II from running. '
3253                                   'Press OK to start an update if this is acceptable:',
3254                                   'Local GSAS-II Mods',
3255                                   wx.OK|wx.CANCEL)
3256            if dlg.ShowModal() != wx.ID_OK:
3257                dlg.Destroy()
3258                return
3259            else:
3260                dlg.Destroy()
3261        else:
3262            dlg = wx.MessageDialog(self.frame,
3263                                   'You have version '+local+
3264                                   ' of GSAS-II installed, but the current version is '+repos+
3265                                   '. Press OK to start an update:',
3266                                   'GSAS-II Updates',
3267                                   wx.OK|wx.CANCEL)
3268            if dlg.ShowModal() != wx.ID_OK:
3269                dlg.Destroy()
3270                return
3271            dlg.Destroy()
3272        print 'start updates'
3273        dlg = wx.MessageDialog(self.frame,
3274                               'Your project will now be saved, GSAS-II will exit and an update '
3275                               'will be performed and GSAS-II will restart. Press Cancel to '
3276                               'abort the update',
3277                               'Start update?',
3278                               wx.OK|wx.CANCEL)
3279        if dlg.ShowModal() != wx.ID_OK:
3280            dlg.Destroy()
3281            return
3282        dlg.Destroy()
3283        try:
3284            self.frame.OnFileSave(event)
3285            GPX = self.frame.GSASprojectfile
3286        except AttributeError:
3287            self.frame.G2frame.OnFileSave(event)
3288            GPX = self.frame.G2frame.GSASprojectfile
3289        GSASIIpath.svnUpdateProcess(projectfile=GPX)
3290        return
3291
3292    def OnSelectVersion(self,event):
3293        '''Allow the user to select a specific version of GSAS-II
3294        '''
3295        if not GSASIIpath.whichsvn():
3296            dlg = wx.MessageDialog(self,'No Subversion','Cannot update GSAS-II because subversion (svn) '+
3297                                   'was not found.'
3298                                   ,wx.OK)
3299            dlg.ShowModal()
3300            return
3301        local = GSASIIpath.svnGetRev()
3302        if local is None: 
3303            dlg = wx.MessageDialog(self.frame,
3304                                   'Unable to run subversion on the GSAS-II current directory. Is GSAS-II installed correctly?',
3305                                   'Subversion error',
3306                                   wx.OK)
3307            dlg.ShowModal()
3308            dlg.Destroy()
3309            return
3310        mods = GSASIIpath.svnFindLocalChanges()
3311        if mods:
3312            dlg = wx.MessageDialog(self.frame,
3313                                   'You have version '+local+
3314                                   ' of GSAS-II installed'
3315                                   '. However, '+str(len(mods))+
3316                                   ' file(s) on your local computer have been modified.'
3317                                   ' Downdating will attempt to merge your local changes with '
3318                                   'the selected GSAS-II version. '
3319                                   'Downdating is not encouraged because '
3320                                   'if merging is not possible, your local changes will be '
3321                                   'discarded. It is also possible that the '
3322                                   'local changes my prevent GSAS-II from running. '
3323                                   'Press OK to continue anyway.',
3324                                   'Local GSAS-II Mods',
3325                                   wx.OK|wx.CANCEL)
3326            if dlg.ShowModal() != wx.ID_OK:
3327                dlg.Destroy()
3328                return
3329            dlg.Destroy()
3330        if GSASIIpath.svnGetRev(local=False) is None:
3331            dlg = wx.MessageDialog(self.frame,
3332                                   'Error obtaining current GSAS-II version. Is internet access working correctly?',
3333                                   'Subversion error',
3334                                   wx.OK)
3335            dlg.ShowModal()
3336            dlg.Destroy()
3337            return
3338        dlg = downdate(parent=self.frame)
3339        if dlg.ShowModal() == wx.ID_OK:
3340            ver = dlg.getVersion()
3341        else:
3342            dlg.Destroy()
3343            return
3344        dlg.Destroy()
3345        print('start regress to '+str(ver))
3346        try:
3347            self.frame.OnFileSave(event)
3348            GPX = self.frame.GSASprojectfile
3349        except AttributeError:
3350            self.frame.G2frame.OnFileSave(event)
3351            GPX = self.frame.G2frame.GSASprojectfile
3352        GSASIIpath.svnUpdateProcess(projectfile=GPX,version=str(ver))
3353        return
3354
3355################################################################################
3356class HelpButton(wx.Button):
3357    '''Create a help button that displays help information.
3358    The text is displayed in a modal message window.
3359
3360    TODO: it might be nice if it were non-modal: e.g. it stays around until
3361    the parent is deleted or the user closes it, but this did not work for
3362    me.
3363
3364    :param parent: the panel which will be the parent of the button
3365    :param str msg: the help text to be displayed
3366    '''
3367    def __init__(self,parent,msg):
3368        if sys.platform == "darwin": 
3369            wx.Button.__init__(self,parent,wx.ID_HELP)
3370        else:
3371            wx.Button.__init__(self,parent,wx.ID_ANY,'?',style=wx.BU_EXACTFIT)
3372        self.Bind(wx.EVT_BUTTON,self._onPress)
3373        self.msg=StripIndents(msg)
3374        self.parent = parent
3375    def _onClose(self,event):
3376        self.dlg.EndModal(wx.ID_CANCEL)
3377    def _onPress(self,event):
3378        'Respond to a button press by displaying the requested text'
3379        #dlg = wx.MessageDialog(self.parent,self.msg,'Help info',wx.OK)
3380        self.dlg = wx.Dialog(self.parent,wx.ID_ANY,'Help information', 
3381                        style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)
3382        #self.dlg.SetBackgroundColour(wx.WHITE)
3383        mainSizer = wx.BoxSizer(wx.VERTICAL)
3384        txt = wx.StaticText(self.dlg,wx.ID_ANY,self.msg)
3385        mainSizer.Add(txt,1,wx.ALL|wx.EXPAND,10)
3386        txt.SetBackgroundColour(wx.WHITE)
3387
3388        btnsizer = wx.BoxSizer(wx.HORIZONTAL)
3389        btn = wx.Button(self.dlg, wx.ID_CLOSE) 
3390        btn.Bind(wx.EVT_BUTTON,self._onClose)
3391        btnsizer.Add(btn)
3392        mainSizer.Add(btnsizer, 0, wx.ALIGN_CENTER|wx.ALL, 5)
3393        self.dlg.SetSizer(mainSizer)
3394        mainSizer.Fit(self.dlg)
3395        self.dlg.CenterOnParent()
3396        self.dlg.ShowModal()
3397        self.dlg.Destroy()
3398################################################################################
3399class MyHtmlPanel(wx.Panel):
3400    '''Defines a panel to display HTML help information, as an alternative to
3401    displaying help information in a web browser.
3402    '''
3403    def __init__(self, frame, id):
3404        self.frame = frame
3405        wx.Panel.__init__(self, frame, id)
3406        sizer = wx.BoxSizer(wx.VERTICAL)
3407        back = wx.Button(self, -1, "Back")
3408        back.Bind(wx.EVT_BUTTON, self.OnBack)
3409        self.htmlwin = G2HtmlWindow(self, id, size=(750,450))
3410        sizer.Add(self.htmlwin, 1,wx.EXPAND)
3411        sizer.Add(back, 0, wx.ALIGN_LEFT, 0)
3412        self.SetSizer(sizer)
3413        sizer.Fit(frame)       
3414        self.Bind(wx.EVT_SIZE,self.OnHelpSize)
3415    def OnHelpSize(self,event):         #does the job but weirdly!!
3416        anchor = self.htmlwin.GetOpenedAnchor()
3417        if anchor:           
3418            self.htmlwin.ScrollToAnchor(anchor)
3419            wx.CallAfter(self.htmlwin.ScrollToAnchor,anchor)
3420            if event: event.Skip()
3421    def OnBack(self, event):
3422        self.htmlwin.HistoryBack()
3423    def LoadFile(self,file):
3424        pos = file.rfind('#')
3425        if pos != -1:
3426            helpfile = file[:pos]
3427            helpanchor = file[pos+1:]
3428        else:
3429            helpfile = file
3430            helpanchor = None
3431        self.htmlwin.LoadPage(helpfile)
3432        if helpanchor is not None:
3433            self.htmlwin.ScrollToAnchor(helpanchor)
3434            xs,ys = self.htmlwin.GetViewStart()
3435            self.htmlwin.Scroll(xs,ys-1)
3436################################################################################
3437class G2HtmlWindow(wx.html.HtmlWindow):
3438    '''Displays help information in a primitive HTML browser type window
3439    '''
3440    def __init__(self, parent, *args, **kwargs):
3441        self.parent = parent
3442        wx.html.HtmlWindow.__init__(self, parent, *args, **kwargs)
3443    def LoadPage(self, *args, **kwargs):
3444        wx.html.HtmlWindow.LoadPage(self, *args, **kwargs)
3445        self.TitlePage()
3446    def OnLinkClicked(self, *args, **kwargs):
3447        wx.html.HtmlWindow.OnLinkClicked(self, *args, **kwargs)
3448        xs,ys = self.GetViewStart()
3449        self.Scroll(xs,ys-1)
3450        self.TitlePage()
3451    def HistoryBack(self, *args, **kwargs):
3452        wx.html.HtmlWindow.HistoryBack(self, *args, **kwargs)
3453        self.TitlePage()
3454    def TitlePage(self):
3455        self.parent.frame.SetTitle(self.GetOpenedPage() + ' -- ' + 
3456            self.GetOpenedPageTitle())
3457
3458################################################################################
3459def StripIndents(msg):
3460    'Strip indentation from multiline strings'
3461    msg1 = msg.replace('\n ','\n')
3462    while msg != msg1:
3463        msg = msg1
3464        msg1 = msg.replace('\n ','\n')
3465    return msg.replace('\n\t','\n')
3466
3467def StripUnicode(string,subs='.'):
3468    '''Strip non-ASCII characters from strings
3469   
3470    :param str string: string to strip Unicode characters from
3471    :param str subs: character(s) to place into string in place of each
3472      Unicode character. Defaults to '.'
3473
3474    :returns: a new string with only ASCII characters
3475    '''
3476    s = ''
3477    for c in string:
3478        if ord(c) < 128:
3479            s += c
3480        else:
3481            s += subs
3482    return s.encode('ascii','replace')
3483       
3484################################################################################
3485# configuration routines (for editing config.py)
3486def SaveGPXdirectory(path):
3487    if GSASIIpath.GetConfigValue('Starting_directory') == path: return
3488    vars = GetConfigValsDocs()
3489    try:
3490        vars['Starting_directory'][1] = path
3491        if GSASIIpath.GetConfigValue('debug'): print('Saving GPX path: '+path)
3492        SaveConfigVars(vars)
3493    except KeyError:
3494        pass
3495
3496def SaveImportDirectory(path):
3497    if GSASIIpath.GetConfigValue('Import_directory') == path: return
3498    vars = GetConfigValsDocs()
3499    try:
3500        vars['Import_directory'][1] = path
3501        if GSASIIpath.GetConfigValue('debug'): print('Saving Import path: '+path)
3502        SaveConfigVars(vars)
3503    except KeyError:
3504        pass
3505
3506def GetConfigValsDocs():
3507    '''Reads the module referenced in fname (often <module>.__file__) and
3508    return a dict with names of global variables as keys.
3509    For each global variable, the value contains four items:
3510
3511    :returns: a dict where keys are names defined in module config_example.py
3512      where the value is a list of four items, as follows:
3513
3514         * item 0: the default value
3515         * item 1: the current value
3516         * item 2: the initial value (starts same as item 1)
3517         * item 3: the "docstring" that follows variable definition
3518
3519    '''
3520    import config_example
3521    import ast
3522    fname = os.path.splitext(config_example.__file__)[0]+'.py' # convert .pyc to .py
3523    with open(fname, 'r') as f:
3524        fstr = f.read()
3525    fstr = fstr.replace('\r\n', '\n').replace('\r', '\n')
3526    if not fstr.endswith('\n'):
3527        fstr += '\n'
3528    tree = ast.parse(fstr)
3529    d = {}
3530    key = None
3531    for node in ast.walk(tree):
3532        if isinstance(node,ast.Assign):
3533            key = node.targets[0].id
3534            d[key] = [config_example.__dict__.get(key),
3535                      GSASIIpath.configDict.get(key),
3536                      GSASIIpath.configDict.get(key),'']
3537        elif isinstance(node,ast.Expr) and key:
3538            d[key][3] = node.value.s.strip()
3539        else:
3540            key = None
3541    return d
3542
3543def SaveConfigVars(vars,parent=None):
3544    '''Write the current config variable values to config.py
3545
3546    :params dict vars: a dictionary of variable settings and meanings as
3547      created in :func:`GetConfigValsDocs`.
3548    :param parent: wx.Frame object or None (default) for parent
3549      of error message if no file can be written.
3550    :returns: True if unable to write the file, None otherwise
3551    '''
3552    # try to write to where an old config file is located
3553    try:
3554        import config
3555        savefile = config.__file__
3556    except ImportError: # no config.py file yet
3557        savefile = os.path.join(GSASIIpath.path2GSAS2,'config.py')
3558    # try to open file for write
3559    try:
3560        savefile = os.path.splitext(savefile)[0]+'.py' # convert .pyc to .py
3561        fp = open(savefile,'w')
3562    except IOError:  # can't write there, write in local mods directory
3563        # create a local mods directory, if needed
3564        if not os.path.exists(os.path.expanduser('~/.G2local/')):
3565            print('Creating directory '+os.path.expanduser('~/.G2local/'))
3566            os.mkdir(os.path.expanduser('~/.G2local/'))
3567            sys.path.insert(0,os.path.expanduser('~/.G2local/'))
3568        savefile = os.path.join(os.path.expanduser('~/.G2local/'),'config.py')
3569        try:
3570            fp = open(savefile,'w')
3571        except IOError:
3572            if parent:
3573                G2MessageBox(parent,'Error trying to write configuration to '+savefile,
3574                    'Unable to save')
3575            else:
3576                print('Error trying to write configuration to '+savefile)
3577            return True
3578    import datetime
3579    fp.write("'''\n")
3580    fp.write("*config.py: Configuration options*\n----------------------------------\n")
3581    fp.write("This file created in SelectConfigSetting on {:%d %b %Y %H:%M}\n".
3582             format(datetime.datetime.now()))
3583    fp.write("'''\n\n")
3584    fp.write("import os.path\n")
3585    fp.write("import GSASIIpath\n\n")
3586    for var in sorted(vars.keys(),key=lambda s: s.lower()):
3587        if vars[var][1] is None: continue
3588        if vars[var][1] == '': continue
3589        if vars[var][0] == vars[var][1]: continue
3590        try:
3591            float(vars[var][1]) # test for number
3592            fp.write(var + ' = ' + str(vars[var][1])+'\n')
3593        except:
3594            try:
3595                eval(vars[var][1]) # test for an expression
3596                fp.write(var + ' = ' + str(vars[var][1])+'\n')
3597            except: # must be a string
3598                varstr = vars[var][1]
3599                if '\\' in varstr:
3600                    fp.write(var + ' = os.path.normpath("' + varstr.replace('\\','/') +'")\n')
3601                else:
3602                    fp.write(var + ' = "' + str(varstr)+'"\n')
3603        if vars[var][3]:
3604            fp.write("'''" + str(vars[var][3]) + "\n'''\n\n")
3605    fp.close()
3606    print('wrote file '+savefile)
3607
3608class SelectConfigSetting(wx.Dialog):
3609    '''Dialog to select configuration variables and set associated values.
3610    '''
3611    def __init__(self,parent=None):
3612        style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER
3613        wx.Dialog.__init__(self, parent, wx.ID_ANY, 'Set Config Variable', style=style)
3614        self.sizer = wx.BoxSizer(wx.VERTICAL)
3615        self.vars = GetConfigValsDocs()
3616       
3617        label = wx.StaticText(
3618            self,  wx.ID_ANY,
3619            'Select a GSAS-II configuration variable to change'
3620            )
3621        self.sizer.Add(label, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
3622        self.choice = {}
3623        btn = G2ChoiceButton(self, sorted(self.vars.keys(),key=lambda s: s.lower()),
3624            strLoc=self.choice,strKey=0,onChoice=self.OnSelection)
3625        btn.SetLabel("")
3626        self.sizer.Add(btn)
3627
3628        self.varsizer = wx.BoxSizer(wx.VERTICAL)
3629        self.sizer.Add(self.varsizer,1,wx.ALL|wx.EXPAND,1)
3630       
3631        self.doclbl = wx.StaticBox(self, wx.ID_ANY, "")
3632        self.doclblsizr = wx.StaticBoxSizer(self.doclbl)
3633        self.docinfo = wx.StaticText(self,  wx.ID_ANY, "")
3634        self.doclblsizr.Add(self.docinfo, 0, wx.ALIGN_LEFT|wx.ALL, 5)
3635        self.sizer.Add(self.doclblsizr, 0, wx.ALIGN_LEFT|wx.EXPAND|wx.ALL, 5)
3636        btnsizer = wx.BoxSizer(wx.HORIZONTAL)
3637        self.saveBtn = wx.Button(self,-1,"Save current settings")
3638        btnsizer.Add(self.saveBtn, 0, wx.ALL, 2) 
3639        self.saveBtn.Bind(wx.EVT_BUTTON, self.OnSave)
3640        self.saveBtn.Enable(False)
3641        self.applyBtn = wx.Button(self,-1,"Use current (no save)")
3642        btnsizer.Add(self.applyBtn, 0, wx.ALL, 2) 
3643        self.applyBtn.Bind(wx.EVT_BUTTON, self.OnApplyChanges)
3644        self.applyBtn.Enable(False)
3645       
3646        btn = wx.Button(self,wx.ID_CANCEL)
3647        btnsizer.Add(btn, 0, wx.ALL, 2) 
3648        self.sizer.Add(btnsizer, 0, wx.ALIGN_CENTRE|wx.ALL, 5) 
3649               
3650        self.SetSizer(self.sizer)
3651        self.sizer.Fit(self)
3652        self.CenterOnParent()
3653       
3654    def OnChange(self,event=None):
3655        ''' Check if anything been changed. Turn the save button on/off.
3656        '''
3657        for var in self.vars:
3658            if self.vars[var][0] is None and self.vars[var][1] is not None:
3659                # make blank strings into None, if that is the default
3660                if self.vars[var][1].strip() == '': self.vars[var][1] = None
3661            if self.vars[var][1] != self.vars[var][2]:
3662                #print 'changed',var,self.vars[var][:3]
3663                self.saveBtn.Enable(True)
3664                self.applyBtn.Enable(True)
3665                break
3666        else:
3667            self.saveBtn.Enable(False)
3668            self.applyBtn.Enable(False)
3669        try:
3670            self.resetBtn.Enable(True)
3671        except:
3672            pass
3673       
3674    def OnApplyChanges(self,event=None):
3675        'Set config variables to match the current settings'
3676        GSASIIpath.SetConfigValue(self.vars)
3677        self.EndModal(wx.ID_OK)
3678       
3679    def OnSave(self,event):
3680        '''Write the config variables to config.py and then set them
3681        as the current settings
3682        '''
3683        if not SaveConfigVars(self.vars,parent=self):
3684            self.OnApplyChanges() # force a reload of the config settings
3685            self.EndModal(wx.ID_OK)
3686
3687    def OnBoolSelect(self,event):
3688        'Respond to a change in a True/False variable'
3689        rb = event.GetEventObject()
3690        var = self.choice[0]
3691        self.vars[var][1] = (rb.GetSelection() == 0)
3692        self.OnChange()
3693        wx.CallAfter(self.OnSelection)
3694       
3695    def onSelDir(self,event):
3696        'Select a directory from a menu'
3697        dlg = wx.DirDialog(self, "Choose a directory:",style=wx.DD_DEFAULT_STYLE)
3698        if dlg.ShowModal() == wx.ID_OK:
3699            var = self.choice[0]
3700            self.vars[var][1] = dlg.GetPath()
3701            self.strEd.SetValue(self.vars[var][1])
3702            self.OnChange()
3703        dlg.Destroy()
3704       
3705    def OnSelection(self):
3706        'show a selected variable'
3707        def OnNewColorBar(event):
3708            self.vars['Contour_color'][1] = self.colSel.GetValue()
3709            self.OnChange(event)
3710
3711        self.varsizer.DeleteWindows()
3712        var = self.choice[0]
3713        showdef = True
3714        if var not in self.vars:
3715            raise Exception,"How did this happen?"
3716        if type(self.vars[var][0]) is int:
3717            ed = ValidatedTxtCtrl(self,self.vars[var],1,typeHint=int,OKcontrol=self.OnChange)
3718            self.varsizer.Add(ed, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
3719        elif type(self.vars[var][0]) is float:
3720            ed = ValidatedTxtCtrl(self,self.vars[var],1,typeHint=float,OKcontrol=self.OnChange)
3721            self.varsizer.Add(ed, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
3722        elif type(self.vars[var][0]) is bool:
3723            showdef = False
3724            lbl = "value for "+var
3725            ch = []
3726            for i,v in enumerate((True,False)):
3727                s = str(v)
3728                if v == self.vars[var][0]:
3729                    defopt = i
3730                    s += ' (default)'
3731                ch += [s]
3732            rb = wx.RadioBox(self, wx.ID_ANY, lbl, wx.DefaultPosition, wx.DefaultSize,
3733                ch, 1, wx.RA_SPECIFY_COLS)
3734            # set initial value
3735            if self.vars[var][1] is None:
3736                rb.SetSelection(defopt)
3737            elif self.vars[var][1]:
3738                rb.SetSelection(0)
3739            else:
3740                rb.SetSelection(1)
3741            rb.Bind(wx.EVT_RADIOBOX,self.OnBoolSelect)
3742            self.varsizer.Add(rb, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
3743        else:
3744            if var.endswith('_directory') or var.endswith('_location'):
3745                btn = wx.Button(self,wx.ID_ANY,'Select from dialog...')
3746                sz = (400,-1)
3747            else:
3748                btn = None
3749                sz = (250,-1)
3750            if var == 'Contour_color':
3751                if self.vars[var][1] is None:
3752                    self.vars[var][1] = 'paired'
3753                colorList = sorted([m for m in mpl.cm.datad.keys() ],key=lambda s: s.lower())   #if not m.endswith("_r")
3754                self.colSel = wx.ComboBox(self,value=self.vars[var][1],choices=colorList,
3755                    style=wx.CB_READONLY|wx.CB_DROPDOWN)
3756                self.colSel.Bind(wx.EVT_COMBOBOX, OnNewColorBar)
3757                self.varsizer.Add(self.colSel, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
3758            else:
3759                self.strEd = ValidatedTxtCtrl(self,self.vars[var],1,typeHint=str,
3760                    OKcontrol=self.OnChange,size=sz)
3761                if self.vars[var][1] is not None:
3762                    self.strEd.SetValue(self.vars[var][1])
3763                self.varsizer.Add(self.strEd, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
3764            if btn:
3765                btn.Bind(wx.EVT_BUTTON,self.onSelDir)
3766                self.varsizer.Add(btn, 0, wx.ALIGN_CENTRE|wx.ALL, 5) 
3767        # button for reset to default value
3768        lbl = "Reset to Default"
3769        if showdef: # spell out default when needed
3770            lbl += ' (='+str(self.vars[var][0])+')'
3771            #label = wx.StaticText(self,  wx.ID_ANY, 'Default value = '+str(self.vars[var][0]))
3772            #self.varsizer.Add(label, 0, wx.ALIGN_LEFT|wx.ALL, 5)
3773        self.resetBtn = wx.Button(self,-1,lbl)
3774        self.resetBtn.Bind(wx.EVT_BUTTON, self.OnClear)
3775        if self.vars[var][1] is not None and self.vars[var][1] != '': # show current value, if one
3776            #label = wx.StaticText(self,  wx.ID_ANY, 'Current value = '+str(self.vars[var][1]))
3777            #self.varsizer.Add(label, 0, wx.ALIGN_LEFT|wx.ALL, 5)
3778            self.resetBtn.Enable(True)
3779        else:
3780            self.resetBtn.Enable(False)
3781        self.varsizer.Add(self.resetBtn, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
3782        # show meaning, if defined
3783        self.doclbl.SetLabel("Description of "+str(var)) 
3784        if self.vars[var][3]:
3785            self.docinfo.SetLabel(self.vars[var][3])
3786        else:
3787            self.docinfo.SetLabel("(not documented)")
3788        self.sizer.Fit(self)
3789        self.CenterOnParent()
3790        wx.CallAfter(self.SendSizeEvent)
3791
3792    def OnClear(self, event):
3793        var = self.choice[0]
3794        self.vars[var][1] = self.vars[var][0]
3795        self.OnChange()
3796        wx.CallAfter(self.OnSelection)
3797       
3798################################################################################
3799class downdate(wx.Dialog):
3800    '''Dialog to allow a user to select a version of GSAS-II to install
3801    '''
3802    def __init__(self,parent=None):
3803        style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER
3804        wx.Dialog.__init__(self, parent, wx.ID_ANY, 'Select Version', style=style)
3805        pnl = wx.Panel(self)
3806        sizer = wx.BoxSizer(wx.VERTICAL)
3807        insver = GSASIIpath.svnGetRev(local=True)
3808        curver = int(GSASIIpath.svnGetRev(local=False))
3809        label = wx.StaticText(
3810            pnl,  wx.ID_ANY,
3811            'Select a specific GSAS-II version to install'
3812            )
3813        sizer.Add(label, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
3814        sizer1 = wx.BoxSizer(wx.HORIZONTAL)
3815        sizer1.Add(
3816            wx.StaticText(pnl,  wx.ID_ANY,
3817                          'Currently installed version: '+str(insver)),
3818            0, wx.ALIGN_CENTRE|wx.ALL, 5)
3819        sizer.Add(sizer1)
3820        sizer1 = wx.BoxSizer(wx.HORIZONTAL)
3821        sizer1.Add(
3822            wx.StaticText(pnl,  wx.ID_ANY,
3823                          'Select GSAS-II version to install: '),
3824            0, wx.ALIGN_CENTRE|wx.ALL, 5)
3825        self.spin = wx.SpinCtrl(pnl, wx.ID_ANY,size=(150,-1))
3826        self.spin.SetRange(1, curver)
3827        self.spin.SetValue(curver)
3828        self.Bind(wx.EVT_SPINCTRL, self._onSpin, self.spin)
3829        self.Bind(wx.EVT_KILL_FOCUS, self._onSpin, self.spin)
3830        sizer1.Add(self.spin)
3831        sizer.Add(sizer1)
3832
3833        line = wx.StaticLine(pnl,-1, size=(-1,3), style=wx.LI_HORIZONTAL)
3834        sizer.Add(line, 0, wx.EXPAND|wx.ALIGN_CENTER|wx.ALL, 10)
3835
3836        self.text = wx.StaticText(pnl,  wx.ID_ANY, "")
3837        sizer.Add(self.text, 0, wx.ALIGN_LEFT|wx.EXPAND|wx.ALL, 5)
3838
3839        line = wx.StaticLine(pnl,-1, size=(-1,3), style=wx.LI_HORIZONTAL)
3840        sizer.Add(line, 0, wx.EXPAND|wx.ALIGN_CENTER|wx.ALL, 10)
3841        sizer.Add(
3842            wx.StaticText(
3843                pnl,  wx.ID_ANY,
3844                'If "Install" is pressed, your project will be saved;\n'
3845                'GSAS-II will exit; The specified version will be loaded\n'
3846                'and GSAS-II will restart. Press "Cancel" to abort.'),
3847            0, wx.EXPAND|wx.ALL, 10)
3848        btnsizer = wx.StdDialogButtonSizer()
3849        btn = wx.Button(pnl, wx.ID_OK, "Install")
3850        btn.SetDefault()
3851        btnsizer.AddButton(btn)
3852        btn = wx.Button(pnl, wx.ID_CANCEL)
3853        btnsizer.AddButton(btn)
3854        btnsizer.Realize()
3855        sizer.Add(btnsizer, 0, wx.ALIGN_CENTER|wx.ALL, 5)
3856        pnl.SetSizer(sizer)
3857        sizer.Fit(self)
3858        self.topsizer=sizer
3859        self.CenterOnParent()
3860        self._onSpin(None)
3861
3862    def _onSpin(self,event):
3863        'Called to load info about the selected version in the dialog'
3864        if event: event.Skip()
3865        ver = self.spin.GetValue()
3866        d = GSASIIpath.svnGetLog(version=ver)
3867        date = d.get('date','?').split('T')[0]
3868        s = '(Version '+str(ver)+' created '+date
3869        s += ' by '+d.get('author','?')+')'
3870        msg = d.get('msg')
3871        if msg: s += '\n\nComment: '+msg
3872        self.text.SetLabel(s)
3873        self.topsizer.Fit(self)
3874
3875    def getVersion(self):
3876        'Get the version number in the dialog'
3877        return self.spin.GetValue()
3878
3879################################################################################
3880#### Display Help information
3881################################################################################
3882# define some globals
3883htmlPanel = None
3884htmlFrame = None
3885htmlFirstUse = True
3886#helpLocDict = {}  # to be implemented if we ever split gsasii.html over multiple files
3887path2GSAS2 = os.path.dirname(os.path.realpath(__file__)) # save location of this file
3888def ShowHelp(helpType,frame):
3889    '''Called to bring up a web page for documentation.'''
3890    global htmlFirstUse,htmlPanel,htmlFrame
3891    # no defined link to use, create a default based on key
3892    helplink = 'gsasII.html'
3893    if helpType:
3894        helplink += '#'+helpType.replace(')','').replace('(','_').replace(' ','_')
3895    # determine if a web browser or the internal viewer should be used for help info
3896    if GSASIIpath.GetConfigValue('Help_mode'):
3897        helpMode = GSASIIpath.GetConfigValue('Help_mode')
3898    else:
3899        helpMode = 'browser'
3900    if helpMode == 'internal':
3901        helplink = os.path.join(path2GSAS2,'help',helplink)
3902        try:
3903            htmlPanel.LoadFile(helplink)
3904            htmlFrame.Raise()
3905        except:
3906            htmlFrame = wx.Frame(frame, -1, size=(610, 510))
3907            htmlFrame.Show(True)
3908            htmlFrame.SetTitle("HTML Window") # N.B. reset later in LoadFile
3909            htmlPanel = MyHtmlPanel(htmlFrame,-1)
3910            htmlPanel.LoadFile(helplink)
3911    else:
3912        if sys.platform == "darwin": # for Mac, force use of safari to preserve anchors on file URLs
3913            wb = webbrowser.MacOSXOSAScript('safari')
3914        else:
3915            wb = webbrowser
3916        helplink = os.path.join(path2GSAS2,'help',helplink)
3917        pfx = "file://"
3918        if sys.platform.lower().startswith('win'):
3919            pfx = ''
3920        #if GSASIIpath.GetConfigValue('debug'): print 'Help link=',pfx+helplink
3921        if htmlFirstUse:
3922            wb.open_new(pfx+helplink)
3923            htmlFirstUse = False
3924        else:
3925            wb.open(pfx+helplink, new=0, autoraise=True)
3926
3927def ShowWebPage(URL,frame):
3928    '''Called to show a tutorial web page.
3929    '''
3930    global htmlFirstUse,htmlPanel,htmlFrame
3931    # determine if a web browser or the internal viewer should be used for help info
3932    if GSASIIpath.GetConfigValue('Help_mode'):
3933        helpMode = GSASIIpath.GetConfigValue('Help_mode')
3934    else:
3935        helpMode = 'browser'
3936    if helpMode == 'internal':
3937        try:
3938            htmlPanel.LoadFile(URL)
3939            htmlFrame.Raise()
3940        except:
3941            htmlFrame = wx.Frame(frame, -1, size=(610, 510))
3942            htmlFrame.Show(True)
3943            htmlFrame.SetTitle("HTML Window") # N.B. reset later in LoadFile
3944            htmlPanel = MyHtmlPanel(htmlFrame,-1)
3945            htmlPanel.LoadFile(URL)
3946    else:
3947        if URL.startswith('http'): 
3948            pfx = ''
3949        elif sys.platform.lower().startswith('win'):
3950            pfx = ''
3951        else:
3952            pfx = "file://"
3953        if htmlFirstUse:
3954            webbrowser.open_new(pfx+URL)
3955            htmlFirstUse = False
3956        else:
3957            webbrowser.open(pfx+URL, new=0, autoraise=True)
3958
3959################################################################################
3960#### Tutorials support
3961################################################################################
3962G2BaseURL = "https://subversion.xray.aps.anl.gov/pyGSAS"
3963# N.B. tutorialCatalog is generated by routine catalog.py, which also generates the appropriate
3964# empty directories (.../MT/* .../trunk/GSASII/* *=[help,Exercises])
3965tutorialCatalog = (
3966    # tutorial dir,      web page file name,      title for page
3967
3968    ['StartingGSASII', 'Starting GSAS.htm', 'Starting GSAS-II'],
3969       
3970    ['LabData', 'Laboratory X.htm', 'Fitting laboratory X-ray powder data for fluoroapatite'],
3971    ['CWNeutron', 'Neutron CW Powder Data.htm', 'CW Neutron Powder fit for Yttrium-Iron Garnet'],
3972
3973    ['FitPeaks', 'Fit Peaks.htm', 'Fitting individual peaks & autoindexing'],
3974    ['CFjadarite', 'Charge Flipping in GSAS.htm', '     Charge Flipping structure solution for jadarite'],
3975    ['CFsucrose', 'Charge Flipping - sucrose.htm','     Charge Flipping structure solution for sucrose'],
3976    ['BkgFit', 'FitBkgTut.htm',  'Fitting the Starting Background using Fixed Points'],
3977       
3978    ['CWCombined', 'Combined refinement.htm', 'Combined X-ray/CW-neutron refinement of PbSO4'],
3979    ['TOF-CW Joint Refinement', 'TOF combined XN Rietveld refinement in GSAS.htm', 'Combined X-ray/TOF-neutron Rietveld refinement'],
3980    ['SeqRefine', 'SequentialTutorial.htm', 'Sequential refinement of multiple datasets'],
3981    ['SeqParametric', 'ParametricFitting.htm', '     Parametric Fitting and Pseudo Variables for Sequential Fits'],
3982       
3983    ['StackingFaults-I', 'Stacking Faults-I.htm', 'Stacking fault simulations for diamond'],
3984    ['StackingFaults-II', 'Stacking Faults II.htm', 'Stacking fault simulations for Keokuk kaolinite'],
3985    ['StackingFaults-III', 'Stacking Faults-III.htm', 'Stacking fault simulations for Georgia kaolinite'],
3986       
3987    ['CFXraySingleCrystal', 'CFSingleCrystal.htm', 'Charge Flipping structure solution with Xray single crystal data'],       
3988    ['TOF Charge Flipping', 'Charge Flipping with TOF single crystal data in GSASII.htm', 'Charge flipping with neutron TOF single crystal data'],
3989    ['MCsimanneal', 'MCSA in GSAS.htm', 'Monte-Carlo simulated annealing structure determination'],
3990       
3991    ['MerohedralTwins', 'Merohedral twin refinement in GSAS.htm', 'Merohedral twin refinements'],
3992
3993    ['2DCalibration', 'Calibration of an area detector in GSAS.htm', 'Calibration of an area detector'],
3994    ['2DIntegration', 'Integration of area detector data in GSAS.htm', '     Integration of area detector data'],
3995    ['TOF Calibration', 'Calibration of a TOF powder diffractometer.htm', 'Calibration of a Neutron TOF diffractometer'],
3996    ['TOF Single Crystal Refinement', 'TOF single crystal refinement in GSAS.htm', 'Single crystal refinement from TOF data'],
3997       
3998    ['2DStrain', 'Strain fitting of 2D data in GSAS-II.htm', 'Strain fitting of 2D data'],
3999    ['2DTexture', 'Texture analysis of 2D data in GSAS-II.htm', 'Texture analysis of 2D data'],
4000             
4001    ['SAsize', 'Small Angle Size Distribution.htm', 'Small angle x-ray data size distribution (alumina powder)'],
4002    ['SAfit', 'Fitting Small Angle Scattering Data.htm', '     Fitting small angle x-ray data (alumina powder)'],
4003    ['SAimages', 'Small Angle Image Processing.htm', 'Image Processing of small angle x-ray data'],
4004    ['SAseqref', 'Sequential Refinement of Small Angle Scattering Data.htm', 'Sequential refinement with small angle scattering data'],
4005   
4006    #['ExampleDir', 'ExamplePage.html', 'Example Tutorial Title'],
4007    )
4008
4009class OpenTutorial(wx.Dialog):
4010    '''Open a tutorial web page, optionally copying the web page, screen images and
4011    data file(s) to the local disk.
4012    '''
4013   
4014    def __init__(self,parent):
4015        style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER
4016        wx.Dialog.__init__(self, parent, wx.ID_ANY, 'Open Tutorial', style=style)
4017        self.frame = parent
4018        # self.frame can be the tree window frame or the data editing window frame, set G2frame to the
4019        # tree either way
4020        if hasattr(self.frame,'G2frame'):
4021            self.G2frame = self.frame.G2frame
4022        else:
4023            self.G2frame = self.frame
4024        pnl = wx.Panel(self)
4025        sizer = wx.BoxSizer(wx.VERTICAL)
4026        sizer1 = wx.BoxSizer(wx.HORIZONTAL)       
4027        label = wx.StaticText(
4028            pnl,  wx.ID_ANY,
4029            'Select the tutorial to be run and the mode of access'
4030            )
4031        msg = '''GSAS-II tutorials and their sample data files
4032        require a fair amount of storage space; few users will
4033        use all of them. This dialog allows users to load selected
4034        tutorials (along with their sample data) to their computer;
4035        optionally all tutorials can be downloaded.
4036
4037        Downloaded tutorials can be viewed and run without internet
4038        access. Tutorials can also be viewed without download, but
4039        users will need to download the sample data files manually.
4040
4041        The location used to download tutorials is set using the
4042        "Set download location" which is saved as the "Tutorial_location"
4043        configuration option see File/Preference or the
4044        config_example.py file. System managers can select to have
4045        tutorial files installed at a shared location.
4046        '''
4047        self.SetTutorialPath()
4048        hlp = HelpButton(pnl,msg)
4049        sizer1.Add((-1,-1),1, wx.EXPAND, 0)
4050        sizer1.Add(label, 0, wx.ALIGN_CENTRE|wx.ALL, 0)
4051        sizer1.Add((-1,-1),1, wx.EXPAND, 0)
4052        sizer1.Add(hlp,0,wx.ALIGN_RIGHT|wx.ALL)
4053        sizer.Add(sizer1,0,wx.EXPAND|wx.ALL,0)
4054        sizer.Add((10,10))
4055        sizer0 = wx.BoxSizer(wx.HORIZONTAL)       
4056        sizer1a = wx.BoxSizer(wx.VERTICAL)
4057        sizer1b = wx.BoxSizer(wx.VERTICAL)
4058        btn = wx.Button(pnl, wx.ID_ANY, "Download a tutorial and view")
4059        btn.Bind(wx.EVT_BUTTON, self.SelectAndDownload)
4060        sizer1a.Add(btn,0,WACV)
4061        btn = wx.Button(pnl, wx.ID_ANY, "Select from downloaded tutorials")
4062        btn.Bind(wx.EVT_BUTTON, self.onSelectDownloaded)
4063        sizer1a.Add(btn,0,WACV)
4064        btn = wx.Button(pnl, wx.ID_ANY, "Browse tutorial on web")
4065        btn.Bind(wx.EVT_BUTTON, self.onWebBrowse)
4066        sizer1a.Add(btn,0,WACV)
4067        btn = wx.Button(pnl, wx.ID_ANY, "Update downloaded tutorials")
4068        btn.Bind(wx.EVT_BUTTON, self.UpdateDownloaded)
4069        sizer1b.Add(btn,0,WACV)
4070        btn = wx.Button(pnl, wx.ID_ANY, "Download all tutorials")
4071        btn.Bind(wx.EVT_BUTTON, self.DownloadAll)
4072        sizer1b.Add(btn,0,WACV)
4073        sizer0.Add(sizer1a,0,wx.EXPAND|wx.ALL,0)
4074        sizer0.Add(sizer1b,0,wx.EXPAND|wx.ALL,0)
4075        sizer.Add(sizer0,5,wx.EXPAND|wx.ALL,5)
4076       
4077        sizer.Add((10,10))
4078        sizer1 = wx.BoxSizer(wx.HORIZONTAL)
4079        btn = wx.Button(pnl, wx.ID_ANY, "Set download location")
4080        btn.Bind(wx.EVT_BUTTON, self.SelectDownloadLoc)
4081        sizer1.Add(btn,0,WACV)
4082        self.dataLoc = wx.StaticText(pnl, wx.ID_ANY,self.tutorialPath)
4083        sizer1.Add(self.dataLoc,0,WACV)
4084        sizer.Add(sizer1)
4085       
4086        btnsizer = wx.StdDialogButtonSizer()
4087        btn = wx.Button(pnl, wx.ID_CANCEL,"Done")
4088        btnsizer.AddButton(btn)
4089        btnsizer.Realize()
4090        sizer.Add(btnsizer, 0, wx.ALIGN_CENTER|wx.ALL, 5)
4091        pnl.SetSizer(sizer)
4092        sizer.Fit(self)
4093        self.topsizer=sizer
4094        self.CenterOnParent()
4095
4096    def SetTutorialPath(self):
4097        '''Get the tutorial location if set; if not pick a default
4098        directory in a logical place
4099        '''
4100        if GSASIIpath.GetConfigValue('Tutorial_location'):
4101            self.tutorialPath = os.path.abspath(GSASIIpath.GetConfigValue('Tutorial_location'))
4102        elif (sys.platform.lower().startswith('win') and
4103              os.path.exists(os.path.abspath(os.path.expanduser('~/My Documents')))):
4104            self.tutorialPath = os.path.abspath(os.path.expanduser('~/My Documents/G2tutorials'))
4105        elif (sys.platform.lower().startswith('win') and
4106              os.path.exists(os.path.abspath(os.path.expanduser('~/Documents')))):
4107            self.tutorialPath = os.path.abspath(os.path.expanduser('~/Documents/G2tutorials'))
4108        else:
4109            self.tutorialPath = os.path.abspath(os.path.expanduser('~/G2tutorials'))
4110
4111    def SelectAndDownload(self,event):
4112        '''Make a list of all tutorials on web and allow user to choose one to
4113        download and then view
4114        '''
4115        indices = [j for j,i in enumerate(tutorialCatalog)
4116            if not os.path.exists(os.path.join(self.tutorialPath,i[0],i[1]))]
4117        if not indices:
4118            G2MessageBox(self,'All tutorials are downloaded','None to download')
4119            return
4120        choices = [tutorialCatalog[i][2] for i in indices]
4121        selected = self.ChooseTutorial(choices)
4122        if selected is None: return
4123        j = indices[selected]
4124        fullpath = os.path.join(self.tutorialPath,tutorialCatalog[j][0],tutorialCatalog[j][1])
4125        fulldir = os.path.join(self.tutorialPath,tutorialCatalog[j][0])
4126        URL = G2BaseURL+'/Tutorials/'+tutorialCatalog[j][0]+'/'
4127        if GSASIIpath.svnInstallDir(URL,fulldir):
4128            ShowWebPage(fullpath,self.frame)
4129        else:
4130            G2MessageBox(self,'Error downloading tutorial','Download error')
4131        self.EndModal(wx.ID_OK)
4132        self.G2frame.TutorialImportDir = os.path.join(self.tutorialPath,tutorialCatalog[j][0],'data')
4133
4134    def onSelectDownloaded(self,event):
4135        '''Select a previously downloaded tutorial
4136        '''
4137        indices = [j for j,i in enumerate(tutorialCatalog)
4138            if os.path.exists(os.path.join(self.tutorialPath,i[0],i[1]))]
4139        if not indices:
4140            G2MessageBox(self,
4141                         'There are no downloaded tutorials in '+self.tutorialPath,
4142                         'None downloaded')
4143            return
4144        choices = [tutorialCatalog[i][2] for i in indices]
4145        selected = self.ChooseTutorial(choices)
4146        if selected is None: return
4147        j = indices[selected]
4148        fullpath = os.path.join(self.tutorialPath,tutorialCatalog[j][0],tutorialCatalog[j][1])
4149        self.EndModal(wx.ID_OK)
4150        ShowWebPage(fullpath,self.frame)
4151        self.G2frame.TutorialImportDir = os.path.join(self.tutorialPath,tutorialCatalog[j][0],'data')
4152       
4153    def onWebBrowse(self,event):
4154        '''Make a list of all tutorials on web and allow user to view one.
4155        '''
4156        choices = [i[2] for i in tutorialCatalog]
4157        selected = self.ChooseTutorial(choices)
4158        if selected is None: return       
4159        tutdir = tutorialCatalog[selected][0]
4160        tutfil = tutorialCatalog[selected][1]
4161        # open web page remotely, don't worry about data
4162        URL = G2BaseURL+'/Tutorials/'+tutdir+'/'+tutfil
4163        self.EndModal(wx.ID_OK)
4164        ShowWebPage(URL,self.frame)
4165       
4166    def ChooseTutorial(self,choices):
4167        'choose a tutorial from a list'
4168        def onDoubleClick(event):
4169            'double-click closes the dialog'
4170            dlg.EndModal(wx.ID_OK)
4171        dlg = wx.Dialog(self,wx.ID_ANY,
4172                        'Select a tutorial to view. NB: indented ones require prerequisite',
4173                        style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)
4174        pnl = wx.Panel(dlg)
4175        sizer = wx.BoxSizer(wx.VERTICAL)
4176        listbox = wx.ListBox(pnl, wx.ID_ANY, choices=choices,
4177                             size=(450, 100),
4178                             style=wx.LB_SINGLE)
4179        sizer.Add(listbox,1,WACV|wx.EXPAND|wx.ALL,1)
4180        listbox.Bind(wx.EVT_LISTBOX_DCLICK, onDoubleClick)
4181        sizer.Add((10,10))
4182        btnsizer = wx.StdDialogButtonSizer()
4183        btn = wx.Button(pnl, wx.ID_CANCEL)
4184        btnsizer.AddButton(btn)
4185        OKbtn = wx.Button(pnl, wx.ID_OK)
4186        OKbtn.SetDefault()
4187        btnsizer.AddButton(OKbtn)
4188        btnsizer.Realize()
4189        sizer.Add((-1,5))
4190        sizer.Add(btnsizer,0,wx.ALIGN_RIGHT,50)
4191       
4192        pnl.SetSizer(sizer)
4193        sizer.Fit(dlg)
4194        self.CenterOnParent()
4195        if dlg.ShowModal() != wx.ID_OK:
4196            dlg.Destroy()
4197            return
4198        selected = listbox.GetSelection()
4199        dlg.Destroy()
4200        wx.Yield() # close window right away so user sees something happen
4201        if selected < 0: return
4202        return selected
4203
4204    def UpdateDownloaded(self,event):
4205        'Find the downloaded tutorials and run an svn update on them'
4206        updated = 0
4207        for i in tutorialCatalog:
4208            if not os.path.exists(os.path.join(self.tutorialPath,i[0],i[1])): continue
4209            print('Updating '+i[0])
4210            GSASIIpath.svnUpdateDir(os.path.join(self.tutorialPath,i[0]))
4211            updated += 0
4212        if not updated:
4213            G2MessageBox(self,'Warning, you have no downloaded tutorials','None Downloaded')
4214        self.EndModal(wx.ID_OK)
4215       
4216    def DownloadAll(self,event):
4217        'Download or update all tutorials'
4218        fail = ''
4219        for i in tutorialCatalog:
4220            if os.path.exists(os.path.join(self.tutorialPath,i[0],i[1])):
4221                print('Updating '+i[0])
4222                GSASIIpath.svnUpdateDir(os.path.join(self.tutorialPath,i[0]))
4223            else:
4224                fulldir = os.path.join(self.tutorialPath,i[0])
4225                URL = G2BaseURL+'/Tutorials/'+i[0]+'/'
4226                if not GSASIIpath.svnInstallDir(URL,fulldir):
4227                    if fail: fail += ', '
4228                    fail += i[0]
4229        if fail: 
4230            G2MessageBox(self,'Error downloading tutorial(s)\n\t'+fail,'Download error')
4231        self.EndModal(wx.ID_OK)
4232                   
4233    def SelectDownloadLoc(self,event):
4234        '''Select a download location,
4235        Cancel resets to the default
4236        '''
4237        dlg = wx.DirDialog(self, "Choose a directory for tutorial downloads:",
4238                           defaultPath=self.tutorialPath)#,style=wx.DD_DEFAULT_STYLE)
4239                           #)
4240        try:
4241            if dlg.ShowModal() != wx.ID_OK:
4242                return
4243            pth = dlg.GetPath()
4244        finally:
4245            dlg.Destroy()
4246
4247        if not os.path.exists(pth):
4248            try:
4249                os.makedirs(pth)    #failing for no obvious reason
4250            except OSError:
4251                msg = 'The selected directory is not valid.\n\t'
4252                msg += pth
4253                msg += '\n\nAn attempt to create the directory failed'
4254                G2MessageBox(self.frame,msg)
4255                return
4256        if os.path.exists(os.path.join(pth,"help")) and os.path.exists(os.path.join(pth,"Exercises")):
4257            print("Note that you may have old tutorial files in the following directories")
4258            print('\t'+os.path.join(pth,"help"))
4259            print('\t'+os.path.join(pth,"Exercises"))
4260            print('Subdirectories in the above can be deleted to save space\n\n')
4261        self.tutorialPath = pth
4262        self.dataLoc.SetLabel(self.tutorialPath)
4263        if GSASIIpath.GetConfigValue('Tutorial_location') == pth: return
4264        vars = GetConfigValsDocs()
4265        try:
4266            vars['Tutorial_location'][1] = pth
4267            if GSASIIpath.GetConfigValue('debug'): print('Saving Tutorial_location: '+pth)
4268            GSASIIpath.SetConfigValue(vars)
4269            SaveConfigVars(vars)
4270        except KeyError:
4271            pass
4272           
4273if __name__ == '__main__':
4274    app = wx.PySimpleApp()
4275    GSASIIpath.InvokeDebugOpts()
4276    frm = wx.Frame(None) # create a frame
4277    frm.Show(True)
4278   
4279    #======================================================================
4280    # test Grid with GridFractionEditor
4281    #======================================================================
4282    # tbl = [[1.,2.,3.],[1.1,2.1,3.1]]
4283    # colTypes = 3*[wg.GRID_VALUE_FLOAT+':10,5',]
4284    # Gtbl = Table(tbl,types=colTypes,rowLabels=['a','b'],colLabels=['1','2','3'])
4285    # Grid = GSGrid(frm)
4286    # Grid.SetTable(Gtbl,True)
4287    # for i in (0,1,2):
4288    #     attr = wx.grid.GridCellAttr()
4289    #     attr.IncRef()
4290    #     attr.SetEditor(GridFractionEditor(Grid))
4291    #     Grid.SetColAttr(i, attr)
4292    # frm.SetSize((400,200))
4293    # app.MainLoop()
4294    # sys.exit()
4295    #======================================================================
4296    # test Tutorial access
4297    #======================================================================
4298    # dlg = OpenTutorial(frm)
4299    # if dlg.ShowModal() == wx.ID_OK:
4300    #     print "OK"
4301    # else:
4302    #     print "Cancel"
4303    # dlg.Destroy()
4304    # sys.exit()
4305    #======================================================================
4306    # test ScrolledMultiEditor
4307    #======================================================================
4308    # Data1 = {
4309    #      'Order':1,
4310    #      'omega':'string',
4311    #      'chi':2.0,
4312    #      'phi':'',
4313    #      }
4314    # elemlst = sorted(Data1.keys())
4315    # prelbl = sorted(Data1.keys())
4316    # dictlst = len(elemlst)*[Data1,]
4317    #Data2 = [True,False,False,True]
4318    #Checkdictlst = len(Data2)*[Data2,]
4319    #Checkelemlst = range(len(Checkdictlst))
4320    # print 'before',Data1,'\n',Data2
4321    # dlg = ScrolledMultiEditor(
4322    #     frm,dictlst,elemlst,prelbl,
4323    #     checkdictlst=Checkdictlst,checkelemlst=Checkelemlst,
4324    #     checklabel="Refine?",
4325    #     header="test")
4326    # if dlg.ShowModal() == wx.ID_OK:
4327    #     print "OK"
4328    # else:
4329    #     print "Cancel"
4330    # print 'after',Data1,'\n',Data2
4331    # dlg.Destroy()
4332    Data3 = {
4333         'Order':1.0,
4334         'omega':1.1,
4335         'chi':2.0,
4336         'phi':2.3,
4337         'Order1':1.0,
4338         'omega1':1.1,
4339         'chi1':2.0,
4340         'phi1':2.3,
4341         'Order2':1.0,
4342         'omega2':1.1,
4343         'chi2':2.0,
4344         'phi2':2.3,
4345         }
4346    elemlst = sorted(Data3.keys())
4347    dictlst = len(elemlst)*[Data3,]
4348    prelbl = elemlst[:]
4349    prelbl[0]="this is a much longer label to stretch things out"
4350    Data2 = len(elemlst)*[False,]
4351    Data2[1] = Data2[3] = True
4352    Checkdictlst = len(elemlst)*[Data2,]
4353    Checkelemlst = range(len(Checkdictlst))
4354    #print 'before',Data3,'\n',Data2
4355    #print dictlst,"\n",elemlst
4356    #print Checkdictlst,"\n",Checkelemlst
4357    # dlg = ScrolledMultiEditor(
4358    #     frm,dictlst,elemlst,prelbl,
4359    #     checkdictlst=Checkdictlst,checkelemlst=Checkelemlst,
4360    #     checklabel="Refine?",
4361    #     header="test",CopyButton=True)
4362    # if dlg.ShowModal() == wx.ID_OK:
4363    #     print "OK"
4364    # else:
4365    #     print "Cancel"
4366    #print 'after',Data3,'\n',Data2
4367
4368    # Data2 = list(range(100))
4369    # elemlst += range(2,6)
4370    # postlbl += range(2,6)
4371    # dictlst += len(range(2,6))*[Data2,]
4372
4373    # prelbl = range(len(elemlst))
4374    # postlbl[1] = "a very long label for the 2nd item to force a horiz. scrollbar"
4375    # header="""This is a longer\nmultiline and perhaps silly header"""
4376    # dlg = ScrolledMultiEditor(frm,dictlst,elemlst,prelbl,postlbl,
4377    #                           header=header,CopyButton=True)
4378    # print Data1
4379    # if dlg.ShowModal() == wx.ID_OK:
4380    #     for d,k in zip(dictlst,elemlst):
4381    #         print k,d[k]
4382    # dlg.Destroy()
4383    # if CallScrolledMultiEditor(frm,dictlst,elemlst,prelbl,postlbl,
4384    #                            header=header):
4385    #     for d,k in zip(dictlst,elemlst):
4386    #         print k,d[k]
4387
4388    #======================================================================
4389    # test G2MultiChoiceDialog
4390    #======================================================================
4391    choices = []
4392    for i in range(21):
4393        choices.append("option_"+str(i))
4394    od = {
4395        'label_1':'This is a bool','value_1':True,
4396        'label_2':'This is a int','value_2':-1,
4397        'label_3':'This is a float','value_3':1.0,
4398        'label_4':'This is a string','value_4':'test',}
4399    dlg = G2MultiChoiceDialog(frm, 'Sequential refinement',
4400                              'Select dataset to include',
4401                              choices,extraOpts=od)
4402    sel = range(2,11,2)
4403    dlg.SetSelections(sel)
4404    dlg.SetSelections((1,5))
4405    if dlg.ShowModal() == wx.ID_OK:
4406        for sel in dlg.GetSelections():
4407            print sel,choices[sel]
4408    print od
4409    od = {}
4410    dlg = G2MultiChoiceDialog(frm, 'Sequential refinement',
4411                              'Select dataset to include',
4412                              choices,extraOpts=od)
4413    sel = range(2,11,2)
4414    dlg.SetSelections(sel)
4415    dlg.SetSelections((1,5))
4416    if dlg.ShowModal() == wx.ID_OK: pass
4417    #======================================================================
4418    # test wx.MultiChoiceDialog
4419    #======================================================================
4420    # dlg = wx.MultiChoiceDialog(frm, 'Sequential refinement',
4421    #                           'Select dataset to include',
4422    #                           choices)
4423    # sel = range(2,11,2)
4424    # dlg.SetSelections(sel)
4425    # dlg.SetSelections((1,5))
4426    # if dlg.ShowModal() == wx.ID_OK:
4427    #     for sel in dlg.GetSelections():
4428    #         print sel,choices[sel]
4429
4430    # pnl = wx.Panel(frm)
4431    # siz = wx.BoxSizer(wx.VERTICAL)
4432
4433    # td = {'Goni':200.,'a':1.,'calc':1./3.,'string':'s'}
4434    # for key in sorted(td):
4435    #     txt = ValidatedTxtCtrl(pnl,td,key)
4436    #     siz.Add(txt)
4437    # pnl.SetSizer(siz)
4438    # siz.Fit(frm)
4439    # app.MainLoop()
4440    # print td
Note: See TracBrowser for help on using the repository browser.