source: branch/2frame/GSASIIctrls.py @ 2899

Last change on this file since 2899 was 2899, checked in by toby, 6 years ago

partial reorg

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