source: trunk/GSASIIctrls.py @ 2869

Last change on this file since 2869 was 2869, checked in by toby, 4 years ago

change color on Seq parm fit plot

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