source: trunk/GSASIIctrls.py @ 2546

Last change on this file since 2546 was 2546, checked in by vondreele, 5 years ago

cleanup of all GSASII main routines - remove unused variables, dead code, etc.

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