source: trunk/GSASIIctrlGUI.py @ 3743

Last change on this file since 3743 was 3737, checked in by vondreele, 7 years ago

reorder descriptions in front of G2ctrlGUI to alpha order - now easier to find stuff & add a couple more to list
use copy.deepcopy on PDF controls inside autointegrate
fixes to supersymmetry stuff & magnetic stuff to get correct cell multiplicities etc.
A fix t G2strIO to make ssymetry refinements work

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