source: trunk/GSASIIctrls.py @ 2416

Last change on this file since 2416 was 2416, checked in by vondreele, 5 years ago

fix issues with summing images & powder patterns - SumDialog? is now a scrolled window & image sum files now opened correctly after sum is finished

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