source: trunk/GSASIIctrlGUI.py @ 3977

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

Update help info for Sample Parms & add help button

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