source: branch/2frame/GSASIIctrlGUI.py @ 2909

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

improve window title; still working on help

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