source: trunk/GSASIIctrlGUI.py @ 3342

Last change on this file since 3342 was 3342, checked in by toby, 4 years ago

add fit profile terms tutorial

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