source: trunk/GSASIIctrls.py @ 2826

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

Revise space group entry

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