source: trunk/GSASIIctrlGUI.py @ 3209

Last change on this file since 3209 was 3209, checked in by toby, 4 years ago

Add simulation to scriptable & add to tutorial; add sphinx index to ctrlsGUI; remove path from RunGSASII.bat

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