source: trunk/GSASIIctrls.py @ 2509

Last change on this file since 2509 was 2509, checked in by toby, 7 years ago

fixes to help

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