source: trunk/GSASIIctrlGUI.py @ 3973

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

Load all fix; doc updates

  • 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-13 03:08:22 +0000 (Mon, 13 May 2019) $
5# $Author: toby $
6# $Revision: 3973 $
7# $URL: trunk/GSASIIctrlGUI.py $
8# $Id: GSASIIctrlGUI.py 3973 2019-05-13 03:08:22Z 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: 3973 $")
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            txtSize = txt.GetSize()[1]
2579        else:
2580            Sizer.Add(wx.StaticText(panel,label=title),0,WACV)
2581            txtSize = 0
2582        columnsSizer = wx.BoxSizer(wx.HORIZONTAL)
2583        self.sel = []
2584        self.mod = []
2585        Indx = {}
2586        for icol,col in enumerate(self.ColumnData):
2587            colSizer = wx.BoxSizer(wx.VERTICAL)
2588            colSizer.Add(wx.StaticText(panel,label=' Column #%d Select:'%(icol)),0,WACV)
2589            self.sel.append(wx.ComboBox(panel,value=' ',choices=self.ChoiceList,style=wx.CB_READONLY|wx.CB_DROPDOWN))
2590            colSizer.Add(self.sel[-1])
2591            colData = wx.TextCtrl(panel,value='\n'.join(self.ColumnData[icol]),size=(120,-1),
2592                style=wx.TE_MULTILINE|wx.TE_READONLY|wx.TE_DONTWRAP)
2593            colSizer.Add(colData,1,wx.ALL|WACV|wx.EXPAND,1)
2594            colSizer.Add(wx.StaticText(panel,label=' Modify by:'),0,WACV)
2595            mod = wx.TextCtrl(panel,size=(120,-1),value='',style=wx.TE_PROCESS_ENTER)
2596            mod.Bind(wx.EVT_TEXT_ENTER,OnModify)
2597            mod.Bind(wx.EVT_KILL_FOCUS,OnModify)
2598            Indx[mod.GetId()] = [icol,colData]
2599            colSizer.Add(mod,0,WACV)
2600            columnsSizer.Add(colSizer,0,wx.ALL|WACV|wx.EXPAND,10)
2601        Sizer.Add(columnsSizer,1,wx.ALL|WACV|wx.EXPAND,1)
2602        Sizer.Add(wx.StaticText(panel,label=' For modify by, enter arithmetic string eg. "-12345.67". "+", "-", "*", "/", "**" all allowed'),0,WACV) 
2603        Sizer.Add((-1,10))
2604        # OK/Cancel buttons
2605        btnsizer = wx.StdDialogButtonSizer()
2606        if useOK:
2607            self.OKbtn = wx.Button(panel, wx.ID_OK)
2608            self.OKbtn.SetDefault()
2609            btnsizer.AddButton(self.OKbtn)
2610            self.OKbtn.Bind(wx.EVT_BUTTON, OnOk)
2611        if useCANCEL:
2612            btn = wx.Button(panel, wx.ID_CANCEL)
2613            btnsizer.AddButton(btn)
2614        btnsizer.Realize()
2615        Sizer.Add((-1,5))
2616        Sizer.Add(btnsizer,0,wx.ALIGN_LEFT,20)
2617        Sizer.Add((-1,5))
2618        # OK done, let's get outa here
2619        panel.SetSizer(Sizer)
2620        panel.SetAutoLayout(1)
2621        panel.SetupScrolling()
2622        Size = [450,375]
2623        panel.SetSize(Size)
2624        Size[0] += 25; Size[1]+= 25+txtSize
2625        self.SetSize(Size)
2626       
2627    def GetSelection(self):
2628        'Returns the selected sample parm for each column'
2629        selCols = []
2630        for item in self.sel:
2631            selCols.append(item.GetValue())
2632        return selCols,self.ColumnData
2633   
2634################################################################################
2635class G2HistoDataDialog(wx.Dialog):
2636    '''A dialog for editing histogram data globally.
2637   
2638    :param wx.Frame ParentFrame: reference to parent frame
2639    :param str title: heading above list of choices
2640    :param str header: Title to place on window frame
2641    :param list ParmList: a list of names for the columns
2642    :param list ParmFmt: a list of formatting strings for the columns
2643    :param list: HistoList: a list of histogram names
2644    :param list ParmData: a list of lists of data matched to ParmList; one for each item in HistoList
2645    :param bool monoFont: If False (default), use a variable-spaced font;
2646      if True use a equally-spaced font.
2647    :param kw: optional keyword parameters for the wx.Dialog may
2648      be included such as size [which defaults to `(320,310)`] and
2649      style (which defaults to
2650      ``wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER | wx.CENTRE | wx.OK | wx.CANCEL``);
2651      note that ``wx.OK`` and ``wx.CANCEL`` controls the presence of the eponymous buttons in the dialog.
2652    :returns: the modified ParmData
2653   
2654    '''
2655
2656    def __init__(self,parent, title, header,ParmList,ParmFmt,HistoList,ParmData,
2657                 monoFont=False, **kw):
2658
2659        def OnOk(sevent):
2660            parent.Raise()
2661            self.EndModal(wx.ID_OK)
2662           
2663        def OnModify(event):
2664            Obj = event.GetEventObject()
2665            irow,it = Indx[Obj.GetId()]
2666            try:
2667                val = float(Obj.GetValue())
2668            except ValueError:
2669                val = self.ParmData[irow][it]
2670            self.ParmData[irow][it] = val
2671            Obj.SetValue(self.ParmFmt[it]%val)
2672                       
2673        # process keyword parameters, notably style
2674        options = {'size':(600,310), # default Frame keywords
2675                   'style':wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER|wx.CENTRE| wx.OK | wx.CANCEL,
2676                   }
2677        options.update(kw)
2678        self.ParmList = ParmList
2679        self.ParmFmt = ParmFmt
2680        self.HistoList = HistoList
2681        self.ParmData = ParmData
2682        nCol = len(ParmList)
2683        if options['style'] & wx.OK:
2684            useOK = True
2685            options['style'] ^= wx.OK
2686        else:
2687            useOK = False
2688        if options['style'] & wx.CANCEL:
2689            useCANCEL = True
2690            options['style'] ^= wx.CANCEL
2691        else:
2692            useCANCEL = False       
2693        # create the dialog frame
2694        wx.Dialog.__init__(self,parent,wx.ID_ANY,header,**options)
2695        panel = wxscroll.ScrolledPanel(self)
2696        # fill the dialog
2697        Sizer = wx.BoxSizer(wx.VERTICAL)
2698        Sizer.Add((-1,5))
2699        Sizer.Add(wx.StaticText(panel,label=title),0,WACV)
2700        dataSizer = wx.FlexGridSizer(0,nCol+1,0,0)
2701        self.sel = []
2702        self.mod = []
2703        Indx = {}
2704        for item in ['Histogram',]+self.ParmList:
2705            dataSizer.Add(wx.StaticText(panel,-1,label=' %10s '%(item)),0,WACV)
2706        for irow,name in enumerate(self.HistoList):
2707            dataSizer.Add(wx.StaticText(panel,label=name),0,WACV|wx.LEFT|wx.RIGHT,10)
2708            for it,item in enumerate(self.ParmData[irow]):
2709                dat = wx.TextCtrl(panel,-1,value=self.ParmFmt[it]%(item),style=wx.TE_PROCESS_ENTER)
2710                dataSizer.Add(dat,0,WACV)
2711                dat.Bind(wx.EVT_TEXT_ENTER,OnModify)
2712                dat.Bind(wx.EVT_KILL_FOCUS,OnModify)
2713                Indx[dat.GetId()] = [irow,it]
2714        Sizer.Add(dataSizer)
2715        Sizer.Add((-1,10))
2716        # OK/Cancel buttons
2717        btnsizer = wx.StdDialogButtonSizer()
2718        if useOK:
2719            self.OKbtn = wx.Button(panel, wx.ID_OK)
2720            self.OKbtn.SetDefault()
2721            btnsizer.AddButton(self.OKbtn)
2722            self.OKbtn.Bind(wx.EVT_BUTTON, OnOk)
2723        if useCANCEL:
2724            btn = wx.Button(panel, wx.ID_CANCEL)
2725            btnsizer.AddButton(btn)
2726        btnsizer.Realize()
2727        Sizer.Add((-1,5))
2728        Sizer.Add(btnsizer,0,wx.ALIGN_LEFT,20)
2729        Sizer.Add((-1,5))
2730        # OK done, let's get outa here
2731        panel.SetSizer(Sizer)
2732        panel.SetAutoLayout(1)
2733        panel.SetupScrolling()
2734        Size = [450,375]
2735        panel.SetSize(Size)
2736        Size[0] += 25; Size[1]+= 25
2737        self.SetSize(Size)
2738       
2739    def GetData(self):
2740        'Returns the modified ParmData'
2741        return self.ParmData
2742   
2743################################################################################
2744def ItemSelector(ChoiceList, ParentFrame=None,
2745                 title='Select an item',
2746                 size=None, header='Item Selector',
2747                 useCancel=True,multiple=False):
2748    ''' Provide a wx dialog to select a single item or multiple items from list of choices
2749
2750    :param list ChoiceList: a list of choices where one will be selected
2751    :param wx.Frame ParentFrame: Name of parent frame (default None)
2752    :param str title: heading above list of choices (default 'Select an item')
2753    :param wx.Size size: Size for dialog to be created (default None -- size as needed)
2754    :param str header: Title to place on window frame (default 'Item Selector')
2755    :param bool useCancel: If True (default) both the OK and Cancel buttons are offered
2756    :param bool multiple: If True then multiple items can be selected (default False)
2757   
2758    :returns: the selection index or None or a selection list if multiple is true
2759
2760    Called by GSASIIdataGUI.OnReOrgSelSeq() Which is not fully implemented.
2761    '''
2762    if multiple:
2763        if useCancel:
2764            dlg = G2MultiChoiceDialog(
2765                ParentFrame,title, header, ChoiceList)
2766        else:
2767            dlg = G2MultiChoiceDialog(
2768                ParentFrame,title, header, ChoiceList,
2769                style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER|wx.OK|wx.CENTRE)
2770    else:
2771        if useCancel:
2772            dlg = wx.SingleChoiceDialog(
2773                ParentFrame,title, header, ChoiceList)
2774        else:
2775            dlg = wx.SingleChoiceDialog(
2776                ParentFrame,title, header,ChoiceList,
2777                style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER|wx.OK|wx.CENTRE)
2778    if size: dlg.SetSize(size)
2779    if dlg.ShowModal() == wx.ID_OK:
2780        if multiple:
2781            dlg.Destroy()
2782            return dlg.GetSelections()
2783        else:
2784            dlg.Destroy()
2785            return dlg.GetSelection()
2786    else:
2787        dlg.Destroy()
2788        return None
2789    dlg.Destroy()
2790
2791######################################################### Column-order selection dialog
2792def GetItemOrder(parent,keylist,vallookup,posdict):
2793    '''Creates a dialog where items can be ordered into columns
2794   
2795    :param list keylist: is a list of keys for column assignments
2796    :param dict vallookup: is a dict keyed by names in keylist where each item is a dict.
2797       Each inner dict contains variable names as keys and their associated values
2798    :param dict posdict: is a dict keyed by names in keylist where each item is a dict.
2799       Each inner dict contains column numbers as keys and their associated
2800       variable name as a value. This is used for both input and output.
2801       
2802    '''
2803    dlg = wx.Dialog(parent,style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)
2804    sizer = wx.BoxSizer(wx.VERTICAL)
2805    spanel = OrderBox(dlg,keylist,vallookup,posdict)
2806    spanel.Fit()
2807    sizer.Add(spanel,1,wx.EXPAND)
2808    btnsizer = wx.StdDialogButtonSizer()
2809    btn = wx.Button(dlg, wx.ID_OK)
2810    btn.SetDefault()
2811    btnsizer.AddButton(btn)
2812    #btn = wx.Button(dlg, wx.ID_CANCEL)
2813    #btnsizer.AddButton(btn)
2814    btnsizer.Realize()
2815    sizer.Add(btnsizer, 0, wx.ALIGN_CENTER_VERTICAL|wx.EXPAND|wx.ALL, 5)
2816    dlg.SetSizer(sizer)
2817    sizer.Fit(dlg)
2818    dlg.ShowModal()
2819
2820################################################################################
2821class MultiIntegerDialog(wx.Dialog):
2822    '''Input a series of integers based on prompts
2823    '''
2824    def __init__(self,parent,title,prompts,values):
2825        wx.Dialog.__init__(self,parent,-1,title, 
2826            pos=wx.DefaultPosition,style=wx.DEFAULT_DIALOG_STYLE)
2827        self.panel = wx.Panel(self)         #just a dummy - gets destroyed in Draw!
2828        self.values = values
2829        self.prompts = prompts
2830        self.Draw()
2831       
2832    def Draw(self):
2833       
2834        def OnValItem(event):
2835            event.Skip()
2836            Obj = event.GetEventObject()
2837            ind = Indx[Obj.GetId()]
2838            try:
2839                val = int(Obj.GetValue())
2840                if val <= 0:
2841                    raise ValueError
2842            except ValueError:
2843                val = self.values[ind]
2844            self.values[ind] = val
2845            Obj.SetValue('%d'%(val))
2846           
2847        self.panel.Destroy()
2848        self.panel = wx.Panel(self)
2849        mainSizer = wx.BoxSizer(wx.VERTICAL)
2850        Indx = {}
2851        for ival,[prompt,value] in enumerate(zip(self.prompts,self.values)):
2852            mainSizer.Add(wx.StaticText(self.panel,-1,prompt),0,wx.ALIGN_CENTER)
2853            valItem = wx.TextCtrl(self.panel,-1,value='%d'%(value),style=wx.TE_PROCESS_ENTER)
2854            mainSizer.Add(valItem,0,wx.ALIGN_CENTER)
2855            Indx[valItem.GetId()] = ival
2856            valItem.Bind(wx.EVT_TEXT_ENTER,OnValItem)
2857            valItem.Bind(wx.EVT_KILL_FOCUS,OnValItem)
2858        OkBtn = wx.Button(self.panel,-1,"Ok")
2859        OkBtn.Bind(wx.EVT_BUTTON, self.OnOk)
2860        CancelBtn = wx.Button(self.panel,-1,'Cancel')
2861        CancelBtn.Bind(wx.EVT_BUTTON, self.OnCancel)
2862        btnSizer = wx.BoxSizer(wx.HORIZONTAL)
2863        btnSizer.Add((20,20),1)
2864        btnSizer.Add(OkBtn)
2865        btnSizer.Add(CancelBtn)
2866        btnSizer.Add((20,20),1)
2867        mainSizer.Add(btnSizer,0,wx.EXPAND|wx.BOTTOM|wx.TOP, 10)
2868        self.panel.SetSizer(mainSizer)
2869        self.panel.Fit()
2870        self.Fit()
2871
2872    def GetValues(self):
2873        return self.values
2874       
2875    def OnOk(self,event):
2876        parent = self.GetParent()
2877        parent.Raise()
2878        self.EndModal(wx.ID_OK)             
2879       
2880    def OnCancel(self,event):
2881        parent = self.GetParent()
2882        parent.Raise()
2883        self.EndModal(wx.ID_CANCEL)
2884
2885################################################################################
2886class MultiColumnSelection(wx.Dialog):
2887    '''Defines a Dialog widget that can be used to select an item from a multicolumn list.
2888    The first column should be short, but remaining columns are word-wrapped if the
2889    length of the information extends beyond the column.
2890   
2891    When created, the dialog will be shown and <dlg>.Selection will be set to the index
2892    of the selected row, or -1. Be sure to use <dlg>.Destroy() to remove the window
2893    after reading the selection. If the dialog cannot be shown because a very old
2894    version of wxPython is in use, <dlg>.Selection will be None.
2895   
2896    :param wx.Frame parent: the parent frame (or None)
2897    :param str title: A title for the dialog window
2898    :param list colLabels: labels for each column
2899    :param list choices: a nested list with a value for each row in the table. Within each value
2900      should be a list of values for each column. There must be at least one value, but it is
2901      OK to have more or fewer values than there are column labels (colLabels). Extra are ignored
2902      and unspecified columns are left blank.
2903    :param list colWidths: a list of int values specifying the column width for each
2904      column in the table (pixels). There must be a value for every column label (colLabels).
2905    :param int height: an optional height (pixels) for the table (defaults to 400)
2906   
2907    Example use::
2908   
2909        lbls = ('col 1','col 2','col 3')
2910        choices=(['test1','explanation of test 1'],
2911                 ['b', 'a really really long line that will be word-wrapped'],
2912                 ['test3','more explanation text','optional 3rd column text'])
2913        colWidths=[200,400,100]
2914        dlg = MultiColumnSelection(frm,'select tutorial',lbls,choices,colWidths)
2915        value = choices[dlg.Selection][0]
2916        dlg.Destroy()
2917   
2918    '''
2919    def __init__(self, parent, title, colLabels, choices, colWidths, height=400, *args, **kw):
2920        if len(colLabels) != len(colWidths):
2921            raise ValueError('Length of colLabels) != colWidths')
2922        sizex = 20 # extra room for borders, etc.
2923        for i in colWidths: sizex += i
2924        wx.Dialog.__init__(self, parent, wx.ID_ANY, title, *args,
2925                           style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER,
2926                           size=(sizex,height), **kw)
2927        try:
2928            from wx.lib.wordwrap import wordwrap
2929            import wx.lib.agw.ultimatelistctrl as ULC
2930        except ImportError:
2931            self.Selection = None
2932            return
2933        mainSizer = wx.BoxSizer(wx.VERTICAL)
2934        self.list = ULC.UltimateListCtrl(self, agwStyle=ULC.ULC_REPORT|ULC.ULC_HAS_VARIABLE_ROW_HEIGHT
2935                                         |ULC.ULC_HRULES|ULC.ULC_SINGLE_SEL)
2936        for i,(lbl,wid) in enumerate(zip(colLabels, colWidths)):
2937            self.list.InsertColumn(i, lbl, width=wid)
2938        for i,item in enumerate(choices):
2939            self.list.InsertStringItem(i, item[0])
2940            for j,item in enumerate(item[1:len(colLabels)]):
2941                item = wordwrap(StripIndents(item,True), colWidths[j+1], wx.ClientDC(self))
2942                self.list.SetStringItem(i,1+j, item)
2943        # make buttons
2944        mainSizer.Add(self.list, 1, wx.EXPAND|wx.ALL, 1)
2945        btnsizer = wx.BoxSizer(wx.HORIZONTAL)
2946        OKbtn = wx.Button(self, wx.ID_OK)
2947        OKbtn.SetDefault()
2948        btnsizer.Add(OKbtn)
2949        btn = wx.Button(self, wx.ID_CLOSE,"Cancel") 
2950        btnsizer.Add(btn)
2951        mainSizer.Add(btnsizer, 0, wx.ALIGN_CENTER|wx.ALL, 5)
2952        # bindings for close of window, double-click,...
2953        OKbtn.Bind(wx.EVT_BUTTON,self._onSelect)
2954        btn.Bind(wx.EVT_BUTTON,self._onClose)
2955        self.Bind(wx.EVT_CLOSE, self._onClose)
2956        self.Bind(wx.EVT_LIST_ITEM_ACTIVATED, self._onSelect)
2957        self.SetSizer(mainSizer)
2958        self.Selection = -1
2959        self.ShowModal()
2960    def _onClose(self,event):
2961        event.Skip()
2962        self.EndModal(wx.ID_CANCEL)
2963    def _onSelect(self,event):
2964        if self.list.GetNextSelected(-1) == -1: return
2965        self.Selection = self.list.GetNextSelected(-1)
2966        self.EndModal(wx.ID_OK)
2967       
2968################################################################################
2969class OrderBox(wxscroll.ScrolledPanel):
2970    '''Creates a panel with scrollbars where items can be ordered into columns
2971   
2972    :param list keylist: is a list of keys for column assignments
2973    :param dict vallookup: is a dict keyed by names in keylist where each item is a dict.
2974      Each inner dict contains variable names as keys and their associated values
2975    :param dict posdict: is a dict keyed by names in keylist where each item is a dict.
2976      Each inner dict contains column numbers as keys and their associated
2977      variable name as a value. This is used for both input and output.
2978     
2979    '''
2980    def __init__(self,parent,keylist,vallookup,posdict,*arg,**kw):
2981        self.keylist = keylist
2982        self.vallookup = vallookup
2983        self.posdict = posdict
2984        self.maxcol = 0
2985        for nam in keylist:
2986            posdict = self.posdict[nam]
2987            if posdict.keys():
2988                self.maxcol = max(self.maxcol, max(posdict))
2989        wxscroll.ScrolledPanel.__init__(self,parent,wx.ID_ANY,*arg,**kw)
2990        self.GBsizer = wx.GridBagSizer(4,4)
2991        self.SetBackgroundColour(WHITE)
2992        self.SetSizer(self.GBsizer)
2993        colList = [str(i) for i in range(self.maxcol+2)]
2994        for i in range(self.maxcol+1):
2995            wid = wx.StaticText(self,wx.ID_ANY,str(i),style=wx.ALIGN_CENTER)
2996            wid.SetBackgroundColour(DULL_YELLOW)
2997            wid.SetMinSize((50,-1))
2998            self.GBsizer.Add(wid,(0,i),flag=wx.ALIGN_CENTER|wx.EXPAND)
2999        self.chceDict = {}
3000        for row,nam in enumerate(self.keylist):
3001            posdict = self.posdict[nam]
3002            for col in posdict:
3003                lbl = posdict[col]
3004                pnl = wx.Panel(self,wx.ID_ANY)
3005                pnl.SetBackgroundColour(VERY_LIGHT_GREY)
3006                insize = wx.BoxSizer(wx.VERTICAL)
3007                wid = wx.Choice(pnl,wx.ID_ANY,choices=colList)
3008                insize.Add(wid,0,wx.EXPAND|wx.BOTTOM,3)
3009                wid.SetSelection(col)
3010                self.chceDict[wid] = (row,col)
3011                wid.Bind(wx.EVT_CHOICE,self.OnChoice)
3012                wid = wx.StaticText(pnl,wx.ID_ANY,lbl)
3013                insize.Add(wid,0,flag=wx.EXPAND)
3014                try:
3015                    val = G2py3.FormatSigFigs(self.vallookup[nam][lbl],maxdigits=8)
3016                except KeyError:
3017                    val = '?'
3018                wid = wx.StaticText(pnl,wx.ID_ANY,'('+val+')')
3019                insize.Add(wid,0,flag=wx.EXPAND)
3020                pnl.SetSizer(insize)
3021                self.GBsizer.Add(pnl,(row+1,col),flag=wx.EXPAND)
3022        self.SetAutoLayout(1)
3023        self.SetupScrolling()
3024        self.SetMinSize((
3025            min(700,self.GBsizer.GetSize()[0]),
3026            self.GBsizer.GetSize()[1]+20))
3027    def OnChoice(self,event):
3028        '''Called when a column is assigned to a variable
3029        '''
3030        row,col = self.chceDict[event.EventObject] # which variable was this?
3031        newcol = event.Selection # where will it be moved?
3032        if newcol == col:
3033            return # no change: nothing to do!
3034        prevmaxcol = self.maxcol # save current table size
3035        key = self.keylist[row] # get the key for the current row
3036        lbl = self.posdict[key][col] # selected variable name
3037        lbl1 = self.posdict[key].get(col+1,'') # next variable name, if any
3038        # if a posXXX variable is selected, and the next variable is posXXX, move them together
3039        repeat = 1
3040        if lbl[:3] == 'pos' and lbl1[:3] == 'int' and lbl[3:] == lbl1[3:]:
3041            repeat = 2
3042        for i in range(repeat): # process the posXXX and then the intXXX (or a single variable)
3043            col += i
3044            newcol += i
3045            if newcol in self.posdict[key]:
3046                # find first non-blank after newcol
3047                for mtcol in range(newcol+1,self.maxcol+2):
3048                    if mtcol not in self.posdict[key]: break
3049                l1 = range(mtcol,newcol,-1)+[newcol]
3050                l = range(mtcol-1,newcol-1,-1)+[col]
3051            else:
3052                l1 = [newcol]
3053                l = [col]
3054            # move all of the items, starting from the last column
3055            for newcol,col in zip(l1,l):
3056                #print 'moving',col,'to',newcol
3057                self.posdict[key][newcol] = self.posdict[key][col]
3058                del self.posdict[key][col]
3059                self.maxcol = max(self.maxcol,newcol)
3060                obj = self.GBsizer.FindItemAtPosition((row+1,col))
3061                self.GBsizer.SetItemPosition(obj.GetWindow(),(row+1,newcol))
3062                for wid in obj.GetWindow().Children:
3063                    if wid in self.chceDict:
3064                        self.chceDict[wid] = (row,newcol)
3065                        wid.SetSelection(self.chceDict[wid][1])
3066        # has the table gotten larger? If so we need new column heading(s)
3067        if prevmaxcol != self.maxcol:
3068            for i in range(prevmaxcol+1,self.maxcol+1):
3069                wid = wx.StaticText(self,wx.ID_ANY,str(i),style=wx.ALIGN_CENTER)
3070                wid.SetBackgroundColour(DULL_YELLOW)
3071                wid.SetMinSize((50,-1))
3072                self.GBsizer.Add(wid,(0,i),flag=wx.ALIGN_CENTER|wx.EXPAND)
3073            colList = [str(i) for i in range(self.maxcol+2)]
3074            for wid in self.chceDict:
3075                wid.SetItems(colList)
3076                wid.SetSelection(self.chceDict[wid][1])
3077        self.GBsizer.Layout()
3078        self.FitInside()
3079       
3080################################################################################
3081def GetImportFile(G2frame, message, defaultDir="", defaultFile="",
3082                      style=wx.FD_OPEN, parent=None,
3083                      *args, **kwargs):
3084    '''Uses a customized dialog that gets files from the appropriate import directory.
3085    Arguments are used the same as in :func:`wx.FileDialog`. Selection of
3086    multiple files is allowed if argument style includes wx.FD_MULTIPLE.
3087
3088    The default initial directory (unless overridden with argument defaultDir)
3089    is found in G2frame.TutorialImportDir, config setting Import_directory or
3090    G2frame.LastImportDir, see :func:`GetImportPath`.
3091
3092    The path of the first file entered is used to set G2frame.LastImportDir
3093    and optionally config setting Import_directory.
3094
3095    :returns: a list of files or an empty list
3096    '''
3097    if not parent: parent = G2frame
3098    #if GSASIIpath.GetConfigValue('debug'): print('debug: GetImportFile from '+defaultDir)
3099    dlg = wx.FileDialog(parent, message, defaultDir, defaultFile, *args,
3100                        style=style, **kwargs)
3101    pth = GetImportPath(G2frame)
3102    if not defaultDir and pth: dlg.SetDirectory(pth)
3103    try:
3104        if dlg.ShowModal() == wx.ID_OK:
3105            if style & wx.FD_MULTIPLE:
3106                filelist = dlg.GetPaths()
3107                if len(filelist) == 0: return []
3108            else:
3109                filelist = [dlg.GetPath(),]
3110            # not sure if we want to do this (why use wx.CHANGE_DIR?)
3111            if style & wx.FD_CHANGE_DIR: # to get Mac/Linux to change directory like windows!
3112                os.chdir(dlg.GetDirectory())
3113        else: # cancel was pressed
3114            return []
3115    finally:
3116        dlg.Destroy()
3117    # save the path of the first file and reset the TutorialImportDir variable
3118    pth = os.path.split(os.path.abspath(filelist[0]))[0]
3119    if GSASIIpath.GetConfigValue('Save_paths'): SaveImportDirectory(pth)
3120    G2frame.LastImportDir = pth
3121    G2frame.TutorialImportDir = None
3122    return filelist
3123
3124def GetImportPath(G2frame):
3125    '''Determines the default location to use for importing files. Tries sequentially
3126    G2frame.TutorialImportDir, config var Import_directory and G2frame.LastImportDir.
3127   
3128    :returns: a string containing the path to be used when reading files or None
3129      if none of the above are specified.
3130    '''
3131    if G2frame.TutorialImportDir:
3132        if os.path.exists(G2frame.TutorialImportDir):
3133            return G2frame.TutorialImportDir
3134        elif GSASIIpath.GetConfigValue('debug'):
3135            print('DBG_Tutorial location (TutorialImportDir) not found: '+G2frame.TutorialImportDir)
3136    pth = GSASIIpath.GetConfigValue('Import_directory')
3137    if pth:
3138        pth = os.path.expanduser(pth)
3139        if os.path.exists(pth):
3140            return pth
3141        elif GSASIIpath.GetConfigValue('debug'):
3142            print('Ignoring Config Import_directory value: '+
3143                      GSASIIpath.GetConfigValue('Import_directory'))
3144    if G2frame.LastImportDir:
3145        if os.path.exists(G2frame.LastImportDir):
3146            return G2frame.LastImportDir
3147        elif GSASIIpath.GetConfigValue('debug'):
3148            print('DBG_Warning: G2frame.LastImportDir not found = '+G2frame.LastImportDir)
3149    return None
3150
3151def GetExportPath(G2frame):
3152    '''Determines the default location to use for writing files. Tries sequentially
3153    G2frame.LastExportDir and G2frame.LastGPXdir.
3154   
3155    :returns: a string containing the path to be used when writing files or '.'
3156      if none of the above are specified.
3157    '''
3158    if G2frame.LastExportDir:
3159        return G2frame.LastExportDir
3160    elif G2frame.LastGPXdir:
3161        return G2frame.LastGPXdir
3162    else:
3163        return '.'
3164
3165################################################################################
3166class SGMessageBox(wx.Dialog):
3167    ''' Special version of MessageBox that displays space group & super space group text
3168    in two blocks
3169    '''
3170    def __init__(self,parent,title,text,table,spins=[],):
3171        wx.Dialog.__init__(self,parent,wx.ID_ANY,title,pos=wx.DefaultPosition,
3172            style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)
3173        self.text = text
3174        self.table = table
3175        self.panel = wx.Panel(self)
3176        self.spins = spins
3177        mainSizer = wx.BoxSizer(wx.VERTICAL)
3178        mainSizer.Add((0,10))
3179        for line in text:
3180            mainSizer.Add(wx.StaticText(self.panel,label='     %s     '%(line)),0,WACV)
3181        ncol = self.table[0].count(',')+1
3182        tableSizer = wx.FlexGridSizer(0,2*ncol+3,0,0)
3183        j = 0
3184        for item in self.table:
3185            if 'for' in item:
3186                mainSizer.Add(tableSizer,0,wx.ALIGN_LEFT)
3187                mainSizer.Add(wx.StaticText(self.panel,label=item),0,WACV)
3188                tableSizer = wx.FlexGridSizer(0,2*ncol+3,0,0)
3189                continue
3190            num,flds = item.split(')')
3191            tableSizer.Add(wx.StaticText(self.panel,label='     %s  '%(num+')')),0,WACV|wx.ALIGN_LEFT)           
3192            flds = flds.replace(' ','').split(',')
3193            for i,fld in enumerate(flds):
3194                if i < ncol-1:
3195                    text = wx.StaticText(self.panel,label='%s, '%(fld))
3196                else:
3197                    text = wx.StaticText(self.panel,label='%s'%(fld))
3198                if len(self.spins) and self.spins[j] < 0:
3199                    text.SetForegroundColour('Red')
3200                tableSizer.Add(text,0,WACV|wx.ALIGN_RIGHT)
3201            if not j%2:
3202                tableSizer.Add((20,0))
3203            j += 1
3204           
3205        def OnPrintOps(event):
3206            print(' Symmetry operations for %s:'%self.text[0].split(':')[1])
3207            for opText in G2spc.TextOps(self.text,self.table,reverse=True):
3208                print(opText.replace(' ','')) 
3209           
3210        mainSizer.Add(tableSizer,0,wx.ALIGN_LEFT)
3211        btnsizer = wx.BoxSizer(wx.HORIZONTAL)
3212        OKbtn = wx.Button(self.panel, wx.ID_OK)
3213        OKbtn.Bind(wx.EVT_BUTTON, self.OnOk)
3214        btnsizer.Add(OKbtn)
3215        printBtn = wx.Button(self.panel,label='Print Ops')
3216        printBtn.Bind(wx.EVT_BUTTON, OnPrintOps)
3217        btnsizer.Add(printBtn)
3218        mainSizer.Add((0,10))
3219        mainSizer.Add(btnsizer,0,wx.ALIGN_CENTER)
3220        self.panel.SetSizer(mainSizer)
3221        self.panel.Fit()
3222        self.Fit()
3223        size = self.GetSize()
3224        self.SetSize([size[0]+20,size[1]])
3225
3226    def Show(self):
3227        '''Use this method after creating the dialog to post it
3228        '''
3229        self.ShowModal()
3230        return
3231
3232    def OnOk(self,event):
3233        parent = self.GetParent()
3234        parent.Raise()
3235        self.EndModal(wx.ID_OK)
3236
3237################################################################################
3238class SGMagSpinBox(wx.Dialog):
3239    ''' Special version of MessageBox that displays magnetic spin text
3240    '''
3241    def __init__(self,parent,title,text,table,Cents,names,spins,ifGray):
3242        wx.Dialog.__init__(self,parent,wx.ID_ANY,title,pos=wx.DefaultPosition,
3243            style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER,size=wx.Size(420,350))
3244        self.text = text
3245        self.table = table
3246        self.names = names
3247        Nnames = len(self.names)
3248        self.spins = spins
3249        self.ifGray = ifGray
3250        self.PrintTable = [' Magnetic symmetry operations for %s:'%self.text[0].split(':')[1],]
3251        self.panel = wxscroll.ScrolledPanel(self)
3252        mainSizer = wx.BoxSizer(wx.VERTICAL)
3253        mainSizer.Add((0,10))
3254        cents = [0,]
3255        if len(Cents) > 1:
3256            cents = self.text[-1].split(';')
3257        for line in self.text:
3258            mainSizer.Add(wx.StaticText(self.panel,label='     %s     '%(line)),0,WACV)
3259            if 'equivalent' in line:
3260                break
3261        ncol = self.table[0].count(',')+2
3262        nG = 1
3263        j = 0
3264        for ng in range(nG):
3265            if ng:
3266                mainSizer.Add(wx.StaticText(self.panel,label="      for (0,0,0)+1'"),0,WACV)
3267                j = 0
3268            for ic,cent in enumerate(cents):
3269                Cent = np.zeros(3)
3270                if cent:
3271                    cent = cent.strip(' (').strip(')+\n')
3272                    Cent = np.array(eval(cent)[:3])
3273#                Cent = np.array(Cents[ic])
3274                if ic:
3275                    if cent: cent = cent.strip(' (').strip(')+\n')
3276                    label = '      for (%s)+'%(cent)
3277                    if ng:     #test for gray operators
3278                        label += "1'"
3279                    mainSizer.Add(wx.StaticText(self.panel,label=label),0,WACV)
3280                tableSizer = wx.FlexGridSizer(0,2*ncol+3,0,0)
3281                for item in self.table:
3282                    if ')' not in item:
3283                        continue
3284                    flds = item.split(')')[1]
3285                    tableSizer.Add(wx.StaticText(self.panel,label='  (%2d)  '%(j+1)),0,WACV)           
3286                    flds = flds.replace(' ','').split(',')
3287                    for i,fld in enumerate(flds):
3288                        if i < ncol-1:
3289                            text = wx.StaticText(self.panel,label='%s, '%(fld))
3290                        else:
3291                            text = wx.StaticText(self.panel,label='%s '%(fld))
3292                        tableSizer.Add(text,0,WACV)
3293                    text = wx.StaticText(self.panel,label=' (%s) '%(self.names[j%Nnames]))
3294                    try:
3295                        if self.spins[j] < 0:
3296                            text.SetForegroundColour('Red')
3297                            item += ',-1'
3298                        else:
3299                            item += ',+1'
3300                    except IndexError:
3301                        print(self.spins,j,self.names[j%Nnames])
3302                        item += ',+1'
3303                    M,T,S = G2spc.MagText2MTS(item.split(')')[1].replace(' ',''),CIF=False)
3304                    T = (T+Cent)%1.
3305                    item = G2spc.MT2text([M,T],reverse=True)
3306                    if S > 0:
3307                        item += ',+1'
3308                    else:
3309                        item += ',-1'
3310                    self.PrintTable.append(item.replace(' ','').lower())
3311                    tableSizer.Add(text,0,WACV)
3312                    if not j%2:
3313                        tableSizer.Add((20,0))
3314                    j += 1
3315                mainSizer.Add(tableSizer,0,WACV)
3316           
3317           
3318        def OnPrintOps(event):
3319            for item in self.PrintTable:
3320                print(item)
3321           
3322        btnsizer = wx.BoxSizer(wx.HORIZONTAL)
3323        OKbtn = wx.Button(self.panel, wx.ID_OK)
3324        btnsizer.Add(OKbtn)
3325        printBtn = wx.Button(self.panel,label='Print Ops')
3326        printBtn.Bind(wx.EVT_BUTTON, OnPrintOps)
3327        btnsizer.Add(printBtn)
3328        OKbtn.SetFocus()
3329        mainSizer.Add((0,10))
3330        mainSizer.Add(btnsizer,0,wx.ALIGN_CENTER)
3331       
3332        self.panel.SetSizer(mainSizer)
3333        self.panel.SetAutoLayout(True)
3334        self.panel.SetScrollRate(10,10)
3335        self.panel.SendSizeEvent()
3336
3337
3338    def Show(self):
3339        '''Use this method after creating the dialog to post it
3340        '''
3341        self.ShowModal()
3342        return
3343   
3344
3345################################################################################
3346class DisAglDialog(wx.Dialog):
3347    '''Distance/Angle Controls input dialog. After
3348    :meth:`ShowModal` returns, the results are found in
3349    dict :attr:`self.data`, which is accessed using :meth:`GetData`.
3350
3351    :param wx.Frame parent: reference to parent frame (or None)
3352    :param dict data: a dict containing the current
3353      search ranges or an empty dict, which causes default values
3354      to be used.
3355      Will be used to set element `DisAglCtls` in
3356      :ref:`Phase Tree Item <Phase_table>`
3357    :param dict default:  A dict containing the default
3358      search ranges for each element.
3359    :param bool Reset: if True (default), show Reset button
3360    :param bool Angle: if True (default), show angle radii
3361    '''
3362    def __init__(self,parent,data,default,Reset=True,Angle=True):
3363        text = 'Distance Angle Controls'
3364        if not Angle:
3365            text = 'Distance Controls'
3366        wx.Dialog.__init__(self,parent,wx.ID_ANY,text, 
3367            pos=wx.DefaultPosition,style=wx.DEFAULT_DIALOG_STYLE)
3368        self.default = default
3369        self.Reset = Reset
3370        self.Angle = Angle
3371        self.panel = None
3372        self._default(data,self.default)
3373        self.Draw(self.data)
3374               
3375    def _default(self,data,default):
3376        '''Set starting values for the search values, either from
3377        the input array or from defaults, if input is null
3378        '''
3379        if data:
3380            self.data = copy.deepcopy(data) # don't mess with originals
3381        else:
3382            self.data = {}
3383            self.data['Name'] = default['Name']
3384            self.data['Factors'] = [0.85,0.85]
3385            self.data['AtomTypes'] = default['AtomTypes']
3386            self.data['BondRadii'] = default['BondRadii'][:]
3387            self.data['AngleRadii'] = default['AngleRadii'][:]
3388
3389    def Draw(self,data):
3390        '''Creates the contents of the dialog. Normally called
3391        by :meth:`__init__`.
3392        '''
3393        if self.panel: self.panel.Destroy()
3394        self.panel = wx.Panel(self)
3395        mainSizer = wx.BoxSizer(wx.VERTICAL)
3396        mainSizer.Add(wx.StaticText(self.panel,-1,'Controls for phase '+data['Name']),
3397            0,WACV|wx.LEFT,10)
3398        mainSizer.Add((10,10),1)
3399       
3400        ncol = 3
3401        if not self.Angle:
3402            ncol=2
3403        radiiSizer = wx.FlexGridSizer(0,ncol,5,5)
3404        radiiSizer.Add(wx.StaticText(self.panel,-1,' Type'),0,WACV)
3405        radiiSizer.Add(wx.StaticText(self.panel,-1,'Bond radii'),0,WACV)
3406        if self.Angle:
3407            radiiSizer.Add(wx.StaticText(self.panel,-1,'Angle radii'),0,WACV)
3408        self.objList = {}
3409        for id,item in enumerate(self.data['AtomTypes']):
3410            radiiSizer.Add(wx.StaticText(self.panel,-1,' '+item),0,WACV)
3411            bRadii = ValidatedTxtCtrl(self.panel,data['BondRadii'],id,nDig=(10,3))
3412            radiiSizer.Add(bRadii,0,WACV)
3413            if self.Angle:
3414                aRadii = ValidatedTxtCtrl(self.panel,data['AngleRadii'],id,nDig=(10,3))
3415                radiiSizer.Add(aRadii,0,WACV)
3416        mainSizer.Add(radiiSizer,0,wx.EXPAND)
3417        if self.Angle:
3418            factorSizer = wx.FlexGridSizer(0,2,5,5)
3419            Names = ['Bond','Angle']
3420            for i,name in enumerate(Names):
3421                factorSizer.Add(wx.StaticText(self.panel,-1,name+' search factor'),0,WACV)
3422                bondFact = ValidatedTxtCtrl(self.panel,data['Factors'],i,nDig=(10,3))
3423                factorSizer.Add(bondFact)
3424            mainSizer.Add(factorSizer,0,wx.EXPAND)
3425       
3426        OkBtn = wx.Button(self.panel,-1,"Ok")
3427        OkBtn.Bind(wx.EVT_BUTTON, self.OnOk)
3428        btnSizer = wx.BoxSizer(wx.HORIZONTAL)
3429        btnSizer.Add((20,20),1)
3430        btnSizer.Add(OkBtn)
3431        if self.Reset:
3432            ResetBtn = wx.Button(self.panel,-1,'Reset')
3433            ResetBtn.Bind(wx.EVT_BUTTON, self.OnReset)
3434            btnSizer.Add(ResetBtn)
3435        btnSizer.Add((20,20),1)
3436        mainSizer.Add(btnSizer,0,wx.EXPAND|wx.BOTTOM|wx.TOP, 10)
3437        self.panel.SetSizer(mainSizer)
3438        self.panel.Fit()
3439        self.Fit()
3440   
3441    def GetData(self):
3442        'Returns the values from the dialog'
3443        return self.data
3444       
3445    def OnOk(self,event):
3446        'Called when the OK button is pressed'
3447        parent = self.GetParent()
3448        parent.Raise()
3449        self.EndModal(wx.ID_OK)             
3450       
3451    def OnReset(self,event):
3452        'Called when the Reset button is pressed'
3453        data = {}
3454        self._default(data,self.default)
3455        wx.CallAfter(self.Draw,self.data)
3456               
3457################################################################################
3458class ShowLSParms(wx.Dialog):
3459    '''Create frame to show least-squares parameters
3460    '''
3461    def __init__(self,parent,title,parmDict,varyList,fullVaryList,
3462                 size=(375,430)):
3463       
3464        wx.Dialog.__init__(self,parent,wx.ID_ANY,title,size=size,
3465                           style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)
3466        self.parmChoice = 'Phase'
3467        self.parmDict = parmDict
3468        self.varyList = varyList
3469        self.fullVaryList = fullVaryList
3470        self.choiceDict = {}
3471
3472        # make lists of variables of different types along with lists of parameter names, histogram #s, phase #s,...
3473        self.parmNames = sorted(list(parmDict.keys()))
3474        if '2' in platform.python_version_tuple()[0]: 
3475            basestr = basestring
3476        else:
3477            basestr = str
3478        splitNames = [item.split(':') for item in self.parmNames if len(item) > 3 and not isinstance(self.parmDict[item],basestr)]
3479        globNames = [':'.join(item) for item in splitNames if not item[0] and not item[1]]
3480        if len(globNames):
3481            self.choiceDict['Global'] = G2obj.SortVariables(globNames)
3482        self.globVars = sorted(list(set([' ',]+[item[2] for item in splitNames if not item[0] and not item[1]])))
3483        hisNames = [':'.join(item) for item in splitNames if not item[0] and item[1]]
3484        self.choiceDict['Histogram'] = G2obj.SortVariables(hisNames)
3485        self.hisNums = sorted(list(set([int(item.split(':')[1]) for item in hisNames])))
3486        self.hisNums = [' ',]+[str(item) for item in self.hisNums]
3487        self.hisVars = sorted(list(set([' ',]+[item[2] for item in splitNames if not item[0]])))
3488        phasNames = [':'.join(item) for item in splitNames if not item[1] and 'is' not in item[2]]
3489        self.choiceDict['Phase'] = G2obj.SortVariables(phasNames)
3490        self.phasNums = sorted([' ',]+list(set([item.split(':')[0] for item in phasNames])))
3491        if '' in self.phasNums: self.phasNums.remove('')
3492        self.phasVars = sorted(list(set([' ',]+[item[2] for item in splitNames if not item[1] and 'is' not in item[2]])))
3493        hapNames = [':'.join(item) for item in splitNames if item[0] and item[1]]
3494        self.choiceDict['Phase/Histo'] = G2obj.SortVariables(hapNames)
3495        self.hapVars = sorted(list(set([' ',]+[item[2] for item in splitNames if item[0] and item[1]])))
3496       
3497        self.hisNum = ' '
3498        self.phasNum = ' '
3499        self.varName = ' '
3500        self.listSel = 'Refined'
3501        self.DrawPanel()
3502       
3503    def repaintScrollTbl(self):
3504        '''Shows the selected variables
3505
3506        This routine is pretty slow when lots of variables are selected. Why?
3507        '''
3508        self.countSizer.Clear(True)
3509        self.headSizer.Clear(True)
3510        self.scrolledSizer.Clear(True)
3511        self.explainSizer.Clear(True)
3512       
3513        explainRefine = False
3514        count = 0
3515        for name in self.choiceDict[self.parmChoice]:
3516            if '2' in platform.python_version_tuple()[0]: 
3517                basestr = basestring
3518            else:
3519                basestr = str
3520            if isinstance(self.parmDict[name],basestr): continue
3521            if 'Refined' in self.listSel and (name not in self.fullVaryList
3522                                              ) and (name not in self.varyList):
3523                continue
3524            if 'Phase' in self.parmChoice:
3525                if self.phasNum != ' ' and name.split(':')[0] != self.phasNum: continue
3526            if 'Histo' in self.parmChoice:
3527                if self.hisNum != ' ' and name.split(':')[1] != self.hisNum: continue
3528            if (self.varName != ' ') and (self.varName not in name): continue
3529            try:
3530                value = G2py3.FormatSigFigs(self.parmDict[name])
3531            except TypeError:
3532                value = str(self.parmDict[name])+' -?' # unexpected
3533                #continue
3534            v = G2obj.getVarDescr(name)
3535            if v is None or v[-1] is None:
3536                self.scrolledSizer.Add((-1,-1))
3537            else:               
3538                ch = HelpButton(self.panel,G2obj.fmtVarDescr(name))
3539                self.scrolledSizer.Add(ch,0,wx.LEFT|wx.RIGHT|WACV|wx.ALIGN_CENTER,1)
3540            self.scrolledSizer.Add(wx.StaticText(self.panel,wx.ID_ANY,str(name)))
3541            if name in self.varyList:
3542                self.scrolledSizer.Add(wx.StaticText(self.panel,label='R',size=(50,-1)))   #TODO? maybe a checkbox for one stop refinement flag setting?
3543            elif name in self.fullVaryList:
3544                self.scrolledSizer.Add(wx.StaticText(self.panel,label='C',size=(50,-1)))
3545                explainRefine = True
3546            else:
3547                self.scrolledSizer.Add((50,-1))
3548            self.scrolledSizer.Add(wx.StaticText(self.panel,label=value),0,wx.ALIGN_RIGHT)
3549            count += 1
3550            if count > 200:
3551                msg = wx.StaticText(self,label='Too many parameters selected. Showing first 200')
3552                msg.SetBackgroundColour(wx.YELLOW)
3553                self.countSizer.Add(msg,0,wx.ALIGN_LEFT)
3554                self.countSizer.Add((-1,10))
3555                break
3556       
3557        if explainRefine:
3558            self.explainSizer.Add(
3559                wx.StaticText(self,label='"R" indicates a refined variable\n'+
3560                    '"C" indicates generated from a constraint'),0, wx.ALL,0)
3561        self.panel.SetAutoLayout(1)
3562        self.panel.SetupScrolling()
3563        self.SetMinSize(self.GetSize())        # Allow window to be enlarged but not made smaller
3564        for txt,wid,loc in zip(['','Parameter name','refine?','value'],self.scrolledSizer.GetColWidths(),
3565                           [wx.ALIGN_LEFT,wx.ALIGN_LEFT,wx.ALIGN_LEFT,wx.ALIGN_RIGHT]):
3566            self.headSizer.Add(wx.StaticText(self,wx.ID_ANY,txt,size=(wid,-1),style=loc),0,loc)
3567        self.SendSizeEvent()
3568           
3569    def DrawPanel(self):
3570        '''Draws the contents of the entire dialog. Called initially & when radio buttons are pressed
3571        '''
3572        def _OnParmSel(event):
3573            self.parmChoice = parmSel.GetStringSelection()
3574            self.varName = ' '
3575            wx.CallLater(100,self.DrawPanel)
3576           
3577        def OnPhasSel(event):
3578            event.Skip()
3579            self.phasNum = phasSel.GetValue()
3580            self.varName = ' '
3581            if varSel: varSel.SetSelection(0)
3582            wx.CallAfter(self.repaintScrollTbl)
3583
3584        def OnHistSel(event):
3585            event.Skip()
3586            self.hisNum = histSel.GetValue()
3587            self.varName = ' '
3588            if varSel: varSel.SetSelection(0)
3589            wx.CallAfter(self.repaintScrollTbl)
3590           
3591        def OnVarSel(event):
3592            event.Skip()
3593            self.varName = varSel.GetValue()
3594            self.phasNum = ' '
3595            if phasSel: phasSel.SetSelection(0)
3596            self.hisNum = ' '
3597            if histSel: histSel.SetSelection(0)
3598            wx.CallAfter(self.repaintScrollTbl)
3599           
3600        def OnListSel(event):
3601            self.listSel = listSel.GetStringSelection()
3602            wx.CallLater(100,self.DrawPanel)
3603                       
3604        def OnVarSpin(event):
3605            '''Respond when any of the SpinButton widgets are pressed'''
3606            event.Skip()
3607            Spinner = event.GetEventObject()
3608            move = Spinner.GetValue()
3609            Spinner.SetValue(0)
3610            varSel,binding = self.SpinDict[Spinner.GetId()]
3611            i = varSel.GetSelection() - move
3612            if i < 0:
3613                i = varSel.GetCount()-1
3614            elif i >= varSel.GetCount():
3615                i = 0
3616            varSel.SetSelection(i)
3617            wx.CallLater(100,binding,event)
3618
3619        def AddSpinner(varSizer,label,SelCtrl,binding):
3620            '''Add a label and a SpinButton to a Combo widget (SelCtrl)
3621            Saves a pointer to the combo widget and the callback used by that widget
3622            '''
3623            SelCtrl.Bind(wx.EVT_COMBOBOX,binding)
3624            varSizer.Add(wx.StaticText(self,label=label))
3625            varSelSizer = wx.BoxSizer(wx.HORIZONTAL)
3626            varSelSizer.Add(SelCtrl,0)
3627            varSpin = wx.SpinButton(self,style=wx.SP_VERTICAL)
3628            varSpin.SetValue(0)
3629            varSpin.SetRange(-1,1)
3630            varSpin.Bind(wx.EVT_SPIN, OnVarSpin)
3631            self.SpinDict[varSpin.GetId()] = SelCtrl,binding
3632            varSelSizer.Add(varSpin,0)
3633            varSizer.Add(varSelSizer,0)
3634           
3635        if self.GetSizer(): self.GetSizer().Clear(True)
3636        self.SpinDict = {}
3637        mainSizer = wx.BoxSizer(wx.VERTICAL)
3638        num = len(self.varyList)
3639        mainSizer.Add(wx.StaticText(self,label=' Number of refined variables: '+str(num)),0,wx.ALIGN_CENTER)
3640        if len(self.varyList) != len(self.fullVaryList):
3641            num = len(self.fullVaryList) - len(self.varyList)
3642            mainSizer.Add(wx.StaticText(self,label=' + '+str(num)+' parameters are varied via constraints'))
3643        choice = ['Phase','Phase/Histo','Histogram']
3644        if 'Global' in self.choiceDict:
3645            choice += ['Global',]
3646        parmSizer = wx.BoxSizer(wx.HORIZONTAL)
3647        parmSel = wx.RadioBox(self,wx.ID_ANY,'Parameter type:',choices=choice,
3648            majorDimension=1,style=wx.RA_SPECIFY_COLS)
3649        parmSel.Bind(wx.EVT_RADIOBOX,_OnParmSel)
3650        parmSel.SetStringSelection(self.parmChoice)
3651        parmSizer.Add(parmSel,0)
3652       
3653        selectionsSizer = wx.BoxSizer(wx.VERTICAL)       
3654        varSizer = wx.BoxSizer(wx.VERTICAL)
3655        varSel = None
3656        if self.parmChoice != 'Global': 
3657            if self.parmChoice in ['Phase',]:
3658                varSel = wx.ComboBox(self,choices=self.phasVars,value=self.varName,
3659                    style=wx.CB_READONLY|wx.CB_DROPDOWN)
3660            elif self.parmChoice in ['Histogram',]:
3661                varSel = wx.ComboBox(self,choices=self.hisVars,value=self.varName,
3662                    style=wx.CB_READONLY|wx.CB_DROPDOWN)
3663            elif self.parmChoice in ['Phase/Histo',]:
3664                varSel = wx.ComboBox(self,choices=self.hapVars,value=self.varName,
3665                    style=wx.CB_READONLY|wx.CB_DROPDOWN)
3666            AddSpinner(varSizer,'Parameter',varSel,OnVarSel)
3667        selectionsSizer.Add(varSizer,0)
3668               
3669        varSizer = wx.BoxSizer(wx.HORIZONTAL)
3670        phasSel = None
3671        if self.parmChoice in ['Phase','Phase/Histo'] and len(self.phasNums) > 1:
3672            numSizer = wx.BoxSizer(wx.VERTICAL)
3673            phasSel = wx.ComboBox(self,choices=self.phasNums,value=self.phasNum,
3674                style=wx.CB_READONLY|wx.CB_DROPDOWN)
3675            AddSpinner(numSizer,'Phase',phasSel,OnPhasSel)
3676            varSizer.Add(numSizer)
3677
3678        histSel = None
3679        if self.parmChoice in ['Histogram','Phase/Histo'] and len(self.hisNums) > 1:
3680            numSizer = wx.BoxSizer(wx.VERTICAL)
3681            histSel = wx.ComboBox(self,choices=self.hisNums,value=self.hisNum,
3682                style=wx.CB_READONLY|wx.CB_DROPDOWN)
3683            AddSpinner(numSizer,'Histogram',histSel,OnHistSel)
3684            varSizer.Add(numSizer)
3685        selectionsSizer.Add(varSizer,0)
3686        parmSizer.Add(selectionsSizer,0)
3687        mainSizer.Add(parmSizer,0)
3688       
3689        listSel = wx.RadioBox(self,wx.ID_ANY,'Parameter type:',
3690            choices=['All','Refined'],
3691            majorDimension=0,style=wx.RA_SPECIFY_COLS)
3692        listSel.SetStringSelection(self.listSel)
3693        listSel.Bind(wx.EVT_RADIOBOX,OnListSel)
3694        mainSizer.Add(listSel,0)
3695       
3696        self.countSizer = wx.BoxSizer(wx.VERTICAL)
3697        mainSizer.Add(self.countSizer)
3698        self.headSizer = wx.BoxSizer(wx.HORIZONTAL) # non-scrolling header       
3699        mainSizer.Add(self.headSizer,0)
3700        self.panel = wxscroll.ScrolledPanel(self)       
3701        self.scrolledSizer = wx.FlexGridSizer(cols=4,hgap=2,vgap=2)
3702        self.panel.SetSizer(self.scrolledSizer)
3703        mainSizer.Add(self.panel,1,wx.ALL|wx.EXPAND,1)
3704        self.explainSizer = wx.BoxSizer(wx.VERTICAL)
3705        mainSizer.Add(self.explainSizer)
3706        # make OK button
3707        btnsizer = wx.BoxSizer(wx.HORIZONTAL)
3708        btn = wx.Button(self, wx.ID_CLOSE,"Close") 
3709        btn.Bind(wx.EVT_BUTTON,self._onClose)
3710        btnsizer.Add(btn)
3711        mainSizer.Add(btnsizer, 0, wx.ALIGN_CENTER|wx.ALL, 5)
3712        self.SetSizer(mainSizer)
3713        wx.CallAfter(self.repaintScrollTbl)
3714               
3715    def _onClose(self,event):
3716        self.EndModal(wx.ID_CANCEL)
3717
3718################################################################################
3719#####  Customized Grid Support
3720################################################################################           
3721class GSGrid(wg.Grid):
3722    '''Basic wx.Grid implementation
3723    '''
3724    def __init__(self, parent, name=''):
3725        wg.Grid.__init__(self,parent,-1,name=name)
3726        if hasattr(parent.TopLevelParent,'currentGrids'):
3727            parent.TopLevelParent.currentGrids.append(self)      # save a reference to the grid in the Frame
3728        self.SetScrollRate(0,0)         #GSAS-II grids have no scroll bars by default
3729           
3730    def Clear(self):
3731        wg.Grid.ClearGrid(self)
3732       
3733    def SetCellReadOnly(self,r,c,readonly=True):
3734        self.SetReadOnly(r,c,isReadOnly=readonly)
3735       
3736    def SetCellStyle(self,r,c,color="white",readonly=True):
3737        self.SetCellBackgroundColour(r,c,color)
3738        self.SetReadOnly(r,c,isReadOnly=readonly)
3739       
3740    def GetSelection(self):
3741        #this is to satisfy structure drawing stuff in G2plt when focus changes
3742        return None
3743
3744    def InstallGridToolTip(self, rowcolhintcallback,
3745                           colLblCallback=None,rowLblCallback=None):
3746        '''code to display a tooltip for each item on a grid
3747        from http://wiki.wxpython.org/wxGrid%20ToolTips (buggy!), expanded to
3748        column and row labels using hints from
3749        https://groups.google.com/forum/#!topic/wxPython-users/bm8OARRVDCs
3750
3751        :param function rowcolhintcallback: a routine that returns a text
3752          string depending on the selected row and column, to be used in
3753          explaining grid entries.
3754        :param function colLblCallback: a routine that returns a text
3755          string depending on the selected column, to be used in
3756          explaining grid columns (if None, the default), column labels
3757          do not get a tooltip.
3758        :param function rowLblCallback: a routine that returns a text
3759          string depending on the selected row, to be used in
3760          explaining grid rows (if None, the default), row labels
3761          do not get a tooltip.
3762        '''
3763        prev_rowcol = [None,None,None]
3764        def OnMouseMotion(event):
3765            # event.GetRow() and event.GetCol() would be nice to have here,
3766            # but as this is a mouse event, not a grid event, they are not
3767            # available and we need to compute them by hand.
3768            x, y = self.CalcUnscrolledPosition(event.GetPosition())
3769            row = self.YToRow(y)
3770            col = self.XToCol(x)
3771            hinttext = ''
3772            win = event.GetEventObject()
3773            if [row,col,win] == prev_rowcol: # no change from last position
3774                if event: event.Skip()
3775                return
3776            if win == self.GetGridWindow() and row >= 0 and col >= 0:
3777                hinttext = rowcolhintcallback(row, col)
3778            elif win == self.GetGridColLabelWindow() and col >= 0:
3779                if colLblCallback: hinttext = colLblCallback(col)
3780            elif win == self.GetGridRowLabelWindow() and row >= 0:
3781                if rowLblCallback: hinttext = rowLblCallback(row)
3782            else: # this should be the upper left corner, which is empty
3783                if event: event.Skip()
3784                return
3785            if hinttext is None: hinttext = ''
3786            if 'phoenix' in wx.version():
3787                win.SetToolTip(hinttext)
3788            else:
3789                win.SetToolTipString(hinttext)
3790            prev_rowcol[:] = [row,col,win]
3791            if event: event.Skip()
3792        if 'phoenix' in wx.version():
3793            self.GetGridWindow().Bind(wx.EVT_MOTION,OnMouseMotion)
3794            if colLblCallback: self.GetGridColLabelWindow().Bind(wx.EVT_MOTION,OnMouseMotion)
3795            if rowLblCallback: self.GetGridRowLabelWindow().Bind(wx.EVT_MOTION,OnMouseMotion)
3796        else:
3797            wx.EVT_MOTION(self.GetGridWindow(), OnMouseMotion)
3798            if colLblCallback: wx.EVT_MOTION(self.GetGridColLabelWindow(), OnMouseMotion)
3799            if rowLblCallback: wx.EVT_MOTION(self.GetGridRowLabelWindow(), OnMouseMotion)
3800                                                   
3801################################################################################           
3802class Table(wg.PyGridTableBase):        #TODO: this works in python 3/phoenix but pygridtablebase doesn't exist
3803    '''Basic data table for use with GSgrid
3804    '''
3805    def __init__(self, data=[], rowLabels=None, colLabels=None, types = None):
3806        if 'phoenix' in wx.version():
3807            wg.GridTableBase.__init__(self)
3808        else:
3809            wg.PyGridTableBase.__init__(self)
3810        self.colLabels = colLabels
3811        self.rowLabels = rowLabels
3812        self.dataTypes = types
3813        self.data = data
3814       
3815    def AppendRows(self, numRows=1):
3816        self.data.append([])
3817        return True
3818       
3819    def CanGetValueAs(self, row, col, typeName):
3820        if self.dataTypes:
3821            colType = self.dataTypes[col].split(':')[0]
3822            if typeName == colType:
3823                return True
3824            else:
3825                return False
3826        else:
3827            return False
3828
3829    def CanSetValueAs(self, row, col, typeName):
3830        return self.CanGetValueAs(row, col, typeName)
3831
3832    def DeleteRow(self,pos):
3833        data = self.GetData()
3834        self.SetData([])
3835        new = []
3836        for irow,row in enumerate(data):
3837            if irow != pos:
3838                new.append(row)
3839        self.SetData(new)
3840       
3841    def GetColLabelValue(self, col):
3842        if self.colLabels:
3843            return self.colLabels[col]
3844           
3845    def GetData(self):
3846        data = []
3847        for row in range(self.GetNumberRows()):
3848            data.append(self.GetRowValues(row))
3849        return data
3850       
3851    def GetNumberCols(self):
3852        try:
3853            return len(self.colLabels)
3854        except TypeError:
3855            return None
3856       
3857    def GetNumberRows(self):
3858        return len(self.data)
3859       
3860    def GetRowLabelValue(self, row):
3861        if self.rowLabels:
3862            return self.rowLabels[row]
3863       
3864    def GetColValues(self, col):
3865        data = []
3866        for row in range(self.GetNumberRows()):
3867            data.append(self.GetValue(row, col))
3868        return data
3869       
3870    def GetRowValues(self, row):
3871        data = []
3872        for col in range(self.GetNumberCols()):
3873            data.append(self.GetValue(row, col))
3874        return data
3875       
3876    def GetTypeName(self, row, col):
3877        try:
3878            if self.data[row][col] is None:
3879                return wg.GRID_VALUE_STRING
3880            return self.dataTypes[col]
3881        except (TypeError,IndexError):
3882            return wg.GRID_VALUE_STRING
3883
3884    def GetValue(self, row, col):
3885        try:
3886            if self.data[row][col] is None: return ""
3887            return self.data[row][col]
3888        except IndexError:
3889            return None
3890           
3891    def InsertRows(self, pos, rows):
3892        for row in range(rows):
3893            self.data.insert(pos,[])
3894            pos += 1
3895       
3896    def IsEmptyCell(self,row,col):
3897        try:
3898            return not self.data[row][col]
3899        except IndexError:
3900            return True
3901       
3902    def OnKeyPress(self, event):
3903        dellist = self.GetSelectedRows()
3904        if event.GetKeyCode() == wx.WXK_DELETE and dellist:
3905            grid = self.GetView()
3906            for i in dellist: grid.DeleteRow(i)
3907               
3908    def SetColLabelValue(self, col, label):
3909        numcols = self.GetNumberCols()
3910        if col > numcols-1:
3911            self.colLabels.append(label)
3912        else:
3913            self.colLabels[col]=label
3914       
3915    def SetData(self,data):
3916        for row in range(len(data)):
3917            self.SetRowValues(row,data[row])
3918               
3919    def SetRowLabelValue(self, row, label):
3920        self.rowLabels[row]=label
3921           
3922    def SetRowValues(self,row,data):
3923        self.data[row] = data
3924           
3925    def SetValue(self, row, col, value):
3926        def innerSetValue(row, col, value):
3927            try:
3928                self.data[row][col] = value
3929            except TypeError:
3930                return
3931            except IndexError: # has this been tested?
3932                #print row,col,value
3933                # add a new row
3934                if row > self.GetNumberRows():
3935                    self.data.append([''] * self.GetNumberCols())
3936                elif col > self.GetNumberCols():
3937                    for row in range(self.GetNumberRows()): # bug fixed here
3938                        self.data[row].append('')
3939                #print self.data
3940                self.data[row][col] = value
3941        innerSetValue(row, col, value)
3942
3943################################################################################
3944class GridFractionEditor(wg.PyGridCellEditor):
3945    '''A grid cell editor class that allows entry of values as fractions as well
3946    as sine and cosine values [as s() and c()]
3947    '''
3948    def __init__(self,grid):
3949        if 'phoenix' in wx.version():
3950            wg.GridCellEditor.__init__(self)
3951        else:
3952            wg.PyGridCellEditor.__init__(self)
3953
3954    def Create(self, parent, id, evtHandler):
3955        self._tc = wx.TextCtrl(parent, id, "")
3956        self._tc.SetInsertionPoint(0)
3957        self.SetControl(self._tc)
3958
3959        if evtHandler:
3960            self._tc.PushEventHandler(evtHandler)
3961
3962        self._tc.Bind(wx.EVT_CHAR, self.OnChar)
3963
3964    def SetSize(self, rect):
3965        if 'phoenix' in wx.version():
3966            self._tc.SetSize(rect.x, rect.y, rect.width+2, rect.height+2,
3967                               wx.SIZE_ALLOW_MINUS_ONE)
3968        else:
3969            self._tc.SetDimensions(rect.x, rect.y, rect.width+2, rect.height+2,                                wx.SIZE_ALLOW_MINUS_ONE)
3970
3971    def BeginEdit(self, row, col, grid):
3972        self.startValue = grid.GetTable().GetValue(row, col)
3973        self._tc.SetValue(str(self.startValue))
3974        self._tc.SetInsertionPointEnd()
3975        self._tc.SetFocus()
3976        self._tc.SetSelection(0, self._tc.GetLastPosition())
3977
3978    def EndEdit(self, row, col, grid, oldVal=None):
3979        changed = False
3980
3981        self.nextval = self.startValue
3982        val = self._tc.GetValue().lower().strip()
3983        if val != str(self.startValue):
3984            changed = True
3985            neg = False
3986            if val.startswith('-'):
3987                neg = True
3988                val = val[1:]
3989            # allow old GSAS s20 and c20 etc for sind(20) and cosd(20)
3990            if val.startswith('s') and '(' not in val:
3991                val = 'sind('+val.strip('s')+')'
3992            elif val.startswith('c') and '(' not in val:
3993                val = 'cosd('+val.strip('c')+')'
3994            if neg:
3995                val = '-' + val
3996            val = G2py3.FormulaEval(val)
3997            if val is not None:
3998                self.nextval = val
3999            else:
4000                return None
4001            if oldVal is None: # this arg appears in 2.9+; before, we should go ahead & change the table
4002                grid.GetTable().SetValue(row, col, val) # update the table
4003            # otherwise self.ApplyEdit gets called
4004
4005        self.startValue = ''
4006        self._tc.SetValue('')
4007        return changed
4008   
4009    def ApplyEdit(self, row, col, grid):
4010        """ Called only in wx >= 2.9
4011        Save the value of the control into the grid if EndEdit() returns as True
4012        """
4013        grid.GetTable().SetValue(row, col, self.nextval) # update the table
4014
4015    def Reset(self):
4016        self._tc.SetValue(self.startValue)
4017        self._tc.SetInsertionPointEnd()
4018
4019    def Clone(self,grid):
4020        return GridFractionEditor(grid)
4021
4022    def StartingKey(self, evt):
4023        self.OnChar(evt)
4024        if evt.GetSkipped():
4025            self._tc.EmulateKeyPress(evt)
4026
4027    def OnChar(self, evt):
4028        key = evt.GetKeyCode()
4029        if key < 32 or key >= 127:
4030            evt.Skip()
4031        elif chr(key).lower() in '.+-*/0123456789cosind()':
4032            evt.Skip()
4033        else:
4034            evt.StopPropagation()
4035
4036################################################################################
4037#####  Get an output file or directory
4038################################################################################           
4039def askSaveFile(G2frame,defnam,extension,longFormatName,parent=None):
4040    '''Ask the user to supply a file name
4041
4042    :param wx.Frame G2frame: The main GSAS-II window
4043    :param str defnam: a default file name
4044    :param str extension: the default file extension beginning with a '.'
4045    :param str longFormatName: a description of the type of file
4046    :param wx.Frame parent: the parent window for the dialog. Defaults
4047      to G2frame.
4048
4049    :returns: a file name (str) or None if Cancel is pressed
4050    '''
4051
4052    if not parent: parent = G2frame
4053    pth = GetExportPath(G2frame)
4054    #if GSASIIpath.GetConfigValue('debug'): print('debug: askSaveFile to '+pth)
4055    dlg = wx.FileDialog(
4056        parent, 'Input name for file to write', pth, defnam,
4057        longFormatName+' (*'+extension+')|*'+extension,
4058        wx.FD_SAVE|wx.FD_OVERWRITE_PROMPT)
4059    dlg.CenterOnParent()
4060    try:
4061        if dlg.ShowModal() == wx.ID_OK:
4062            filename = dlg.GetPath()
4063            G2frame.LastExportDir = os.path.split(filename)[0]
4064            filename = os.path.splitext(filename)[0]+extension # make sure extension is correct
4065        else:
4066            filename = None
4067    finally:
4068        dlg.Destroy()
4069    return filename
4070
4071def askSaveDirectory(G2frame):
4072    '''Ask the user to supply a directory name. Path name is used as the
4073    starting point for the next export path search.
4074
4075    :returns: a directory name (str) or None if Cancel is pressed
4076    '''
4077    pth = GetExportPath(G2frame)
4078    dlg = wx.DirDialog(
4079            G2frame, 'Input directory where file(s) will be written', pth,
4080            wx.DD_DEFAULT_STYLE)
4081    dlg.CenterOnParent()
4082    try:
4083        if dlg.ShowModal() == wx.ID_OK:
4084            filename = dlg.GetPath()
4085            G2frame.LastExportDir = filename
4086        else:
4087            filename = None
4088    finally:
4089        dlg.Destroy()
4090    return filename
4091
4092################################################################################
4093#####  Customized Notebook
4094################################################################################           
4095class GSNoteBook(wx.aui.AuiNotebook):
4096    '''Notebook used in various locations; implemented with wx.aui extension
4097    '''
4098    def __init__(self, parent, name='',size = None,style=wx.aui.AUI_NB_TOP |
4099        wx.aui.AUI_NB_SCROLL_BUTTONS):
4100        wx.aui.AuiNotebook.__init__(self, parent, style=style)
4101        if size: self.SetSize(size)
4102        self.parent = parent
4103        self.PageChangeHandler = None
4104       
4105    def PageChangeEvent(self,event):
4106        pass
4107#        G2frame = self.parent.G2frame
4108#        page = event.GetSelection()
4109#        if self.PageChangeHandler:
4110#            if log.LogInfo['Logging']:
4111#                log.MakeTabLog(
4112#                    G2frame.dataWindow.GetTitle(),
4113#                    G2frame.dataDisplay.GetPageText(page)
4114#                    )
4115#            self.PageChangeHandler(event)
4116           
4117#    def Bind(self,eventtype,handler,*args,**kwargs):
4118#        '''Override the Bind() function so that page change events can be trapped
4119#        '''
4120#        if eventtype == wx.aui.EVT_AUINOTEBOOK_PAGE_CHANGED:
4121#            self.PageChangeHandler = handler
4122#            wx.aui.AuiNotebook.Bind(self,eventtype,self.PageChangeEvent)
4123#            return
4124#        wx.aui.AuiNotebook.Bind(self,eventtype,handler,*args,**kwargs)
4125                                                     
4126    def Clear(self):
4127        GSNoteBook.DeleteAllPages(self)
4128       
4129    def FindPage(self,name):
4130        numPage = self.GetPageCount()
4131        for page in range(numPage):
4132            if self.GetPageText(page) == name:
4133                return page
4134        return None
4135
4136    def ChangeSelection(self,page):
4137        # in wx.Notebook ChangeSelection is like SetSelection, but it
4138        # does not invoke the event related to pressing the tab button
4139        # I don't see a way to do that in aui.
4140        oldPage = self.GetSelection()
4141        self.SetSelection(page)
4142        return oldPage
4143
4144    # def __getattribute__(self,name):
4145    #     '''This method provides a way to print out a message every time
4146    #     that a method in a class is called -- to see what all the calls
4147    #     might be, or where they might be coming from.
4148    #     Cute trick for debugging!
4149    #     '''
4150    #     attr = object.__getattribute__(self, name)
4151    #     if hasattr(attr, '__call__'):
4152    #         def newfunc(*args, **kwargs):
4153    #             print('GSauiNoteBook calling %s' %attr.__name__)
4154    #             result = attr(*args, **kwargs)
4155    #             return result
4156    #         return newfunc
4157    #     else:
4158    #         return attr
4159           
4160################################################################################
4161#### Help support routines
4162################################################################################
4163class MyHelp(wx.Menu):
4164    '''
4165    A class that creates the contents of a help menu.
4166    The menu will start with two entries:
4167
4168    * 'Help on <helpType>': where helpType is a reference to an HTML page to
4169      be opened
4170    * About: opens an About dialog using OnHelpAbout. N.B. on the Mac this
4171      gets moved to the App menu to be consistent with Apple style.
4172
4173    NOTE: for this to work properly with respect to system menus, the title
4174    for the menu must be &Help, or it will not be processed properly:
4175
4176    ::
4177
4178       menu.Append(menu=MyHelp(self,...),title="&Help")
4179
4180    '''
4181    def __init__(self,frame,includeTree=False,morehelpitems=[]):
4182        wx.Menu.__init__(self,'')
4183        self.HelpById = {}
4184        self.frame = frame
4185        self.Append(wx.ID_ABOUT,'&About GSAS-II','')
4186        frame.Bind(wx.EVT_MENU, self.OnHelpAbout, id=wx.ID_ABOUT)
4187        if GSASIIpath.whichsvn():
4188            helpobj = self.Append(wx.ID_ANY,'&Check for updates','')
4189            frame.Bind(wx.EVT_MENU, self.OnCheckUpdates, helpobj)
4190            helpobj = self.Append(wx.ID_ANY,'&Regress to an old GSAS-II version','')
4191            frame.Bind(wx.EVT_MENU, self.OnSelectVersion, helpobj)
4192            # if GSASIIpath.svnTestBranch():
4193            #     msg = "&Switch back to standard GSAS-II version"
4194            # else:
4195            #     msg = "&Switch to test (2frame) GSAS-II version"
4196            # helpobj = self.Append(
4197            #     help='', id=wx.ID_ANY, kind=wx.ITEM_NORMAL,text=msg)
4198            # frame.Bind(wx.EVT_MENU, self.OnSelectBranch, helpobj)
4199        # provide special help topic names for extra items in help menu
4200        for lbl,indx in morehelpitems:
4201            helpobj = self.Append(wx.ID_ANY,lbl,'')
4202            frame.Bind(wx.EVT_MENU, self.OnHelpById, helpobj)
4203            self.HelpById[helpobj.GetId()] = indx
4204        # add help lookup(s) in gsasii.html
4205        self.AppendSeparator()
4206        if includeTree:
4207            helpobj = self.Append(wx.ID_ANY,'Help on Data tree','')
4208            frame.Bind(wx.EVT_MENU, self.OnHelpById, id=helpobj.GetId())
4209            self.HelpById[helpobj.GetId()] = 'Data tree'
4210        helpobj = self.Append(wx.ID_ANY,'Help on current data tree item','')
4211        frame.Bind(wx.EVT_MENU, self.OnHelpById, id=helpobj.GetId())
4212       
4213    def OnHelpById(self,event):
4214        '''Called when Help on... is pressed in a menu. Brings up a web page
4215        for documentation. Uses the helpKey value from the dataWindow window
4216        unless a special help key value has been defined for this menu id in
4217        self.HelpById
4218
4219        Note that self should now (2frame) be child of the main window (G2frame)
4220        '''
4221        if hasattr(self.frame,'dataWindow'):  # Debug code: check this is called from menu in G2frame
4222            # should always be true in 2 Frame version
4223            dW = self.frame.dataWindow
4224        else:
4225            print('help error: not called from standard menu?')
4226            print (self)
4227            return           
4228        try:
4229            helpKey = dW.helpKey # look up help from helpKey in data window
4230            #if GSASIIpath.GetConfigValue('debug'): print 'DBG_dataWindow help: key=',helpKey
4231        except AttributeError:
4232            helpKey = ''
4233            if GSASIIpath.GetConfigValue('debug'): print('DBG_No helpKey for current dataWindow!')
4234        helpType = self.HelpById.get(event.GetId(),helpKey) # see if there is a special help topic
4235        #if GSASIIpath.GetConfigValue('debug'): print 'DBG_helpKey=',helpKey,'  helpType=',helpType
4236        if helpType == 'Tutorials':
4237            dlg = OpenTutorial(self.frame)
4238            dlg.ShowModal()
4239            dlg.Destroy()
4240            return
4241        else:
4242            ShowHelp(helpType,self.frame)
4243
4244    def OnHelpAbout(self, event):
4245        "Display an 'About GSAS-II' box"
4246        import GSASII
4247        try:
4248            import wx.adv as wxadv  # AboutBox moved here in Phoenix
4249        except:
4250            wxadv = wx
4251        info = wxadv.AboutDialogInfo()
4252        info.Name = 'GSAS-II'
4253        ver = GSASIIpath.svnGetRev()
4254        if not ver:
4255            ver = GSASIIpath.GetVersionNumber()
4256        info.SetVersion(ver)
4257        #info.Developers = ['Robert B. Von Dreele','Brian H. Toby']
4258        info.Copyright = ('(c) ' + time.strftime('%Y') +
4259''' Argonne National Laboratory
4260This product includes software developed
4261by the UChicago Argonne, LLC, as
4262Operator of Argonne National Laboratory.''')
4263        info.Description = '''General Structure Analysis System-II (GSAS-II)
4264Robert B. Von Dreele and Brian H. Toby
4265
4266Please cite as:
4267  B.H. Toby & R.B. Von Dreele, J. Appl. Cryst. 46, 544-549 (2013)
4268For small angle use cite:
4269  R.B. Von Dreele, J. Appl. Cryst. 47, 1748-9 (2014)
4270For DIFFaX use cite:
4271  M.M.J. Treacy, J.M. Newsam & M.W. Deem,
4272  Proc. Roy. Soc. Lond. A 433, 499-520 (1991)
4273'''
4274        info.WebSite = ("https://subversion.xray.aps.anl.gov/trac/pyGSAS","GSAS-II home page")
4275        wxadv.AboutBox(info)
4276
4277    def OnCheckUpdates(self,event):
4278        '''Check if the GSAS-II repository has an update for the current source files
4279        and perform that update if requested.
4280        '''           
4281        if not GSASIIpath.whichsvn():
4282            dlg = wx.MessageDialog(self.frame,
4283                                   'No Subversion','Cannot update GSAS-II because subversion (svn) was not found.',
4284                                   wx.OK)
4285            dlg.ShowModal()
4286            dlg.Destroy()
4287            return
4288        wx.BeginBusyCursor()
4289        local = GSASIIpath.svnGetRev()
4290        if local is None: 
4291            wx.EndBusyCursor()
4292            dlg = wx.MessageDialog(self.frame,
4293                                   'Unable to run subversion on the GSAS-II current directory. Is GSAS-II installed correctly?',
4294                                   'Subversion error',
4295                                   wx.OK)
4296            dlg.ShowModal()
4297            dlg.Destroy()
4298            return
4299        print ('Installed GSAS-II version: '+local)
4300        repos = GSASIIpath.svnGetRev(local=False)
4301        wx.EndBusyCursor()
4302        # has the current branch disappeared? If so, switch to the trunk -- not fully tested
4303        if (repos is None and "not found" in GSASIIpath.svnLastError.lower()
4304            and "path" in GSASIIpath.svnLastError.lower()):
4305            print('Repository is gone, will switch to trunk')
4306            GSASIIpath.svnSwitch2branch()
4307            return
4308        elif repos is None: 
4309            dlg = wx.MessageDialog(self.frame,
4310                                   'Unable to access the GSAS-II server. Is this computer on the internet?',
4311                                   'Server unavailable',
4312                                   wx.OK)
4313            dlg.ShowModal()
4314            dlg.Destroy()
4315            return
4316        print ('GSAS-II version on server: '+repos)
4317        if local == repos:
4318            dlg = wx.MessageDialog(self.frame,
4319                                   'GSAS-II is up-to-date. Version '+local+' is already loaded.',
4320                                   'GSAS-II Up-to-date',
4321                                   wx.OK)
4322            dlg.ShowModal()
4323            dlg.Destroy()
4324            return
4325        mods = GSASIIpath.svnFindLocalChanges()
4326        if mods:
4327            dlg = wx.MessageDialog(self.frame,
4328                                   'You have version '+local+
4329                                   ' of GSAS-II installed, but the current version is '+repos+
4330                                   '. However, '+str(len(mods))+
4331                                   ' file(s) on your local computer have been modified.'
4332                                   ' Updating will attempt to merge your local changes with '
4333                                   'the latest GSAS-II version, but if '
4334                                   'conflicts arise, local changes will be '
4335                                   'discarded. It is also possible that the '
4336                                   'local changes my prevent GSAS-II from running. '
4337                                   'Press OK to start an update if this is acceptable:',
4338                                   'Local GSAS-II Mods',
4339                                   wx.OK|wx.CANCEL)
4340            if dlg.ShowModal() != wx.ID_OK:
4341                dlg.Destroy()
4342                return
4343            else:
4344                dlg.Destroy()
4345        else:
4346            dlg = wx.MessageDialog(self.frame,
4347                                   'You have version '+local+
4348                                   ' of GSAS-II installed, but the current version is '+repos+
4349                                   '. Press OK to start an update:',
4350                                   'GSAS-II Updates',
4351                                   wx.OK|wx.CANCEL)
4352            if dlg.ShowModal() != wx.ID_OK:
4353                dlg.Destroy()
4354                return
4355            dlg.Destroy()
4356        print ('start updates')
4357        dlg = wx.MessageDialog(self.frame,
4358                               'Your project will now be saved, GSAS-II will exit and an update '
4359                               'will be performed and GSAS-II will restart. Press Cancel to '
4360                               'abort the update',
4361                               'Start update?',
4362                               wx.OK|wx.CANCEL)
4363        if dlg.ShowModal() != wx.ID_OK:
4364            dlg.Destroy()
4365            return
4366        dlg.Destroy()
4367        if self.frame.GPXtree.GetCount() > 1:
4368            self.frame.OnFileSave(event)
4369            GPX = self.frame.GSASprojectfile
4370            GSASIIpath.svnUpdateProcess(projectfile=GPX)
4371        else:
4372            GSASIIpath.svnUpdateProcess()
4373        return
4374
4375    def OnSelectVersion(self,event):
4376        '''Allow the user to select a specific version of GSAS-II
4377        '''
4378        if not GSASIIpath.whichsvn():
4379            dlg = wx.MessageDialog(self,'No Subversion','Cannot update GSAS-II because subversion (svn) '+
4380                                   'was not found.'
4381                                   ,wx.OK)
4382            dlg.ShowModal()
4383            return
4384        local = GSASIIpath.svnGetRev()
4385        if local is None: 
4386            dlg = wx.MessageDialog(self.frame,
4387                                   'Unable to run subversion on the GSAS-II current directory. Is GSAS-II installed correctly?',
4388                                   'Subversion error',
4389                                   wx.OK)
4390            dlg.ShowModal()
4391            dlg.Destroy()
4392            return
4393        mods = GSASIIpath.svnFindLocalChanges()
4394        if mods:
4395            dlg = wx.MessageDialog(self.frame,
4396                                   'You have version '+local+
4397                                   ' of GSAS-II installed'
4398                                   '. However, '+str(len(mods))+
4399                                   ' file(s) on your local computer have been modified.'
4400                                   ' Downdating will attempt to merge your local changes with '
4401                                   'the selected GSAS-II version. '
4402                                   'Downdating is not encouraged because '
4403                                   'if merging is not possible, your local changes will be '
4404                                   'discarded. It is also possible that the '
4405                                   'local changes my prevent GSAS-II from running. '
4406                                   'Press OK to continue anyway.',
4407                                   'Local GSAS-II Mods',
4408                                   wx.OK|wx.CANCEL)
4409            if dlg.ShowModal() != wx.ID_OK:
4410                dlg.Destroy()
4411                return
4412            dlg.Destroy()
4413        if GSASIIpath.svnGetRev(local=False) is None:
4414            dlg = wx.MessageDialog(self.frame,
4415                                   'Error obtaining current GSAS-II version. Is internet access working correctly?',
4416                                   'Subversion error',
4417                                   wx.OK)
4418            dlg.ShowModal()
4419            dlg.Destroy()
4420            return
4421        dlg = downdate(parent=self.frame)
4422        if dlg.ShowModal() == wx.ID_OK:
4423            ver = dlg.getVersion()
4424        else:
4425            dlg.Destroy()
4426            return
4427        dlg.Destroy()
4428        print('start regress to '+str(ver))
4429        self.frame.OnFileSave(event)
4430        GPX = self.frame.GSASprojectfile
4431        GSASIIpath.svnUpdateProcess(projectfile=GPX,version=str(ver))
4432        return
4433
4434    # def OnSelectBranch(self,event):
4435    #     '''Allow the user to select branch of GSAS-II or return to trunk
4436    #     N.B. Name of branch to use is hard-coded here. Must contain a slash
4437    #     '''
4438    #     testbranch = '/branch/2frame'
4439    #     if not GSASIIpath.svnTestBranch():
4440    #         dlg = wx.MessageDialog(self.frame,
4441    #                                'Switching to test GSAS-II version',
4442    #                                'Confirm Switch',
4443    #                                wx.OK|wx.CANCEL)
4444    #         if dlg.ShowModal() != wx.ID_OK: return
4445    #         branch = testbranch
4446    #     else:
4447    #         dlg = wx.MessageDialog(self.frame,
4448    #                                'Switching back to standard GSAS-II version',
4449    #                                'Confirm Switch',
4450    #                                wx.OK|wx.CANCEL)
4451    #         if dlg.ShowModal() != wx.ID_OK: return
4452    #         branch = 'trunk'
4453    #     print('start switch')
4454    #     self.frame.OnFileSave(event)
4455    #     GPX = self.frame.GSASprojectfile
4456    #     GSASIIpath.svnUpdateProcess(projectfile=GPX,branch=branch)
4457
4458################################################################################
4459class HelpButton(wx.Button):
4460    '''Create a help button that displays help information.
4461    The text is displayed in a modal message window.
4462
4463    TODO: it might be nice if it were non-modal: e.g. it stays around until
4464    the parent is deleted or the user closes it, but this did not work for
4465    me.
4466
4467    :param parent: the panel which will be the parent of the button
4468    :param str msg: the help text to be displayed
4469    '''
4470    def __init__(self,parent,msg):
4471        if sys.platform == "darwin": 
4472            wx.Button.__init__(self,parent,wx.ID_HELP)
4473        else:
4474            wx.Button.__init__(self,parent,wx.ID_ANY,'?',style=wx.BU_EXACTFIT)
4475        self.Bind(wx.EVT_BUTTON,self._onPress)
4476        self.msg=StripIndents(msg)
4477        self.parent = parent
4478    def _onClose(self,event):
4479        self.dlg.EndModal(wx.ID_CANCEL)
4480    def _onPress(self,event):
4481        'Respond to a button press by displaying the requested text'
4482        #dlg = wx.MessageDialog(self.parent,self.msg,'Help info',wx.OK)
4483        self.dlg = wx.Dialog(self.parent,wx.ID_ANY,'Help information', 
4484                        style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)
4485        #self.dlg.SetBackgroundColour(wx.WHITE)
4486        mainSizer = wx.BoxSizer(wx.VERTICAL)
4487        txt = wx.StaticText(self.dlg,wx.ID_ANY,self.msg)
4488        mainSizer.Add(txt,1,wx.ALL|wx.EXPAND,10)
4489        txt.SetBackgroundColour(wx.WHITE)
4490
4491        btnsizer = wx.BoxSizer(wx.HORIZONTAL)
4492        btn = wx.Button(self.dlg, wx.ID_CLOSE) 
4493        btn.Bind(wx.EVT_BUTTON,self._onClose)
4494        btnsizer.Add(btn)
4495        mainSizer.Add(btnsizer, 0, wx.ALIGN_CENTER|wx.ALL, 5)
4496        self.dlg.SetSizer(mainSizer)
4497        mainSizer.Fit(self.dlg)
4498        self.dlg.CenterOnParent()
4499        self.dlg.ShowModal()
4500        self.dlg.Destroy()
4501################################################################################
4502class MyHtmlPanel(wx.Panel):
4503    '''Defines a panel to display HTML help information, as an alternative to
4504    displaying help information in a web browser.
4505    '''
4506    def __init__(self, frame, newId):
4507        self.frame = frame
4508        wx.Panel.__init__(self, frame, newId)
4509        sizer = wx.BoxSizer(wx.VERTICAL)
4510        back = wx.Button(self, -1, "Back")
4511        back.Bind(wx.EVT_BUTTON, self.OnBack)
4512        self.htmlwin = G2HtmlWindow(self, newId, size=(750,450))
4513        sizer.Add(self.htmlwin, 1,wx.EXPAND)
4514        sizer.Add(back, 0, wx.ALIGN_LEFT, 0)
4515        self.SetSizer(sizer)
4516        sizer.Fit(frame)       
4517        self.Bind(wx.EVT_SIZE,self.OnHelpSize)
4518    def OnHelpSize(self,event):         #does the job but weirdly!!
4519        anchor = self.htmlwin.GetOpenedAnchor()
4520        if anchor:           
4521            self.htmlwin.ScrollToAnchor(anchor)
4522            wx.CallAfter(self.htmlwin.ScrollToAnchor,anchor)
4523            if event: event.Skip()
4524    def OnBack(self, event):
4525        self.htmlwin.HistoryBack()
4526    def LoadFile(self,file):
4527        pos = file.rfind('#')
4528        if pos != -1:
4529            helpfile = file[:pos]
4530            helpanchor = file[pos+1:]
4531        else:
4532            helpfile = file
4533            helpanchor = None
4534        self.htmlwin.LoadPage(helpfile)
4535        if helpanchor is not None:
4536            self.htmlwin.ScrollToAnchor(helpanchor)
4537            xs,ys = self.htmlwin.GetViewStart()
4538            self.htmlwin.Scroll(xs,ys-1)
4539################################################################################
4540class G2HtmlWindow(wx.html.HtmlWindow):
4541    '''Displays help information in a primitive HTML browser type window
4542    '''
4543    def __init__(self, parent, *args, **kwargs):
4544        self.parent = parent
4545        wx.html.HtmlWindow.__init__(self, parent, *args, **kwargs)
4546    def LoadPage(self, *args, **kwargs):
4547        wx.html.HtmlWindow.LoadPage(self, *args, **kwargs)
4548        self.TitlePage()
4549    def OnLinkClicked(self, *args, **kwargs):
4550        wx.html.HtmlWindow.OnLinkClicked(self, *args, **kwargs)
4551        xs,ys = self.GetViewStart()
4552        self.Scroll(xs,ys-1)
4553        self.TitlePage()
4554    def HistoryBack(self, *args, **kwargs):
4555        wx.html.HtmlWindow.HistoryBack(self, *args, **kwargs)
4556        self.TitlePage()
4557    def TitlePage(self):
4558        self.parent.frame.SetTitle(self.GetOpenedPage() + ' -- ' + 
4559            self.GetOpenedPageTitle())
4560
4561################################################################################
4562def StripIndents(msg,singleLine=False):
4563    'Strip indentation from multiline strings'
4564    msg1 = msg.replace('\n ','\n')
4565    while msg != msg1:
4566        msg = msg1
4567        msg1 = msg.replace('\n ','\n')
4568    msg = msg.replace('\n\t','\n')
4569    if singleLine:
4570        return msg.replace('\n',' ')
4571    return msg
4572
4573def StripUnicode(string,subs='.'):
4574    '''Strip non-ASCII characters from strings
4575   
4576    :param str string: string to strip Unicode characters from
4577    :param str subs: character(s) to place into string in place of each
4578      Unicode character. Defaults to '.'
4579
4580    :returns: a new string with only ASCII characters
4581    '''
4582    s = ''
4583    for c in string:
4584        if ord(c) < 128:
4585            s += c
4586        else:
4587            s += subs
4588    return s.encode('ascii','replace')
4589       
4590################################################################################
4591# configuration routines (for editing config.py)
4592def SaveGPXdirectory(path):
4593    if GSASIIpath.GetConfigValue('Starting_directory') == path: return
4594    vars = GetConfigValsDocs()
4595    try:
4596        vars['Starting_directory'][1] = path
4597        if GSASIIpath.GetConfigValue('debug'): print('DBG_Saving GPX path: '+path)
4598        SaveConfigVars(vars)
4599    except KeyError:
4600        pass
4601
4602def SaveImportDirectory(path):
4603    if GSASIIpath.GetConfigValue('Import_directory') == path: return
4604    vars = GetConfigValsDocs()
4605    try:
4606        vars['Import_directory'][1] = path
4607        if GSASIIpath.GetConfigValue('debug'): print('DBG_Saving Import path: '+path)
4608        SaveConfigVars(vars)
4609    except KeyError:
4610        pass
4611
4612def GetConfigValsDocs():
4613    '''Reads the module referenced in fname (often <module>.__file__) and
4614    return a dict with names of global variables as keys.
4615    For each global variable, the value contains four items:
4616
4617    :returns: a dict where keys are names defined in module config_example.py
4618      where the value is a list of four items, as follows:
4619
4620         * item 0: the default value
4621         * item 1: the current value
4622         * item 2: the initial value (starts same as item 1)
4623         * item 3: the "docstring" that follows variable definition
4624
4625    '''
4626    import config_example
4627    import ast
4628    fname = os.path.splitext(config_example.__file__)[0]+'.py' # convert .pyc to .py
4629    if '3' in platform.python_version_tuple()[0]: 
4630        with open(fname, 'r',encoding='utf-8') as f:
4631            fstr = f.read()
4632    else:
4633        with open(fname, 'r') as f:
4634            fstr = f.read()
4635    fstr = fstr.replace('\r\n', '\n').replace('\r', '\n')
4636    if not fstr.endswith('\n'):
4637        fstr += '\n'
4638    tree = ast.parse(fstr)
4639    d = {}
4640    key = None
4641    for node in ast.walk(tree):
4642        if isinstance(node,ast.Assign):
4643            key = node.targets[0].id
4644            d[key] = [config_example.__dict__.get(key),
4645                      GSASIIpath.configDict.get(key),
4646                      GSASIIpath.configDict.get(key),'']
4647        elif isinstance(node,ast.Expr) and key:
4648            d[key][3] = node.value.s.strip()
4649        else:
4650            key = None
4651    return d
4652
4653def SaveConfigVars(vars,parent=None):
4654    '''Write the current config variable values to config.py
4655
4656    :params dict vars: a dictionary of variable settings and meanings as
4657      created in :func:`GetConfigValsDocs`.
4658    :param parent: wx.Frame object or None (default) for parent
4659      of error message if no file can be written.
4660    :returns: True if unable to write the file, None otherwise
4661    '''
4662    # try to write to where an old config file is located
4663    try:
4664        import config
4665        savefile = config.__file__
4666    except ImportError: # no config.py file yet
4667        savefile = os.path.join(GSASIIpath.path2GSAS2,'config.py')
4668    # try to open file for write
4669    try:
4670        savefile = os.path.splitext(savefile)[0]+'.py' # convert .pyc to .py
4671        fp = open(savefile,'w')
4672    except IOError:  # can't write there, write in local mods directory
4673        # create a local mods directory, if needed
4674        g2local = os.path.expanduser('~/.G2local/')
4675        if not os.path.exists(g2local):
4676            try:
4677                print(u'Creating directory '+g2local)
4678                os.mkdir(g2local)
4679            except:
4680                if parent:
4681                    G2MessageBox(parent,u'Error trying to create directory '+g2local,
4682                        'Unable to save')
4683                else:
4684                    print(u'Error trying to create directory '+g2local)
4685                return True
4686            sys.path.insert(0,os.path.expanduser('~/.G2local/'))
4687        savefile = os.path.join(os.path.expanduser('~/.G2local/'),'config.py')
4688        try:
4689            fp = open(savefile,'w')
4690        except IOError:
4691            if parent:
4692                G2MessageBox(parent,'Error trying to write configuration to '+savefile,
4693                    'Unable to save')
4694            else:
4695                print('Error trying to write configuration to '+savefile)
4696            return True
4697    import datetime
4698    fp.write("'''\n")
4699    fp.write("*config.py: Configuration options*\n----------------------------------\n")
4700    fp.write("This file created in SelectConfigSetting on {:%d %b %Y %H:%M}\n".
4701             format(datetime.datetime.now()))
4702    fp.write("'''\n\n")
4703    fp.write("import os.path\n")
4704    fp.write("import GSASIIpath\n\n")
4705    for var in sorted(vars.keys(),key=lambda s: s.lower()):
4706        if vars[var][1] is None: continue
4707        if vars[var][1] == '': continue
4708        if vars[var][0] == vars[var][1]: continue
4709        try:
4710            float(vars[var][1]) # test for number
4711            fp.write(var + ' = ' + str(vars[var][1])+'\n')
4712        except:
4713            try:
4714                eval(vars[var][1]) # test for an expression
4715                fp.write(var + ' = ' + str(vars[var][1])+'\n')
4716            except: # must be a string
4717                varstr = vars[var][1]
4718                if '\\' in varstr:
4719                    fp.write(var + ' = os.path.normpath("' + varstr.replace('\\','/') +'")\n')
4720                else:
4721                    fp.write(var + ' = "' + str(varstr)+'"\n')
4722        if vars[var][3]:
4723            fp.write("'''" + str(vars[var][3]) + "\n'''\n\n")
4724    fp.close()
4725    print('wrote file '+savefile)
4726
4727class SelectConfigSetting(wx.Dialog):
4728    '''Dialog to select configuration variables and set associated values.
4729    '''
4730    def __init__(self,parent=None):
4731        style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER
4732        wx.Dialog.__init__(self, parent, wx.ID_ANY, 'Set Config Variable', style=style)
4733        self.sizer = wx.BoxSizer(wx.VERTICAL)
4734        self.vars = GetConfigValsDocs()
4735       
4736        label = wx.StaticText(
4737            self,  wx.ID_ANY,
4738            'Select a GSAS-II configuration variable to change'
4739            )
4740        self.sizer.Add(label, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
4741        self.choice = {}
4742        btn = G2ChoiceButton(self, sorted(self.vars.keys(),key=lambda s: s.lower()),
4743            strLoc=self.choice,strKey=0,onChoice=self.OnSelection)
4744        btn.SetLabel("")
4745        self.sizer.Add(btn)
4746
4747        self.varsizer = wx.BoxSizer(wx.VERTICAL)
4748        self.sizer.Add(self.varsizer,1,wx.ALL|wx.EXPAND,1)
4749       
4750        self.doclbl = wx.StaticBox(self, wx.ID_ANY, "")
4751        self.doclblsizr = wx.StaticBoxSizer(self.doclbl)
4752        self.docinfo = wx.StaticText(self,  wx.ID_ANY, "")
4753        self.doclblsizr.Add(self.docinfo, 0, wx.ALIGN_LEFT|wx.ALL, 5)
4754        self.sizer.Add(self.doclblsizr, 0, wx.ALIGN_LEFT|wx.EXPAND|wx.ALL, 5)
4755        btnsizer = wx.BoxSizer(wx.HORIZONTAL)
4756        self.saveBtn = wx.Button(self,-1,"Save current settings")
4757        btnsizer.Add(self.saveBtn, 0, wx.ALL, 2) 
4758        self.saveBtn.Bind(wx.EVT_BUTTON, self.OnSave)
4759        self.saveBtn.Enable(False)
4760        self.applyBtn = wx.Button(self,-1,"Use current (no save)")
4761        btnsizer.Add(self.applyBtn, 0, wx.ALL, 2) 
4762        self.applyBtn.Bind(wx.EVT_BUTTON, self.OnApplyChanges)
4763        self.applyBtn.Enable(False)
4764       
4765        btn = wx.Button(self,wx.ID_CANCEL)
4766        btnsizer.Add(btn, 0, wx.ALL, 2) 
4767        self.sizer.Add(btnsizer, 0, wx.ALIGN_CENTRE|wx.ALL, 5) 
4768               
4769        self.SetSizer(self.sizer)
4770        self.sizer.Fit(self)
4771        self.CenterOnParent()
4772       
4773    def OnChange(self,event=None):
4774        ''' Check if anything been changed. Turn the save button on/off.
4775        '''
4776        for var in self.vars:
4777            if self.vars[var][0] is None and self.vars[var][1] is not None:
4778                # make blank strings into None, if that is the default
4779                if self.vars[var][1].strip() == '': self.vars[var][1] = None
4780            if self.vars[var][1] != self.vars[var][2]:
4781                #print 'changed',var,self.vars[var][:3]
4782                self.saveBtn.Enable(True)
4783                self.applyBtn.Enable(True)
4784                break
4785        else:
4786            self.saveBtn.Enable(False)
4787            self.applyBtn.Enable(False)
4788        try:
4789            self.resetBtn.Enable(True)
4790        except:
4791            pass
4792       
4793    def OnApplyChanges(self,event=None):
4794        'Set config variables to match the current settings'
4795        GSASIIpath.SetConfigValue(self.vars)
4796        self.EndModal(wx.ID_OK)
4797        import GSASIImpsubs as G2mp
4798        G2mp.ResetMP()
4799       
4800    def OnSave(self,event):
4801        '''Write the config variables to config.py and then set them
4802        as the current settings
4803        '''
4804        if not SaveConfigVars(self.vars,parent=self):
4805            self.OnApplyChanges() # force a reload of the config settings
4806        else:
4807            self.EndModal(wx.ID_OK)
4808
4809    def OnBoolSelect(self,event):
4810        'Respond to a change in a True/False variable'
4811        rb = event.GetEventObject()
4812        var = self.choice[0]
4813        self.vars[var][1] = (rb.GetSelection() == 0)
4814        self.OnChange()
4815        wx.CallAfter(self.OnSelection)
4816       
4817    def onSelDir(self,event):
4818        'Select a directory from a menu'
4819        dlg = wx.DirDialog(self, "Choose a directory:",style=wx.DD_DEFAULT_STYLE)
4820        if dlg.ShowModal() == wx.ID_OK:
4821            var = self.choice[0]
4822            self.vars[var][1] = dlg.GetPath()
4823            self.strEd.SetValue(self.vars[var][1])
4824            self.OnChange()
4825        dlg.Destroy()
4826       
4827    def OnSelection(self):
4828        'show a selected variable'
4829        def OnNewColorBar(event):
4830            self.vars['Contour_color'][1] = self.colSel.GetValue()
4831            self.OnChange(event)
4832
4833        if 'phoenix' in wx.version():
4834            self.varsizer.Clear(True)
4835        else:
4836            self.varsizer.DeleteWindows()
4837        var = self.choice[0]
4838        showdef = True
4839        if var not in self.vars:
4840            raise Exception("How did this happen?")
4841        if type(self.vars[var][0]) is int:
4842            ed = ValidatedTxtCtrl(self,self.vars[var],1,typeHint=int,OKcontrol=self.OnChange)
4843            self.varsizer.Add(ed, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
4844        elif type(self.vars[var][0]) is float:
4845            ed = ValidatedTxtCtrl(self,self.vars[var],1,typeHint=float,OKcontrol=self.OnChange)
4846            self.varsizer.Add(ed, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
4847        elif type(self.vars[var][0]) is bool:
4848            showdef = False
4849            lbl = "value for "+var
4850            ch = []
4851            for i,v in enumerate((True,False)):
4852                s = str(v)
4853                if v == self.vars[var][0]:
4854                    defopt = i
4855                    s += ' (default)'
4856                ch += [s]
4857            rb = wx.RadioBox(self, wx.ID_ANY, lbl, wx.DefaultPosition, wx.DefaultSize,
4858                ch, 1, wx.RA_SPECIFY_COLS)
4859            # set initial value
4860            if self.vars[var][1] is None:
4861                rb.SetSelection(defopt)
4862            elif self.vars[var][1]:
4863                rb.SetSelection(0)
4864            else:
4865                rb.SetSelection(1)
4866            rb.Bind(wx.EVT_RADIOBOX,self.OnBoolSelect)
4867            self.varsizer.Add(rb, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
4868        else:
4869            if var.endswith('_directory') or var.endswith('_location'):
4870                btn = wx.Button(self,wx.ID_ANY,'Select from dialog...')
4871                sz = (400,-1)
4872            else:
4873                btn = None
4874                sz = (250,-1)
4875            if var == 'Contour_color':
4876                if self.vars[var][1] is None:
4877                    self.vars[var][1] = 'Paired'
4878                colorList = sorted([m for m in mpl.cm.datad.keys() ],key=lambda s: s.lower())   #if not m.endswith("_r")
4879                self.colSel = wx.ComboBox(self,value=self.vars[var][1],choices=colorList,
4880                    style=wx.CB_READONLY|wx.CB_DROPDOWN)
4881                self.colSel.Bind(wx.EVT_COMBOBOX, OnNewColorBar)
4882                self.varsizer.Add(self.colSel, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
4883            else:
4884                self.strEd = ValidatedTxtCtrl(self,self.vars[var],1,typeHint=str,
4885                    OKcontrol=self.OnChange,size=sz)
4886                if self.vars[var][1] is not None:
4887                    self.strEd.SetValue(self.vars[var][1])
4888                self.varsizer.Add(self.strEd, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
4889            if btn:
4890                btn.Bind(wx.EVT_BUTTON,self.onSelDir)
4891                self.varsizer.Add(btn, 0, wx.ALIGN_CENTRE|wx.ALL, 5) 
4892        # button for reset to default value
4893        lbl = "Reset to Default"
4894        if showdef: # spell out default when needed
4895            lbl += ' (='+str(self.vars[var][0])+')'
4896            #label = wx.StaticText(self,  wx.ID_ANY, 'Default value = '+str(self.vars[var][0]))
4897            #self.varsizer.Add(label, 0, wx.ALIGN_LEFT|wx.ALL, 5)
4898        self.resetBtn = wx.Button(self,-1,lbl)
4899        self.resetBtn.Bind(wx.EVT_BUTTON, self.OnClear)
4900        if self.vars[var][1] is not None and self.vars[var][1] != '': # show current value, if one
4901            #label = wx.StaticText(self,  wx.ID_ANY, 'Current value = '+str(self.vars[var][1]))
4902            #self.varsizer.Add(label, 0, wx.ALIGN_LEFT|wx.ALL, 5)
4903            self.resetBtn.Enable(True)
4904        else:
4905            self.resetBtn.Enable(False)
4906        self.varsizer.Add(self.resetBtn, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
4907        # show meaning, if defined
4908        self.doclbl.SetLabel("Description of "+str(var)) 
4909        if self.vars[var][3]:
4910            self.docinfo.SetLabel(self.vars[var][3])
4911        else:
4912            self.docinfo.SetLabel("(not documented)")
4913        self.sizer.Fit(self)
4914        self.CenterOnParent()
4915        wx.CallAfter(self.SendSizeEvent)
4916
4917    def OnClear(self, event):
4918        var = self.choice[0]
4919        self.vars[var][1] = self.vars[var][0]
4920        self.OnChange()
4921        wx.CallAfter(self.OnSelection)
4922       
4923################################################################################
4924class downdate(wx.Dialog):
4925    '''Dialog to allow a user to select a version of GSAS-II to install
4926    '''
4927    def __init__(self,parent=None):
4928        style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER
4929        wx.Dialog.__init__(self, parent, wx.ID_ANY, 'Select Version', style=style)
4930        pnl = wx.Panel(self)
4931        sizer = wx.BoxSizer(wx.VERTICAL)
4932        insver = GSASIIpath.svnGetRev(local=True)
4933        curver = int(GSASIIpath.svnGetRev(local=False))
4934        label = wx.StaticText(
4935            pnl,  wx.ID_ANY,
4936            'Select a specific GSAS-II version to install'
4937            )
4938        sizer.Add(label, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
4939        sizer1 = wx.BoxSizer(wx.HORIZONTAL)
4940        sizer1.Add(
4941            wx.StaticText(pnl,  wx.ID_ANY,
4942                          'Currently installed version: '+str(insver)),
4943            0, wx.ALIGN_CENTRE|wx.ALL, 5)
4944        sizer.Add(sizer1)
4945        sizer1 = wx.BoxSizer(wx.HORIZONTAL)
4946        sizer1.Add(
4947            wx.StaticText(pnl,  wx.ID_ANY,
4948                          'Select GSAS-II version to install: '),
4949            0, wx.ALIGN_CENTRE|wx.ALL, 5)
4950        self.spin = wx.SpinCtrl(pnl, wx.ID_ANY,size=(150,-1))
4951        self.spin.SetRange(1, curver)
4952        self.spin.SetValue(curver)
4953        self.Bind(wx.EVT_SPINCTRL, self._onSpin, self.spin)
4954        self.Bind(wx.EVT_KILL_FOCUS, self._onSpin, self.spin)
4955        sizer1.Add(self.spin)
4956        sizer.Add(sizer1)
4957
4958        line = wx.StaticLine(pnl,-1, size=(-1,3), style=wx.LI_HORIZONTAL)
4959        sizer.Add(line, 0, wx.EXPAND|wx.ALIGN_CENTER|wx.ALL, 10)
4960
4961        self.text = wx.StaticText(pnl,  wx.ID_ANY, "")
4962        sizer.Add(self.text, 0, wx.ALIGN_LEFT|wx.EXPAND|wx.ALL, 5)
4963
4964        line = wx.StaticLine(pnl,-1, size=(-1,3), style=wx.LI_HORIZONTAL)
4965        sizer.Add(line, 0, wx.EXPAND|wx.ALIGN_CENTER|wx.ALL, 10)
4966        sizer.Add(
4967            wx.StaticText(
4968                pnl,  wx.ID_ANY,
4969                'If "Install" is pressed, your project will be saved;\n'
4970                'GSAS-II will exit; The specified version will be loaded\n'
4971                'and GSAS-II will restart. Press "Cancel" to abort.'),
4972            0, wx.EXPAND|wx.ALL, 10<