source: branch/2frame/GSASIIctrlGUI.py @ 2950

Last change on this file since 2950 was 2950, checked in by vondreele, 4 years ago

oops - forgot the SetSizer? in SetPhaseWindow?
make SetScrollRate?(0,0) the default for GSGrid - grids have no scroll bars
remove all those individual SetScrollRates? everywhere a GSGrid is made - all did same thing.

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