source: trunk/GSASIIctrlGUI.py @ 3023

Last change on this file since 3023 was 3023, checked in by odonnell, 8 years ago

remove wx dependency for GSASIIscriptable

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