source: trunk/GSASIIctrlGUI.py @ 3270

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

revise plot update code; add plotting docs & cleanup; always use last phase tab

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