source: trunk/GSASIIctrlGUI.py @ 3445

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

wx phoenix fixes to G2ctrls for tooltip & sequential table motion binds to show tooltips for esds, etc.
replace SelectItem? with SelectDataTreeItem? for seq table after seqrefine finished. Works now.

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