source: trunk/GSASIIctrlGUI.py @ 3871

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

redo G2ColumnIDDialog to resize vertically

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