source: trunk/GSASIIctrls.py @ 2527

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

remove debug code; remove code causing error on CIF export

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