source: trunk/GSASIIctrlGUI.py @ 3485

Last change on this file since 3485 was 3485, checked in by vondreele, 3 years ago

made suggested change from Jeremy Kropf for reading in multiple peak lists.

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