source: trunk/GSASIIctrlGUI.py @ 3385

Last change on this file since 3385 was 3385, checked in by vondreele, 4 years ago

complete the tutorial abstracts

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