source: trunk/GSASIIctrlGUI.py @ 3736

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

modifications to allow Load Unit Cell command for incommensurate phases. (not for phases from mcif files!)
cleanup space group display for magnetic/incommensurate phases

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