source: trunk/GSASIIctrls.py @ 2113

Last change on this file since 2113 was 2113, checked in by vondreele, 7 years ago

better cleanup when main window is closed
add FlagSetDialog? class & use it for wave parameters

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