source: trunk/GSASIIctrls.py @ 2639

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

fix GridFractionEditor? bug on Ubuntu 16.10

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