source: trunk/GSASIIctrlGUI.py @ 3965

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

slightly better approach to tutorial path

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