source: trunk/GSASIIctrlGUI.py @ 3442

Last change on this file since 3442 was 3442, checked in by vondreele, 5 years ago

Install new tutorial - Simple Magnetic structures

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