source: trunk/GSASIIctrlGUI.py @ 3452

Last change on this file since 3452 was 3452, checked in by vondreele, 4 years ago

Add 'C -1' as possible Bravais lattice/space group for Unit cell GUI
fix odd business with basestring in py3. Code failed; just use basestring fine in py3

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