source: branch/2frame/GSASIIctrls.py @ 2888

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