source: trunk/GSASIIctrlGUI.py @ 3345

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

rework ValidatedTextCtrl? so that typeHint overrides the initial variable type; fix display of invalid numbers; use nDig to drive typeHint to float; postpone import of pyspg & polymask when not needed

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