source: trunk/GSASIIctrlGUI.py @ 3826

Last change on this file since 3826 was 3826, checked in by vondreele, 3 years ago

fix future problem with wx.NewId? --> wx.NewIdRef?

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