source: trunk/GSASIIctrls.py @ 2516

Last change on this file since 2516 was 2516, checked in by toby, 5 years ago

revise import to not assume Bank 1 with multibank instparm files; deal with unicode problem in CIF files; improve atoms use of selection from menu; add Pawley menu variable selection (plenty more to do)

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