source: trunk/GSASIIctrlGUI.py @ 3780

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

fix typos; error in CIF Dij reporting; improve generalized restraint GUI

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