source: trunk/GSASIIctrls.py @ 2769

Last change on this file since 2769 was 2769, checked in by vondreele, 6 years ago

remove commented imports from G2ctrls
force float for all common transform matrices
replace wx.TextCtrl? with G2G.ValidatedTxtCtrl? for floats in G2grid
(old code commented out for now - to be removed)
force r space group for H->R transformations (easily forgotten)

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