source: trunk/GSASIIctrls.py @ 2631

Last change on this file since 2631 was 2631, checked in by toby, 6 years ago

unselect invalid text in ValidatedTxtCtrl? to bypass Linux bug

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