source: trunk/GSASIIctrlGUI.py @ 3352

Last change on this file since 3352 was 3352, checked in by toby, 5 years ago

add optional histogram label (shown only as plot title so far); set up for 2-column tutorial selection, but not yet in use

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