source: trunk/GSASIIctrlGUI.py @ 3810

Last change on this file since 3810 was 3810, checked in by toby, 3 years ago

fix range object in GPX file; fix spurious warning on tutorial update

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