source: trunk/GSASIIctrls.py @ 2637

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

fix delete data item selection to include only data items
expand the choices for stride in data selection dialog
cosmetic stuff in plots

  • Property svn:eol-style set to native
  • Property svn:keywords set to Date Author Revision URL Id
File size: 179.4 KB
Line 
1# -*- coding: utf-8 -*-
2#GSASIIctrls - Custom GSAS-II GUI controls
3########### SVN repository information ###################
4# $Date: 2017-01-17 18:13:28 +0000 (Tue, 17 Jan 2017) $
5# $Author: vondreele $
6# $Revision: 2637 $
7# $URL: trunk/GSASIIctrls.py $
8# $Id: GSASIIctrls.py 2637 2017-01-17 18:13:28Z vondreele $
9########### SVN repository information ###################
10'''
11*GSASIIctrls: Custom GUI controls*
12-------------------------------------------
13
14A library of GUI controls for reuse throughout GSAS-II
15
16(at present many are still in GSASIIgrid, but with time will be moved here)
17
18'''
19import os
20import sys
21import wx
22import wx.grid as wg
23# import wx.wizard as wz
24import wx.aui
25import wx.lib.scrolledpanel as wxscroll
26import 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: 2637 $")
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()
2949        if val != self.startValue:
2950            changed = True
2951            neg = False
2952            if '-' in val:
2953                neg = True
2954            if '/' in val and '.' not in val:
2955                val += '.'
2956            elif 's' in val and not 'sind(' in val:
2957                if neg:
2958                    val = '-sind('+val.strip('-s')+')'
2959                else:
2960                    val = 'sind('+val.strip('s')+')'
2961            elif 'c' in val and not 'cosd(' in val:
2962                if neg:
2963                    val = '-cosd('+val.strip('-c')+')'
2964                else:
2965                    val = 'cosd('+val.strip('c')+')'
2966            try:
2967                self.nextval = val = float(eval(val))
2968            except (SyntaxError,NameError,ZeroDivisionError):
2969                val = self.startValue
2970                return None
2971           
2972            if oldVal is None: # this arg appears in 2.9+; before, we should go ahead & change the table
2973                grid.GetTable().SetValue(row, col, val) # update the table
2974            # otherwise self.ApplyEdit gets called
2975
2976        self.startValue = ''
2977        self._tc.SetValue('')
2978        return changed
2979   
2980    def ApplyEdit(self, row, col, grid):
2981        """ Called only in wx >= 2.9
2982        Save the value of the control into the grid if EndEdit() returns as True
2983        """
2984        grid.GetTable().SetValue(row, col, self.nextval) # update the table
2985
2986    def Reset(self):
2987        self._tc.SetValue(self.startValue)
2988        self._tc.SetInsertionPointEnd()
2989
2990    def Clone(self,grid):
2991        return GridFractionEditor(grid)
2992
2993    def StartingKey(self, evt):
2994        self.OnChar(evt)
2995        if evt.GetSkipped():
2996            self._tc.EmulateKeyPress(evt)
2997
2998    def OnChar(self, evt):
2999        key = evt.GetKeyCode()
3000        if key == 15:
3001            return
3002        if key > 255:
3003            evt.Skip()
3004            return
3005        char = chr(key)
3006        if char in '.+-/0123456789cosind()':
3007            self._tc.WriteText(char)
3008        else:
3009            evt.Skip()
3010           
3011################################################################################
3012#####  Customized Notebook
3013################################################################################           
3014class GSNoteBook(wx.aui.AuiNotebook):
3015    '''Notebook used in various locations; implemented with wx.aui extension
3016    '''
3017    def __init__(self, parent, name='',size = None,style=wx.aui.AUI_NB_TOP |
3018        wx.aui.AUI_NB_SCROLL_BUTTONS):
3019        wx.aui.AuiNotebook.__init__(self, parent, style=style)
3020        if size: self.SetSize(size)
3021        self.parent = parent
3022        self.PageChangeHandler = None
3023       
3024    def PageChangeEvent(self,event):
3025        G2frame = self.parent.G2frame
3026        page = event.GetSelection()
3027        if self.PageChangeHandler:
3028            if log.LogInfo['Logging']:
3029                log.MakeTabLog(
3030                    G2frame.dataFrame.GetTitle(),
3031                    G2frame.dataDisplay.GetPageText(page)
3032                    )
3033            self.PageChangeHandler(event)
3034           
3035    def Bind(self,eventtype,handler,*args,**kwargs):
3036        '''Override the Bind() function so that page change events can be trapped
3037        '''
3038        if eventtype == wx.aui.EVT_AUINOTEBOOK_PAGE_CHANGED:
3039            self.PageChangeHandler = handler
3040            wx.aui.AuiNotebook.Bind(self,eventtype,self.PageChangeEvent)
3041            return
3042        wx.aui.AuiNotebook.Bind(self,eventtype,handler,*args,**kwargs)
3043                                                     
3044    def Clear(self):       
3045        GSNoteBook.DeleteAllPages(self)
3046       
3047    def FindPage(self,name):
3048        numPage = self.GetPageCount()
3049        for page in range(numPage):
3050            if self.GetPageText(page) == name:
3051                return page
3052
3053    def ChangeSelection(self,page):
3054        # in wx.Notebook ChangeSelection is like SetSelection, but it
3055        # does not invoke the event related to pressing the tab button
3056        # I don't see a way to do that in aui.
3057        oldPage = self.GetSelection()
3058        self.SetSelection(page)
3059        return oldPage
3060
3061    # def __getattribute__(self,name):
3062    #     '''This method provides a way to print out a message every time
3063    #     that a method in a class is called -- to see what all the calls
3064    #     might be, or where they might be coming from.
3065    #     Cute trick for debugging!
3066    #     '''
3067    #     attr = object.__getattribute__(self, name)
3068    #     if hasattr(attr, '__call__'):
3069    #         def newfunc(*args, **kwargs):
3070    #             print('GSauiNoteBook calling %s' %attr.__name__)
3071    #             result = attr(*args, **kwargs)
3072    #             return result
3073    #         return newfunc
3074    #     else:
3075    #         return attr
3076           
3077################################################################################
3078#### Help support routines
3079################################################################################
3080class MyHelp(wx.Menu):
3081    '''
3082    A class that creates the contents of a help menu.
3083    The menu will start with two entries:
3084
3085    * 'Help on <helpType>': where helpType is a reference to an HTML page to
3086      be opened
3087    * About: opens an About dialog using OnHelpAbout. N.B. on the Mac this
3088      gets moved to the App menu to be consistent with Apple style.
3089
3090    NOTE: for this to work properly with respect to system menus, the title
3091    for the menu must be &Help, or it will not be processed properly:
3092
3093    ::
3094
3095       menu.Append(menu=MyHelp(self,...),title="&Help")
3096
3097    '''
3098    def __init__(self,frame,includeTree=False,morehelpitems=[]):
3099        wx.Menu.__init__(self,'')
3100        self.HelpById = {}
3101        self.frame = frame
3102        self.Append(help='', id=wx.ID_ABOUT, kind=wx.ITEM_NORMAL,
3103            text='&About GSAS-II')
3104        frame.Bind(wx.EVT_MENU, self.OnHelpAbout, id=wx.ID_ABOUT)
3105        if GSASIIpath.whichsvn():
3106            helpobj = self.Append(
3107                help='', id=wx.ID_ANY, kind=wx.ITEM_NORMAL,
3108                text='&Check for updates')
3109            frame.Bind(wx.EVT_MENU, self.OnCheckUpdates, helpobj)
3110            helpobj = self.Append(
3111                help='', id=wx.ID_ANY, kind=wx.ITEM_NORMAL,
3112                text='&Regress to an old GSAS-II version')
3113            frame.Bind(wx.EVT_MENU, self.OnSelectVersion, helpobj)
3114        for lbl,indx in morehelpitems:
3115            helpobj = self.Append(text=lbl,
3116                id=wx.ID_ANY, kind=wx.ITEM_NORMAL)
3117            frame.Bind(wx.EVT_MENU, self.OnHelpById, helpobj)
3118            self.HelpById[helpobj.GetId()] = indx
3119        # add help lookup(s) in gsasii.html
3120        self.AppendSeparator()
3121        if includeTree:
3122            helpobj = self.Append(text='Help on Data tree',
3123                                  id=wx.ID_ANY, kind=wx.ITEM_NORMAL)
3124            frame.Bind(wx.EVT_MENU, self.OnHelpById, id=helpobj.GetId())
3125            self.HelpById[helpobj.GetId()] = 'Data tree'
3126        helpobj = self.Append(text='Help on current data tree item',id=wx.ID_ANY, kind=wx.ITEM_NORMAL)
3127        frame.Bind(wx.EVT_MENU, self.OnHelpById, id=helpobj.GetId())
3128       
3129    def OnHelpById(self,event):
3130        '''Called when Help on... is pressed in a menu. Brings up a web page
3131        for documentation. Uses the helpKey value from the dataFrame window
3132        unless a special help key value has been defined for this menu id in
3133        self.HelpById
3134
3135        Note that self may be child of the main window (G2frame) or of the dataFrame
3136        '''
3137        if hasattr(self.frame,'dataFrame'):  # find the dataFrame
3138            dataFrame = self.frame.dataFrame
3139        else:
3140            dataFrame = self.frame
3141           
3142        try:
3143            helpKey = dataFrame.helpKey # BHT: look up help from helpKey in data window
3144            #if GSASIIpath.GetConfigValue('debug'): print 'dataFrame help: key=',helpKey
3145        except AttributeError:
3146            helpKey = ''
3147            if GSASIIpath.GetConfigValue('debug'):
3148                print('No helpKey for current dataFrame!')
3149        helpType = self.HelpById.get(event.GetId(),helpKey)
3150        if helpType == 'Tutorials': 
3151            dlg = OpenTutorial(self.frame)
3152            dlg.ShowModal()
3153            dlg.Destroy()
3154            return
3155        else:
3156            ShowHelp(helpType,self.frame)
3157
3158    def OnHelpAbout(self, event):
3159        "Display an 'About GSAS-II' box"
3160        import GSASII
3161        info = wx.AboutDialogInfo()
3162        info.Name = 'GSAS-II'
3163        ver = GSASIIpath.svnGetRev()
3164        if ver: 
3165            info.Version = 'Revision '+str(ver)+' (svn), version '+GSASII.__version__
3166        else:
3167            info.Version = 'Revision '+str(GSASIIpath.GetVersionNumber())+' (.py files), version '+GSASII.__version__
3168        #info.Developers = ['Robert B. Von Dreele','Brian H. Toby']
3169        info.Copyright = ('(c) ' + time.strftime('%Y') +
3170''' Argonne National Laboratory
3171This product includes software developed
3172by the UChicago Argonne, LLC, as
3173Operator of Argonne National Laboratory.''')
3174        info.Description = '''General Structure Analysis System-II (GSAS-II)
3175Robert B. Von Dreele and Brian H. Toby
3176
3177Please cite as:
3178  B.H. Toby & R.B. Von Dreele, J. Appl. Cryst. 46, 544-549 (2013)
3179For small angle use cite:
3180  R.B. Von Dreele, J. Appl. Cryst. 47, 1748-9 (2014)
3181For DIFFaX use cite:
3182  M.M.J. Treacy, J.M. Newsam & M.W. Deem,
3183  Proc. Roy. Soc. Lond. A 433, 499-520 (1991)
3184'''
3185        info.WebSite = ("https://subversion.xray.aps.anl.gov/trac/pyGSAS","GSAS-II home page")
3186        wx.AboutBox(info)
3187
3188    def OnCheckUpdates(self,event):
3189        '''Check if the GSAS-II repository has an update for the current source files
3190        and perform that update if requested.
3191        '''           
3192        if not GSASIIpath.whichsvn():
3193            dlg = wx.MessageDialog(self.frame,
3194                                   'No Subversion','Cannot update GSAS-II because subversion (svn) was not found.',
3195                                   wx.OK)
3196            dlg.ShowModal()
3197            dlg.Destroy()
3198            return
3199        wx.BeginBusyCursor()
3200        local = GSASIIpath.svnGetRev()
3201        if local is None: 
3202            wx.EndBusyCursor()
3203            dlg = wx.MessageDialog(self.frame,
3204                                   'Unable to run subversion on the GSAS-II current directory. Is GSAS-II installed correctly?',
3205                                   'Subversion error',
3206                                   wx.OK)
3207            dlg.ShowModal()
3208            dlg.Destroy()
3209            return
3210        print 'Installed GSAS-II version: '+local
3211        repos = GSASIIpath.svnGetRev(local=False)
3212        wx.EndBusyCursor()
3213        if repos is None: 
3214            dlg = wx.MessageDialog(self.frame,
3215                                   'Unable to access the GSAS-II server. Is this computer on the internet?',
3216                                   'Server unavailable',
3217                                   wx.OK)
3218            dlg.ShowModal()
3219            dlg.Destroy()
3220            return
3221        print 'GSAS-II version on server: '+repos
3222        if local == repos:
3223            dlg = wx.MessageDialog(self.frame,
3224                                   'GSAS-II is up-to-date. Version '+local+' is already loaded.',
3225                                   'GSAS-II Up-to-date',
3226                                   wx.OK)
3227            dlg.ShowModal()
3228            dlg.Destroy()
3229            return
3230        mods = GSASIIpath.svnFindLocalChanges()
3231        if mods:
3232            dlg = wx.MessageDialog(self.frame,
3233                                   'You have version '+local+
3234                                   ' of GSAS-II installed, but the current version is '+repos+
3235                                   '. However, '+str(len(mods))+
3236                                   ' file(s) on your local computer have been modified.'
3237                                   ' Updating will attempt to merge your local changes with '
3238                                   'the latest GSAS-II version, but if '
3239                                   'conflicts arise, local changes will be '
3240                                   'discarded. It is also possible that the '
3241                                   'local changes my prevent GSAS-II from running. '
3242                                   'Press OK to start an update if this is acceptable:',
3243                                   'Local GSAS-II Mods',
3244                                   wx.OK|wx.CANCEL)
3245            if dlg.ShowModal() != wx.ID_OK:
3246                dlg.Destroy()
3247                return
3248            else:
3249                dlg.Destroy()
3250        else:
3251            dlg = wx.MessageDialog(self.frame,
3252                                   'You have version '+local+
3253                                   ' of GSAS-II installed, but the current version is '+repos+
3254                                   '. Press OK to start an update:',
3255                                   'GSAS-II Updates',
3256                                   wx.OK|wx.CANCEL)
3257            if dlg.ShowModal() != wx.ID_OK:
3258                dlg.Destroy()
3259                return
3260            dlg.Destroy()
3261        print 'start updates'
3262        dlg = wx.MessageDialog(self.frame,
3263                               'Your project will now be saved, GSAS-II will exit and an update '
3264                               'will be performed and GSAS-II will restart. Press Cancel to '
3265                               'abort the update',
3266                               'Start update?',
3267                               wx.OK|wx.CANCEL)
3268        if dlg.ShowModal() != wx.ID_OK:
3269            dlg.Destroy()
3270            return
3271        dlg.Destroy()
3272        try:
3273            self.frame.OnFileSave(event)
3274        except AttributeError:
3275            self.frame.G2frame.OnFileSave(event)
3276        GSASIIpath.svnUpdateProcess(projectfile=self.frame.GSASprojectfile)
3277        return
3278
3279    def OnSelectVersion(self,event):
3280        '''Allow the user to select a specific version of GSAS-II
3281        '''
3282        if not GSASIIpath.whichsvn():
3283            dlg = wx.MessageDialog(self,'No Subversion','Cannot update GSAS-II because subversion (svn) '+
3284                                   'was not found.'
3285                                   ,wx.OK)
3286            dlg.ShowModal()
3287            return
3288        local = GSASIIpath.svnGetRev()
3289        if local is None: 
3290            dlg = wx.MessageDialog(self.frame,
3291                                   'Unable to run subversion on the GSAS-II current directory. Is GSAS-II installed correctly?',
3292                                   'Subversion error',
3293                                   wx.OK)
3294            dlg.ShowModal()
3295            dlg.Destroy()
3296            return
3297        mods = GSASIIpath.svnFindLocalChanges()
3298        if mods:
3299            dlg = wx.MessageDialog(self.frame,
3300                                   'You have version '+local+
3301                                   ' of GSAS-II installed'
3302                                   '. However, '+str(len(mods))+
3303                                   ' file(s) on your local computer have been modified.'
3304                                   ' Downdating will attempt to merge your local changes with '
3305                                   'the selected GSAS-II version. '
3306                                   'Downdating is not encouraged because '
3307                                   'if merging is not possible, your local changes will be '
3308                                   'discarded. It is also possible that the '
3309                                   'local changes my prevent GSAS-II from running. '
3310                                   'Press OK to continue anyway.',
3311                                   'Local GSAS-II Mods',
3312                                   wx.OK|wx.CANCEL)
3313            if dlg.ShowModal() != wx.ID_OK:
3314                dlg.Destroy()
3315                return
3316            dlg.Destroy()
3317        if GSASIIpath.svnGetRev(local=False) is None:
3318            dlg = wx.MessageDialog(self.frame,
3319                                   'Error obtaining current GSAS-II version. Is internet access working correctly?',
3320                                   'Subversion error',
3321                                   wx.OK)
3322            dlg.ShowModal()
3323            dlg.Destroy()
3324            return
3325        dlg = downdate(parent=self.frame)
3326        if dlg.ShowModal() == wx.ID_OK:
3327            ver = dlg.getVersion()
3328        else:
3329            dlg.Destroy()
3330            return
3331        dlg.Destroy()
3332        print('start regress to '+str(ver))
3333        GSASIIpath.svnUpdateProcess(
3334            projectfile=self.frame.GSASprojectfile,
3335            version=str(ver)
3336            )
3337        try:
3338            self.frame.OnFileSave(event)
3339        except AttributeError:
3340            self.frame.G2frame.OnFileSave(event)
3341        return
3342
3343################################################################################
3344class HelpButton(wx.Button):
3345    '''Create a help button that displays help information.
3346    The text is displayed in a modal message window.
3347
3348    TODO: it might be nice if it were non-modal: e.g. it stays around until
3349    the parent is deleted or the user closes it, but this did not work for
3350    me.
3351
3352    :param parent: the panel which will be the parent of the button
3353    :param str msg: the help text to be displayed
3354    '''
3355    def __init__(self,parent,msg):
3356        if sys.platform == "darwin": 
3357            wx.Button.__init__(self,parent,wx.ID_HELP)
3358        else:
3359            wx.Button.__init__(self,parent,wx.ID_ANY,'?',style=wx.BU_EXACTFIT)
3360        self.Bind(wx.EVT_BUTTON,self._onPress)
3361        self.msg=StripIndents(msg)
3362        self.parent = parent
3363    def _onClose(self,event):
3364        self.dlg.EndModal(wx.ID_CANCEL)
3365    def _onPress(self,event):
3366        'Respond to a button press by displaying the requested text'
3367        #dlg = wx.MessageDialog(self.parent,self.msg,'Help info',wx.OK)
3368        self.dlg = wx.Dialog(self.parent,wx.ID_ANY,'Help information', 
3369                        style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)
3370        #self.dlg.SetBackgroundColour(wx.WHITE)
3371        mainSizer = wx.BoxSizer(wx.VERTICAL)
3372        txt = wx.StaticText(self.dlg,wx.ID_ANY,self.msg)
3373        mainSizer.Add(txt,1,wx.ALL|wx.EXPAND,10)
3374        txt.SetBackgroundColour(wx.WHITE)
3375
3376        btnsizer = wx.BoxSizer(wx.HORIZONTAL)
3377        btn = wx.Button(self.dlg, wx.ID_CLOSE) 
3378        btn.Bind(wx.EVT_BUTTON,self._onClose)
3379        btnsizer.Add(btn)
3380        mainSizer.Add(btnsizer, 0, wx.ALIGN_CENTER|wx.ALL, 5)
3381        self.dlg.SetSizer(mainSizer)
3382        mainSizer.Fit(self.dlg)
3383        self.dlg.CenterOnParent()
3384        self.dlg.ShowModal()
3385        self.dlg.Destroy()
3386################################################################################
3387class MyHtmlPanel(wx.Panel):
3388    '''Defines a panel to display HTML help information, as an alternative to
3389    displaying help information in a web browser.
3390    '''
3391    def __init__(self, frame, id):
3392        self.frame = frame
3393        wx.Panel.__init__(self, frame, id)
3394        sizer = wx.BoxSizer(wx.VERTICAL)
3395        back = wx.Button(self, -1, "Back")
3396        back.Bind(wx.EVT_BUTTON, self.OnBack)
3397        self.htmlwin = G2HtmlWindow(self, id, size=(750,450))
3398        sizer.Add(self.htmlwin, 1,wx.EXPAND)
3399        sizer.Add(back, 0, wx.ALIGN_LEFT, 0)
3400        self.SetSizer(sizer)
3401        sizer.Fit(frame)       
3402        self.Bind(wx.EVT_SIZE,self.OnHelpSize)
3403    def OnHelpSize(self,event):         #does the job but weirdly!!
3404        anchor = self.htmlwin.GetOpenedAnchor()
3405        if anchor:           
3406            self.htmlwin.ScrollToAnchor(anchor)
3407            wx.CallAfter(self.htmlwin.ScrollToAnchor,anchor)
3408            if event: event.Skip()
3409    def OnBack(self, event):
3410        self.htmlwin.HistoryBack()
3411    def LoadFile(self,file):
3412        pos = file.rfind('#')
3413        if pos != -1:
3414            helpfile = file[:pos]
3415            helpanchor = file[pos+1:]
3416        else:
3417            helpfile = file
3418            helpanchor = None
3419        self.htmlwin.LoadPage(helpfile)
3420        if helpanchor is not None:
3421            self.htmlwin.ScrollToAnchor(helpanchor)
3422            xs,ys = self.htmlwin.GetViewStart()
3423            self.htmlwin.Scroll(xs,ys-1)
3424################################################################################
3425class G2HtmlWindow(wx.html.HtmlWindow):
3426    '''Displays help information in a primitive HTML browser type window
3427    '''
3428    def __init__(self, parent, *args, **kwargs):
3429        self.parent = parent
3430        wx.html.HtmlWindow.__init__(self, parent, *args, **kwargs)
3431    def LoadPage(self, *args, **kwargs):
3432        wx.html.HtmlWindow.LoadPage(self, *args, **kwargs)
3433        self.TitlePage()
3434    def OnLinkClicked(self, *args, **kwargs):
3435        wx.html.HtmlWindow.OnLinkClicked(self, *args, **kwargs)
3436        xs,ys = self.GetViewStart()
3437        self.Scroll(xs,ys-1)
3438        self.TitlePage()
3439    def HistoryBack(self, *args, **kwargs):
3440        wx.html.HtmlWindow.HistoryBack(self, *args, **kwargs)
3441        self.TitlePage()
3442    def TitlePage(self):
3443        self.parent.frame.SetTitle(self.GetOpenedPage() + ' -- ' + 
3444            self.GetOpenedPageTitle())
3445
3446################################################################################
3447def StripIndents(msg):
3448    'Strip indentation from multiline strings'
3449    msg1 = msg.replace('\n ','\n')
3450    while msg != msg1:
3451        msg = msg1
3452        msg1 = msg.replace('\n ','\n')
3453    return msg.replace('\n\t','\n')
3454
3455def StripUnicode(string,subs='.'):
3456    '''Strip non-ASCII characters from strings
3457   
3458    :param str string: string to strip Unicode characters from
3459    :param str subs: character(s) to place into string in place of each
3460      Unicode character. Defaults to '.'
3461
3462    :returns: a new string with only ASCII characters
3463    '''
3464    s = ''
3465    for c in string:
3466        if ord(c) < 128:
3467            s += c
3468        else:
3469            s += subs
3470    return s.encode('ascii','replace')
3471       
3472################################################################################
3473# configuration routines (for editing config.py)
3474def SaveGPXdirectory(path):
3475    if GSASIIpath.GetConfigValue('Starting_directory') == path: return
3476    vars = GetConfigValsDocs()
3477    try:
3478        vars['Starting_directory'][1] = path
3479        if GSASIIpath.GetConfigValue('debug'): print('Saving GPX path: '+path)
3480        SaveConfigVars(vars)
3481    except KeyError:
3482        pass
3483
3484def SaveImportDirectory(path):
3485    if GSASIIpath.GetConfigValue('Import_directory') == path: return
3486    vars = GetConfigValsDocs()
3487    try:
3488        vars['Import_directory'][1] = path
3489        if GSASIIpath.GetConfigValue('debug'): print('Saving Import path: '+path)
3490        SaveConfigVars(vars)
3491    except KeyError:
3492        pass
3493
3494def GetConfigValsDocs():
3495    '''Reads the module referenced in fname (often <module>.__file__) and
3496    return a dict with names of global variables as keys.
3497    For each global variable, the value contains four items:
3498
3499    :returns: a dict where keys are names defined in module config_example.py
3500      where the value is a list of four items, as follows:
3501
3502         * item 0: the default value
3503         * item 1: the current value
3504         * item 2: the initial value (starts same as item 1)
3505         * item 3: the "docstring" that follows variable definition
3506
3507    '''
3508    import config_example
3509    import ast
3510    fname = os.path.splitext(config_example.__file__)[0]+'.py' # convert .pyc to .py
3511    with open(fname, 'r') as f:
3512        fstr = f.read()
3513    fstr = fstr.replace('\r\n', '\n').replace('\r', '\n')
3514    if not fstr.endswith('\n'):
3515        fstr += '\n'
3516    tree = ast.parse(fstr)
3517    d = {}
3518    key = None
3519    for node in ast.walk(tree):
3520        if isinstance(node,ast.Assign):
3521            key = node.targets[0].id
3522            d[key] = [config_example.__dict__.get(key),
3523                      GSASIIpath.configDict.get(key),
3524                      GSASIIpath.configDict.get(key),'']
3525        elif isinstance(node,ast.Expr) and key:
3526            d[key][3] = node.value.s.strip()
3527        else:
3528            key = None
3529    return d
3530
3531def SaveConfigVars(vars,parent=None):
3532    '''Write the current config variable values to config.py
3533
3534    :params dict vars: a dictionary of variable settings and meanings as
3535      created in :func:`GetConfigValsDocs`.
3536    :param parent: wx.Frame object or None (default) for parent
3537      of error message if no file can be written.
3538    :returns: True if unable to write the file, None otherwise
3539    '''
3540    # try to write to where an old config file is located
3541    try:
3542        import config
3543        savefile = config.__file__
3544    except ImportError: # no config.py file yet
3545        savefile = os.path.join(GSASIIpath.path2GSAS2,'config.py')
3546    # try to open file for write
3547    try:
3548        savefile = os.path.splitext(savefile)[0]+'.py' # convert .pyc to .py
3549        fp = open(savefile,'w')
3550    except IOError:  # can't write there, write in local mods directory
3551        # create a local mods directory, if needed
3552        if not os.path.exists(os.path.expanduser('~/.G2local/')):
3553            print('Creating directory '+os.path.expanduser('~/.G2local/'))
3554            os.mkdir(os.path.expanduser('~/.G2local/'))
3555            sys.path.insert(0,os.path.expanduser('~/.G2local/'))
3556        savefile = os.path.join(os.path.expanduser('~/.G2local/'),'config.py')
3557        try:
3558            fp = open(savefile,'w')
3559        except IOError:
3560            if parent:
3561                G2MessageBox(parent,'Error trying to write configuration to '+savefile,
3562                    'Unable to save')
3563            else:
3564                print('Error trying to write configuration to '+savefile)
3565            return True
3566    import datetime
3567    fp.write("'''\n")
3568    fp.write("*config.py: Configuration options*\n----------------------------------\n")
3569    fp.write("This file created in SelectConfigSetting on {:%d %b %Y %H:%M}\n".
3570             format(datetime.datetime.now()))
3571    fp.write("'''\n\n")
3572    fp.write("import os.path\n")
3573    fp.write("import GSASIIpath\n\n")
3574    for var in sorted(vars.keys(),key=lambda s: s.lower()):
3575        if vars[var][1] is None: continue
3576        if vars[var][1] == '': continue
3577        if vars[var][0] == vars[var][1]: continue
3578        try:
3579            float(vars[var][1]) # test for number
3580            fp.write(var + ' = ' + str(vars[var][1])+'\n')
3581        except:
3582            try:
3583                eval(vars[var][1]) # test for an expression
3584                fp.write(var + ' = ' + str(vars[var][1])+'\n')
3585            except: # must be a string
3586                varstr = vars[var][1]
3587                if '\\' in varstr:
3588                    fp.write(var + ' = os.path.normpath("' + varstr.replace('\\','/') +'")\n')
3589                else:
3590                    fp.write(var + ' = "' + str(varstr)+'"\n')
3591        if vars[var][3]:
3592            fp.write("'''" + str(vars[var][3]) + "\n'''\n\n")
3593    fp.close()
3594    print('wrote file '+savefile)
3595
3596class SelectConfigSetting(wx.Dialog):
3597    '''Dialog to select configuration variables and set associated values.
3598    '''
3599    def __init__(self,parent=None):
3600        style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER
3601        wx.Dialog.__init__(self, parent, wx.ID_ANY, 'Set Config Variable', style=style)
3602        self.sizer = wx.BoxSizer(wx.VERTICAL)
3603        self.vars = GetConfigValsDocs()
3604       
3605        label = wx.StaticText(
3606            self,  wx.ID_ANY,
3607            'Select a GSAS-II configuration variable to change'
3608            )
3609        self.sizer.Add(label, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
3610        self.choice = {}
3611        btn = G2ChoiceButton(self, sorted(self.vars.keys(),key=lambda s: s.lower()),
3612                                 strLoc=self.choice,strKey=0,
3613                                 onChoice=self.OnSelection)
3614        btn.SetLabel("")
3615        self.sizer.Add(btn)
3616
3617        self.varsizer = wx.BoxSizer(wx.VERTICAL)
3618        self.sizer.Add(self.varsizer,1,wx.ALL|wx.EXPAND,1)
3619       
3620        self.doclbl = wx.StaticBox(self, wx.ID_ANY, "")
3621        self.doclblsizr = wx.StaticBoxSizer(self.doclbl)
3622        self.docinfo = wx.StaticText(self,  wx.ID_ANY, "")
3623        self.doclblsizr.Add(self.docinfo, 0, wx.ALIGN_LEFT|wx.ALL, 5)
3624        self.sizer.Add(self.doclblsizr, 0, wx.ALIGN_LEFT|wx.EXPAND|wx.ALL, 5)
3625        btnsizer = wx.BoxSizer(wx.HORIZONTAL)
3626        self.saveBtn = wx.Button(self,-1,"Save current settings")
3627        btnsizer.Add(self.saveBtn, 0, wx.ALL, 2) 
3628        self.saveBtn.Bind(wx.EVT_BUTTON, self.OnSave)
3629        self.saveBtn.Enable(False)
3630        self.applyBtn = wx.Button(self,-1,"Use current (no save)")
3631        btnsizer.Add(self.applyBtn, 0, wx.ALL, 2) 
3632        self.applyBtn.Bind(wx.EVT_BUTTON, self.OnApplyChanges)
3633        self.applyBtn.Enable(False)
3634       
3635        btn = wx.Button(self,wx.ID_CANCEL)
3636        btnsizer.Add(btn, 0, wx.ALL, 2) 
3637        self.sizer.Add(btnsizer, 0, wx.ALIGN_CENTRE|wx.ALL, 5) 
3638               
3639        self.SetSizer(self.sizer)
3640        self.sizer.Fit(self)
3641        self.CenterOnParent()
3642
3643    def OnChange(self,event=None):
3644        ''' Check if anything been changed. Turn the save button on/off.
3645        '''
3646        for var in self.vars:
3647            if self.vars[var][0] is None and self.vars[var][1] is not None:
3648                # make blank strings into None, if that is the default
3649                if self.vars[var][1].strip() == '': self.vars[var][1] = None
3650            if self.vars[var][1] != self.vars[var][2]:
3651                #print 'changed',var,self.vars[var][:3]
3652                self.saveBtn.Enable(True)
3653                self.applyBtn.Enable(True)
3654                break
3655        else:
3656            self.saveBtn.Enable(False)
3657            self.applyBtn.Enable(False)
3658        try:
3659            self.resetBtn.Enable(True)
3660        except:
3661            pass
3662       
3663    def OnApplyChanges(self,event=None):
3664        'Set config variables to match the current settings'
3665        GSASIIpath.SetConfigValue(self.vars)
3666        self.EndModal(wx.ID_OK)
3667       
3668    def OnSave(self,event):
3669        '''Write the config variables to config.py and then set them
3670        as the current settings
3671        '''
3672        if not SaveConfigVars(self.vars,parent=self):
3673            self.OnApplyChanges() # force a reload of the config settings
3674            self.EndModal(wx.ID_OK)
3675
3676    def OnBoolSelect(self,event):
3677        'Respond to a change in a True/False variable'
3678        rb = event.GetEventObject()
3679        var = self.choice[0]
3680        self.vars[var][1] = (rb.GetSelection() == 0)
3681        self.OnChange()
3682        wx.CallAfter(self.OnSelection)
3683       
3684    def onSelDir(self,event):
3685        'Select a directory from a menu'
3686        dlg = wx.DirDialog(self, "Choose a directory:",style=wx.DD_DEFAULT_STYLE)
3687        if dlg.ShowModal() == wx.ID_OK:
3688            var = self.choice[0]
3689            self.vars[var][1] = dlg.GetPath()
3690            self.strEd.SetValue(self.vars[var][1])
3691            self.OnChange()
3692        dlg.Destroy()
3693       
3694    def OnSelection(self):
3695        'show a selected variable'
3696        self.varsizer.DeleteWindows()
3697        var = self.choice[0]
3698        showdef = True
3699        if var not in self.vars:
3700            raise Exception,"How did this happen?"
3701        if type(self.vars[var][0]) is int:
3702            ed = ValidatedTxtCtrl(self,self.vars[var],1,typeHint=int,OKcontrol=self.OnChange)
3703            self.varsizer.Add(ed, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
3704        elif type(self.vars[var][0]) is float:
3705            ed = ValidatedTxtCtrl(self,self.vars[var],1,typeHint=float,OKcontrol=self.OnChange)
3706            self.varsizer.Add(ed, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
3707        elif type(self.vars[var][0]) is bool:
3708            showdef = False
3709            lbl = "value for "+var
3710            ch = []
3711            for i,v in enumerate((True,False)):
3712                s = str(v)
3713                if v == self.vars[var][0]:
3714                    defopt = i
3715                    s += ' (default)'
3716                ch += [s]
3717            rb = wx.RadioBox(
3718                    self, wx.ID_ANY, lbl, wx.DefaultPosition, wx.DefaultSize,
3719                    ch, 1, wx.RA_SPECIFY_COLS
3720            )
3721            # set initial value
3722            if self.vars[var][1] is None:
3723                rb.SetSelection(defopt)
3724            elif self.vars[var][1]:
3725                rb.SetSelection(0)
3726            else:
3727                rb.SetSelection(1)
3728            rb.Bind(wx.EVT_RADIOBOX,self.OnBoolSelect)
3729            self.varsizer.Add(rb, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
3730        else:
3731            if var.endswith('_directory') or var.endswith('_location'):
3732                btn = wx.Button(self,wx.ID_ANY,'Select from dialog...')
3733                sz = (400,-1)
3734            else:
3735                btn = None
3736                sz = (250,-1)
3737            self.strEd = ValidatedTxtCtrl(self,self.vars[var],1,typeHint=str,OKcontrol=self.OnChange,
3738                                              size=sz)
3739            if self.vars[var][1] is not None:
3740                self.strEd.SetValue(self.vars[var][1])
3741            self.varsizer.Add(self.strEd, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
3742            if btn:
3743                btn.Bind(wx.EVT_BUTTON,self.onSelDir)
3744                self.varsizer.Add(btn, 0, wx.ALIGN_CENTRE|wx.ALL, 5) 
3745        # button for reset to default value
3746        lbl = "Reset to Default"
3747        if showdef: # spell out default when needed
3748            lbl += ' (='+str(self.vars[var][0])+')'
3749            #label = wx.StaticText(self,  wx.ID_ANY, 'Default value = '+str(self.vars[var][0]))
3750            #self.varsizer.Add(label, 0, wx.ALIGN_LEFT|wx.ALL, 5)
3751        self.resetBtn = wx.Button(self,-1,lbl)
3752        self.resetBtn.Bind(wx.EVT_BUTTON, self.OnClear)
3753        if self.vars[var][1] is not None and self.vars[var][1] != '': # show current value, if one
3754            #label = wx.StaticText(self,  wx.ID_ANY, 'Current value = '+str(self.vars[var][1]))
3755            #self.varsizer.Add(label, 0, wx.ALIGN_LEFT|wx.ALL, 5)
3756            self.resetBtn.Enable(True)
3757        else:
3758            self.resetBtn.Enable(False)
3759        self.varsizer.Add(self.resetBtn, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
3760        # show meaning, if defined
3761        self.doclbl.SetLabel("Description of "+str(var)) 
3762        if self.vars[var][3]:
3763            self.docinfo.SetLabel(self.vars[var][3])
3764        else:
3765            self.docinfo.SetLabel("(not documented)")
3766        self.sizer.Fit(self)
3767        self.CenterOnParent()
3768        wx.CallAfter(self.SendSizeEvent)
3769
3770    def OnClear(self, event):
3771        var = self.choice[0]
3772        self.vars[var][1] = self.vars[var][0]
3773        self.OnChange()
3774        wx.CallAfter(self.OnSelection)
3775       
3776################################################################################
3777class downdate(wx.Dialog):
3778    '''Dialog to allow a user to select a version of GSAS-II to install
3779    '''
3780    def __init__(self,parent=None):
3781        style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER
3782        wx.Dialog.__init__(self, parent, wx.ID_ANY, 'Select Version', style=style)
3783        pnl = wx.Panel(self)
3784        sizer = wx.BoxSizer(wx.VERTICAL)
3785        insver = GSASIIpath.svnGetRev(local=True)
3786        curver = int(GSASIIpath.svnGetRev(local=False))
3787        label = wx.StaticText(
3788            pnl,  wx.ID_ANY,
3789            'Select a specific GSAS-II version to install'
3790            )
3791        sizer.Add(label, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
3792        sizer1 = wx.BoxSizer(wx.HORIZONTAL)
3793        sizer1.Add(
3794            wx.StaticText(pnl,  wx.ID_ANY,
3795                          'Currently installed version: '+str(insver)),
3796            0, wx.ALIGN_CENTRE|wx.ALL, 5)
3797        sizer.Add(sizer1)
3798        sizer1 = wx.BoxSizer(wx.HORIZONTAL)
3799        sizer1.Add(
3800            wx.StaticText(pnl,  wx.ID_ANY,
3801                          'Select GSAS-II version to install: '),
3802            0, wx.ALIGN_CENTRE|wx.ALL, 5)
3803        self.spin = wx.SpinCtrl(pnl, wx.ID_ANY,size=(150,-1))
3804        self.spin.SetRange(1, curver)
3805        self.spin.SetValue(curver)
3806        self.Bind(wx.EVT_SPINCTRL, self._onSpin, self.spin)
3807        self.Bind(wx.EVT_KILL_FOCUS, self._onSpin, self.spin)
3808        sizer1.Add(self.spin)
3809        sizer.Add(sizer1)
3810
3811        line = wx.StaticLine(pnl,-1, size=(-1,3), style=wx.LI_HORIZONTAL)
3812        sizer.Add(line, 0, wx.EXPAND|wx.ALIGN_CENTER|wx.ALL, 10)
3813
3814        self.text = wx.StaticText(pnl,  wx.ID_ANY, "")
3815        sizer.Add(self.text, 0, wx.ALIGN_LEFT|wx.EXPAND|wx.ALL, 5)
3816
3817        line = wx.StaticLine(pnl,-1, size=(-1,3), style=wx.LI_HORIZONTAL)
3818        sizer.Add(line, 0, wx.EXPAND|wx.ALIGN_CENTER|wx.ALL, 10)
3819        sizer.Add(
3820            wx.StaticText(
3821                pnl,  wx.ID_ANY,
3822                'If "Install" is pressed, your project will be saved;\n'
3823                'GSAS-II will exit; The specified version will be loaded\n'
3824                'and GSAS-II will restart. Press "Cancel" to abort.'),
3825            0, wx.EXPAND|wx.ALL, 10)
3826        btnsizer = wx.StdDialogButtonSizer()
3827        btn = wx.Button(pnl, wx.ID_OK, "Install")
3828        btn.SetDefault()
3829        btnsizer.AddButton(btn)
3830        btn = wx.Button(pnl, wx.ID_CANCEL)
3831        btnsizer.AddButton(btn)
3832        btnsizer.Realize()
3833        sizer.Add(btnsizer, 0, wx.ALIGN_CENTER|wx.ALL, 5)
3834        pnl.SetSizer(sizer)
3835        sizer.Fit(self)
3836        self.topsizer=sizer
3837        self.CenterOnParent()
3838        self._onSpin(None)
3839
3840    def _onSpin(self,event):
3841        'Called to load info about the selected version in the dialog'
3842        if event: event.Skip()
3843        ver = self.spin.GetValue()
3844        d = GSASIIpath.svnGetLog(version=ver)
3845        date = d.get('date','?').split('T')[0]
3846        s = '(Version '+str(ver)+' created '+date
3847        s += ' by '+d.get('author','?')+')'
3848        msg = d.get('msg')
3849        if msg: s += '\n\nComment: '+msg
3850        self.text.SetLabel(s)
3851        self.topsizer.Fit(self)
3852
3853    def getVersion(self):
3854        'Get the version number in the dialog'
3855        return self.spin.GetValue()
3856
3857################################################################################
3858#### Display Help information
3859################################################################################
3860# define some globals
3861htmlPanel = None
3862htmlFrame = None
3863htmlFirstUse = True
3864#helpLocDict = {}  # to be implemented if we ever split gsasii.html over multiple files
3865path2GSAS2 = os.path.dirname(os.path.realpath(__file__)) # save location of this file
3866def ShowHelp(helpType,frame):
3867    '''Called to bring up a web page for documentation.'''
3868    global htmlFirstUse,htmlPanel,htmlFrame
3869    # no defined link to use, create a default based on key
3870    helplink = 'gsasII.html'
3871    if helpType:
3872        helplink += '#'+helpType.replace(')','').replace('(','_').replace(' ','_')
3873    # determine if a web browser or the internal viewer should be used for help info
3874    if GSASIIpath.GetConfigValue('Help_mode'):
3875        helpMode = GSASIIpath.GetConfigValue('Help_mode')
3876    else:
3877        helpMode = 'browser'
3878    if helpMode == 'internal':
3879        helplink = os.path.join(path2GSAS2,'help',helplink)
3880        try:
3881            htmlPanel.LoadFile(helplink)
3882            htmlFrame.Raise()
3883        except:
3884            htmlFrame = wx.Frame(frame, -1, size=(610, 510))
3885            htmlFrame.Show(True)
3886            htmlFrame.SetTitle("HTML Window") # N.B. reset later in LoadFile
3887            htmlPanel = MyHtmlPanel(htmlFrame,-1)
3888            htmlPanel.LoadFile(helplink)
3889    else:
3890        if sys.platform == "darwin": # for Mac, force use of safari to preserve anchors on file URLs
3891            wb = webbrowser.MacOSXOSAScript('safari')
3892        else:
3893            wb = webbrowser
3894        helplink = os.path.join(path2GSAS2,'help',helplink)
3895        pfx = "file://"
3896        if sys.platform.lower().startswith('win'):
3897            pfx = ''
3898        #if GSASIIpath.GetConfigValue('debug'): print 'Help link=',pfx+helplink
3899        if htmlFirstUse:
3900            wb.open_new(pfx+helplink)
3901            htmlFirstUse = False
3902        else:
3903            wb.open(pfx+helplink, new=0, autoraise=True)
3904
3905def ShowWebPage(URL,frame):
3906    '''Called to show a tutorial web page.
3907    '''
3908    global htmlFirstUse,htmlPanel,htmlFrame
3909    # determine if a web browser or the internal viewer should be used for help info
3910    if GSASIIpath.GetConfigValue('Help_mode'):
3911        helpMode = GSASIIpath.GetConfigValue('Help_mode')
3912    else:
3913        helpMode = 'browser'
3914    if helpMode == 'internal':
3915        try:
3916            htmlPanel.LoadFile(URL)
3917            htmlFrame.Raise()
3918        except:
3919            htmlFrame = wx.Frame(frame, -1, size=(610, 510))
3920            htmlFrame.Show(True)
3921            htmlFrame.SetTitle("HTML Window") # N.B. reset later in LoadFile
3922            htmlPanel = MyHtmlPanel(htmlFrame,-1)
3923            htmlPanel.LoadFile(URL)
3924    else:
3925        if URL.startswith('http'): 
3926            pfx = ''
3927        elif sys.platform.lower().startswith('win'):
3928            pfx = ''
3929        else:
3930            pfx = "file://"
3931        if htmlFirstUse:
3932            webbrowser.open_new(pfx+URL)
3933            htmlFirstUse = False
3934        else:
3935            webbrowser.open(pfx+URL, new=0, autoraise=True)
3936
3937################################################################################
3938#### Tutorials support
3939################################################################################
3940G2BaseURL = "https://subversion.xray.aps.anl.gov/pyGSAS"
3941# N.B. tutorialCatalog is generated by routine catalog.py, which also generates the appropriate
3942# empty directories (.../MT/* .../trunk/GSASII/* *=[help,Exercises])
3943tutorialCatalog = (
3944    # tutorial dir,      web page file name,      title for page
3945
3946    ['StartingGSASII', 'Starting GSAS.htm', 'Starting GSAS-II'],
3947       
3948    ['LabData', 'Laboratory X.htm', 'Fitting laboratory X-ray powder data for fluoroapatite'],
3949    ['CWNeutron', 'Neutron CW Powder Data.htm', 'CW Neutron Powder fit for Yttrium-Iron Garnet'],
3950
3951    ['FitPeaks', 'Fit Peaks.htm', 'Fitting individual peaks & autoindexing'],
3952    ['CFjadarite', 'Charge Flipping in GSAS.htm', '     Charge Flipping structure solution for jadarite'],
3953    ['CFsucrose', 'Charge Flipping - sucrose.htm','     Charge Flipping structure solution for sucrose'],
3954    ['BkgFit', 'FitBkgTut.htm',  'Fitting the Starting Background using Fixed Points'],
3955       
3956    ['CWCombined', 'Combined refinement.htm', 'Combined X-ray/CW-neutron refinement of PbSO4'],
3957    ['TOF-CW Joint Refinement', 'TOF combined XN Rietveld refinement in GSAS.htm', 'Combined X-ray/TOF-neutron Rietveld refinement'],
3958    ['SeqRefine', 'SequentialTutorial.htm', 'Sequential refinement of multiple datasets'],
3959    ['SeqParametric', 'ParametricFitting.htm', '     Parametric Fitting and Pseudo Variables for Sequential Fits'],
3960       
3961    ['StackingFaults-I', 'Stacking Faults-I.htm', 'Stacking fault simulations for diamond'],
3962    ['StackingFaults-II', 'Stacking Faults II.htm', 'Stacking fault simulations for Keokuk kaolinite'],
3963    ['StackingFaults-III', 'Stacking Faults-III.htm', 'Stacking fault simulations for Georgia kaolinite'],
3964       
3965    ['CFXraySingleCrystal', 'CFSingleCrystal.htm', 'Charge Flipping structure solution with Xray single crystal data'],       
3966    ['TOF Charge Flipping', 'Charge Flipping with TOF single crystal data in GSASII.htm', 'Charge flipping with neutron TOF single crystal data'],
3967    ['MCsimanneal', 'MCSA in GSAS.htm', 'Monte-Carlo simulated annealing structure determination'],
3968       
3969    ['MerohedralTwins', 'Merohedral twin refinement in GSAS.htm', 'Merohedral twin refinements'],
3970
3971    ['2DCalibration', 'Calibration of an area detector in GSAS.htm', 'Calibration of an area detector'],
3972    ['2DIntegration', 'Integration of area detector data in GSAS.htm', '     Integration of area detector data'],
3973    ['TOF Calibration', 'Calibration of a TOF powder diffractometer.htm', 'Calibration of a Neutron TOF diffractometer'],
3974    ['TOF Single Crystal Refinement', 'TOF single crystal refinement in GSAS.htm', 'Single crystal refinement from TOF data'],
3975       
3976    ['2DStrain', 'Strain fitting of 2D data in GSAS-II.htm', 'Strain fitting of 2D data'],
3977    ['2DTexture', 'Texture analysis of 2D data in GSAS-II.htm', 'Texture analysis of 2D data'],
3978             
3979    ['SAsize', 'Small Angle Size Distribution.htm', 'Small angle x-ray data size distribution (alumina powder)'],
3980    ['SAfit', 'Fitting Small Angle Scattering Data.htm', '     Fitting small angle x-ray data (alumina powder)'],
3981    ['SAimages', 'Small Angle Image Processing.htm', 'Image Processing of small angle x-ray data'],
3982    ['SAseqref', 'Sequential Refinement of Small Angle Scattering Data.htm', 'Sequential refinement with small angle scattering data'],
3983   
3984    #['ExampleDir', 'ExamplePage.html', 'Example Tutorial Title'],
3985    )
3986
3987class OpenTutorial(wx.Dialog):
3988    '''Open a tutorial, optionally copying it to the local disk. Always copy
3989    the data files locally.
3990
3991    For now tutorials will always be copied into the source code tree, but it
3992    might be better to have an option to copy them somewhere else, for people
3993    who don't have write access to the GSAS-II source code location.
3994    '''
3995   
3996    def __init__(self,parent=None):
3997        style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER
3998        wx.Dialog.__init__(self, parent, wx.ID_ANY, 'Open Tutorial', style=style)
3999        self.frame = parent
4000        pnl = wx.Panel(self)
4001        sizer = wx.BoxSizer(wx.VERTICAL)
4002        sizer1 = wx.BoxSizer(wx.HORIZONTAL)       
4003        label = wx.StaticText(
4004            pnl,  wx.ID_ANY,
4005            'Select the tutorial to be run and the mode of access'
4006            )
4007        msg = '''GSAS-II tutorials and their sample data files
4008        require a fair amount of storage space; few users will
4009        use all of them. This dialog allows users to load selected
4010        tutorials (along with their sample data) to their computer;
4011        optionally all tutorials can be downloaded.
4012
4013        Downloaded tutorials can be viewed and run without internet
4014        access. Tutorials can also be viewed without download, but
4015        users will need to download the sample data files manually.
4016
4017        The location used to download tutorials is set using the
4018        "Set download location" which is saved as the "Tutorial_location"
4019        configuration option see File/Preference or the
4020        config_example.py file. System managers can select to have
4021        tutorial files installed at a shared location.
4022        '''
4023        self.SetTutorialPath()
4024        hlp = HelpButton(pnl,msg)
4025        sizer1.Add((-1,-1),1, wx.EXPAND, 0)
4026        sizer1.Add(label, 0, wx.ALIGN_CENTRE|wx.ALL, 0)
4027        sizer1.Add((-1,-1),1, wx.EXPAND, 0)
4028        sizer1.Add(hlp,0,wx.ALIGN_RIGHT|wx.ALL)
4029        sizer.Add(sizer1,0,wx.EXPAND|wx.ALL,0)
4030        sizer.Add((10,10))
4031        sizer0 = wx.BoxSizer(wx.HORIZONTAL)       
4032        sizer1a = wx.BoxSizer(wx.VERTICAL)
4033        sizer1b = wx.BoxSizer(wx.VERTICAL)
4034        btn = wx.Button(pnl, wx.ID_ANY, "Download a tutorial and view")
4035        btn.Bind(wx.EVT_BUTTON, self.SelectAndDownload)
4036        sizer1a.Add(btn,0,WACV)
4037        btn = wx.Button(pnl, wx.ID_ANY, "Select from downloaded tutorials")
4038        btn.Bind(wx.EVT_BUTTON, self.onSelectDownloaded)
4039        sizer1a.Add(btn,0,WACV)
4040        btn = wx.Button(pnl, wx.ID_ANY, "Browse tutorial on web")
4041        btn.Bind(wx.EVT_BUTTON, self.onWebBrowse)
4042        sizer1a.Add(btn,0,WACV)
4043        btn = wx.Button(pnl, wx.ID_ANY, "Update downloaded tutorials")
4044        btn.Bind(wx.EVT_BUTTON, self.UpdateDownloaded)
4045        sizer1b.Add(btn,0,WACV)
4046        btn = wx.Button(pnl, wx.ID_ANY, "Download all tutorials")
4047        btn.Bind(wx.EVT_BUTTON, self.DownloadAll)
4048        sizer1b.Add(btn,0,WACV)
4049        sizer0.Add(sizer1a,0,wx.EXPAND|wx.ALL,0)
4050        sizer0.Add(sizer1b,0,wx.EXPAND|wx.ALL,0)
4051        sizer.Add(sizer0,5,wx.EXPAND|wx.ALL,5)
4052       
4053        sizer.Add((10,10))
4054        sizer1 = wx.BoxSizer(wx.HORIZONTAL)
4055        btn = wx.Button(pnl, wx.ID_ANY, "Set download location")
4056        btn.Bind(wx.EVT_BUTTON, self.SelectDownloadLoc)
4057        sizer1.Add(btn,0,WACV)
4058        self.dataLoc = wx.StaticText(pnl, wx.ID_ANY,self.tutorialPath)
4059        sizer1.Add(self.dataLoc,0,WACV)
4060        sizer.Add(sizer1)
4061       
4062        btnsizer = wx.StdDialogButtonSizer()
4063        btn = wx.Button(pnl, wx.ID_CANCEL,"Done")
4064        btnsizer.AddButton(btn)
4065        btnsizer.Realize()
4066        sizer.Add(btnsizer, 0, wx.ALIGN_CENTER|wx.ALL, 5)
4067        pnl.SetSizer(sizer)
4068        sizer.Fit(self)
4069        self.topsizer=sizer
4070        self.CenterOnParent()
4071
4072    def SetTutorialPath(self):
4073        '''Get the tutorial location if set; if not pick a default
4074        directory in a logical place
4075        '''
4076        if GSASIIpath.GetConfigValue('Tutorial_location'):
4077            self.tutorialPath = os.path.abspath(GSASIIpath.GetConfigValue('Tutorial_location'))
4078        elif (sys.platform.lower().startswith('win') and
4079              os.path.exists(os.path.abspath(os.path.expanduser('~/My Documents')))):
4080            self.tutorialPath = os.path.abspath(os.path.expanduser('~/My Documents/G2tutorials'))
4081        elif (sys.platform.lower().startswith('win') and
4082              os.path.exists(os.path.abspath(os.path.expanduser('~/Documents')))):
4083            self.tutorialPath = os.path.abspath(os.path.expanduser('~/Documents/G2tutorials'))
4084        else:
4085            self.tutorialPath = os.path.abspath(os.path.expanduser('~/G2tutorials'))
4086
4087    def SelectAndDownload(self,event):
4088        '''Make a list of all tutorials on web and allow user to choose one to
4089        download and then view
4090        '''
4091        indices = [j for j,i in enumerate(tutorialCatalog)
4092            if not os.path.exists(os.path.join(self.tutorialPath,i[0],i[1]))]
4093        if not indices:
4094            G2MessageBox(self,'All tutorials are downloaded','None to download')
4095            return
4096        choices = [tutorialCatalog[i][2] for i in indices]
4097        selected = self.ChooseTutorial(choices)
4098        if selected is None: return
4099        j = indices[selected]
4100        fullpath = os.path.join(self.tutorialPath,tutorialCatalog[j][0],tutorialCatalog[j][1])
4101        fulldir = os.path.join(self.tutorialPath,tutorialCatalog[j][0])
4102        URL = G2BaseURL+'/Tutorials/'+tutorialCatalog[j][0]+'/'
4103        if GSASIIpath.svnInstallDir(URL,fulldir):
4104            ShowWebPage(fullpath,self.frame)
4105        else:
4106            G2MessageBox(self,'Error downloading tutorial','Download error')
4107        self.EndModal(wx.ID_OK)
4108
4109    def onSelectDownloaded(self,event):
4110        '''Select a previously downloaded tutorial
4111        '''
4112        indices = [j for j,i in enumerate(tutorialCatalog)
4113            if os.path.exists(os.path.join(self.tutorialPath,i[0],i[1]))]
4114        if not indices:
4115            G2MessageBox(self,
4116                         'There are no downloaded tutorials in '+self.tutorialPath,
4117                         'None downloaded')
4118            return
4119        choices = [tutorialCatalog[i][2] for i in indices]
4120        selected = self.ChooseTutorial(choices)
4121        if selected is None: return
4122        j = indices[selected]
4123        fullpath = os.path.join(self.tutorialPath,tutorialCatalog[j][0],tutorialCatalog[j][1])
4124        self.EndModal(wx.ID_OK)
4125        ShowWebPage(fullpath,self.frame)
4126
4127    def onWebBrowse(self,event):
4128        '''Make a list of all tutorials on web and allow user to view one.
4129        '''
4130        choices = [i[2] for i in tutorialCatalog]
4131        selected = self.ChooseTutorial(choices)
4132        if selected is None: return       
4133        tutdir = tutorialCatalog[selected][0]
4134        tutfil = tutorialCatalog[selected][1]
4135        # open web page remotely, don't worry about data
4136        URL = G2BaseURL+'/Tutorials/'+tutdir+'/'+tutfil
4137        self.EndModal(wx.ID_OK)
4138        ShowWebPage(URL,self.frame)
4139       
4140    def ChooseTutorial(self,choices):
4141        'choose a tutorial from a list'
4142        def onDoubleClick(event):
4143            'double-click closes the dialog'
4144            dlg.EndModal(wx.ID_OK)
4145        dlg = wx.Dialog(self,wx.ID_ANY,
4146                        'Select a tutorial to view. NB: indented ones require prerequisite',
4147                        style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)
4148        pnl = wx.Panel(dlg)
4149        sizer = wx.BoxSizer(wx.VERTICAL)
4150        listbox = wx.ListBox(pnl, wx.ID_ANY, choices=choices,
4151                             size=(450, 100),
4152                             style=wx.LB_SINGLE)
4153        sizer.Add(listbox,1,WACV|wx.EXPAND|wx.ALL,1)
4154        listbox.Bind(wx.EVT_LISTBOX_DCLICK, onDoubleClick)
4155        sizer.Add((10,10))
4156        btnsizer = wx.StdDialogButtonSizer()
4157        btn = wx.Button(pnl, wx.ID_CANCEL)
4158        btnsizer.AddButton(btn)
4159        OKbtn = wx.Button(pnl, wx.ID_OK)
4160        OKbtn.SetDefault()
4161        btnsizer.AddButton(OKbtn)
4162        btnsizer.Realize()
4163        sizer.Add((-1,5))
4164        sizer.Add(btnsizer,0,wx.ALIGN_RIGHT,50)
4165       
4166        pnl.SetSizer(sizer)
4167        sizer.Fit(dlg)
4168        self.CenterOnParent()
4169        if dlg.ShowModal() != wx.ID_OK:
4170            dlg.Destroy()
4171            return
4172        selected = listbox.GetSelection()
4173        dlg.Destroy()
4174        if selected < 0: return
4175        return selected
4176
4177    def UpdateDownloaded(self,event):
4178        'Find the downloaded tutorials and run an svn update on them'
4179        updated = 0
4180        for i in tutorialCatalog:
4181            if not os.path.exists(os.path.join(self.tutorialPath,i[0],i[1])): continue
4182            print('Updating '+i[0])
4183            GSASIIpath.svnUpdateDir(os.path.join(self.tutorialPath,i[0]))
4184            updated += 0
4185        if not updated:
4186            G2MessageBox(self,'Warning, you have no downloaded tutorials','None Downloaded')
4187        self.EndModal(wx.ID_OK)
4188       
4189    def DownloadAll(self,event):
4190        'Download or update all tutorials'
4191        fail = ''
4192        for i in tutorialCatalog:
4193            if os.path.exists(os.path.join(self.tutorialPath,i[0],i[1])):
4194                print('Updating '+i[0])
4195                GSASIIpath.svnUpdateDir(os.path.join(self.tutorialPath,i[0]))
4196            else:
4197                fulldir = os.path.join(self.tutorialPath,i[0])
4198                URL = G2BaseURL+'/Tutorials/'+i[0]+'/'
4199                if not GSASIIpath.svnInstallDir(URL,fulldir):
4200                    if fail: fail += ', '
4201                    fail += i[0]
4202        if fail: 
4203            G2MessageBox(self,'Error downloading tutorial(s)\n\t'+fail,'Download error')
4204        self.EndModal(wx.ID_OK)
4205                   
4206    def SelectDownloadLoc(self,event):
4207        '''Select a download location,
4208        Cancel resets to the default
4209        '''
4210        dlg = wx.DirDialog(self, "Choose a directory for tutorial downloads:",
4211                           defaultPath=self.tutorialPath)#,style=wx.DD_DEFAULT_STYLE)
4212                           #)
4213        try:
4214            if dlg.ShowModal() != wx.ID_OK:
4215                return
4216            pth = dlg.GetPath()
4217        finally:
4218            dlg.Destroy()
4219
4220        if not os.path.exists(pth):
4221            try:
4222                os.makedirs(pth)    #failing for no obvious reason
4223            except OSError:
4224                msg = 'The selected directory is not valid.\n\t'
4225                msg += pth
4226                msg += '\n\nAn attempt to create the directory failed'
4227                G2MessageBox(self.frame,msg)
4228                return
4229        if os.path.exists(os.path.join(pth,"help")) and os.path.exists(os.path.join(pth,"Exercises")):
4230            print("Note that you may have old tutorial files in the following directories")
4231            print('\t'+os.path.join(pth,"help"))
4232            print('\t'+os.path.join(pth,"Exercises"))
4233            print('Subdirectories in the above can be deleted to save space\n\n')
4234        self.tutorialPath = pth
4235        self.dataLoc.SetLabel(self.tutorialPath)
4236        if GSASIIpath.GetConfigValue('Tutorial_location') == pth: return
4237        vars = GetConfigValsDocs()
4238        try:
4239            vars['Tutorial_location'][1] = pth
4240            if GSASIIpath.GetConfigValue('debug'): print('Saving Tutorial_location: '+pth)
4241            GSASIIpath.SetConfigValue(vars)
4242            SaveConfigVars(vars)
4243        except KeyError:
4244            pass
4245           
4246if __name__ == '__main__':
4247    app = wx.PySimpleApp()
4248    GSASIIpath.InvokeDebugOpts()
4249    frm = wx.Frame(None) # create a frame
4250    frm.Show(True)
4251   
4252    dlg = OpenTutorial(frm)
4253    if dlg.ShowModal() == wx.ID_OK:
4254        print "OK"
4255    else:
4256        print "Cancel"
4257    dlg.Destroy()
4258    sys.exit()
4259    #======================================================================
4260    # test ScrolledMultiEditor
4261    #======================================================================
4262    # Data1 = {
4263    #      'Order':1,
4264    #      'omega':'string',
4265    #      'chi':2.0,
4266    #      'phi':'',
4267    #      }
4268    # elemlst = sorted(Data1.keys())
4269    # prelbl = sorted(Data1.keys())
4270    # dictlst = len(elemlst)*[Data1,]
4271    #Data2 = [True,False,False,True]
4272    #Checkdictlst = len(Data2)*[Data2,]
4273    #Checkelemlst = range(len(Checkdictlst))
4274    # print 'before',Data1,'\n',Data2
4275    # dlg = ScrolledMultiEditor(
4276    #     frm,dictlst,elemlst,prelbl,
4277    #     checkdictlst=Checkdictlst,checkelemlst=Checkelemlst,
4278    #     checklabel="Refine?",
4279    #     header="test")
4280    # if dlg.ShowModal() == wx.ID_OK:
4281    #     print "OK"
4282    # else:
4283    #     print "Cancel"
4284    # print 'after',Data1,'\n',Data2
4285    # dlg.Destroy()
4286    Data3 = {
4287         'Order':1.0,
4288         'omega':1.1,
4289         'chi':2.0,
4290         'phi':2.3,
4291         'Order1':1.0,
4292         'omega1':1.1,
4293         'chi1':2.0,
4294         'phi1':2.3,
4295         'Order2':1.0,
4296         'omega2':1.1,
4297         'chi2':2.0,
4298         'phi2':2.3,
4299         }
4300    elemlst = sorted(Data3.keys())
4301    dictlst = len(elemlst)*[Data3,]
4302    prelbl = elemlst[:]
4303    prelbl[0]="this is a much longer label to stretch things out"
4304    Data2 = len(elemlst)*[False,]
4305    Data2[1] = Data2[3] = True
4306    Checkdictlst = len(elemlst)*[Data2,]
4307    Checkelemlst = range(len(Checkdictlst))
4308    #print 'before',Data3,'\n',Data2
4309    #print dictlst,"\n",elemlst
4310    #print Checkdictlst,"\n",Checkelemlst
4311    # dlg = ScrolledMultiEditor(
4312    #     frm,dictlst,elemlst,prelbl,
4313    #     checkdictlst=Checkdictlst,checkelemlst=Checkelemlst,
4314    #     checklabel="Refine?",
4315    #     header="test",CopyButton=True)
4316    # if dlg.ShowModal() == wx.ID_OK:
4317    #     print "OK"
4318    # else:
4319    #     print "Cancel"
4320    #print 'after',Data3,'\n',Data2
4321
4322    # Data2 = list(range(100))
4323    # elemlst += range(2,6)
4324    # postlbl += range(2,6)
4325    # dictlst += len(range(2,6))*[Data2,]
4326
4327    # prelbl = range(len(elemlst))
4328    # postlbl[1] = "a very long label for the 2nd item to force a horiz. scrollbar"
4329    # header="""This is a longer\nmultiline and perhaps silly header"""
4330    # dlg = ScrolledMultiEditor(frm,dictlst,elemlst,prelbl,postlbl,
4331    #                           header=header,CopyButton=True)
4332    # print Data1
4333    # if dlg.ShowModal() == wx.ID_OK:
4334    #     for d,k in zip(dictlst,elemlst):
4335    #         print k,d[k]
4336    # dlg.Destroy()
4337    # if CallScrolledMultiEditor(frm,dictlst,elemlst,prelbl,postlbl,
4338    #                            header=header):
4339    #     for d,k in zip(dictlst,elemlst):
4340    #         print k,d[k]
4341
4342    #======================================================================
4343    # test G2MultiChoiceDialog
4344    #======================================================================
4345    choices = []
4346    for i in range(21):
4347        choices.append("option_"+str(i))
4348    dlg = G2MultiChoiceDialog(frm, 'Sequential refinement',
4349                              'Select dataset to include',
4350                              choices)
4351    sel = range(2,11,2)
4352    dlg.SetSelections(sel)
4353    dlg.SetSelections((1,5))
4354    if dlg.ShowModal() == wx.ID_OK:
4355        for sel in dlg.GetSelections():
4356            print sel,choices[sel]
4357   
4358    #======================================================================
4359    # test wx.MultiChoiceDialog
4360    #======================================================================
4361    # dlg = wx.MultiChoiceDialog(frm, 'Sequential refinement',
4362    #                           'Select dataset to include',
4363    #                           choices)
4364    # sel = range(2,11,2)
4365    # dlg.SetSelections(sel)
4366    # dlg.SetSelections((1,5))
4367    # if dlg.ShowModal() == wx.ID_OK:
4368    #     for sel in dlg.GetSelections():
4369    #         print sel,choices[sel]
4370
4371    # pnl = wx.Panel(frm)
4372    # siz = wx.BoxSizer(wx.VERTICAL)
4373
4374    # td = {'Goni':200.,'a':1.,'calc':1./3.,'string':'s'}
4375    # for key in sorted(td):
4376    #     txt = ValidatedTxtCtrl(pnl,td,key)
4377    #     siz.Add(txt)
4378    # pnl.SetSizer(siz)
4379    # siz.Fit(frm)
4380    # app.MainLoop()
4381    # print td
Note: See TracBrowser for help on using the repository browser.