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

Last change on this file since 2927 was 2924, checked in by toby, 8 years ago

prevent load of binaries during update

  • Property svn:eol-style set to native
  • Property svn:keywords set to Date Author Revision URL Id
File size: 200.8 KB
Line 
1# -*- coding: utf-8 -*-
2#GSASIIctrlGUI - Custom GSAS-II GUI controls
3########### SVN repository information ###################
4# $Date: 2017-07-08 20:42:02 +0000 (Sat, 08 Jul 2017) $
5# $Author: vondreele $
6# $Revision: 2924 $
7# $URL: branch/2frame/GSASIIctrlGUI.py $
8# $Id: GSASIIctrlGUI.py 2924 2017-07-08 20:42:02Z 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: 2924 $")
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           
3112    def Clear(self):
3113        wg.Grid.ClearGrid(self)
3114       
3115    def SetCellReadOnly(self,r,c,readonly=True):
3116        self.SetReadOnly(r,c,isReadOnly=readonly)
3117       
3118    def SetCellStyle(self,r,c,color="white",readonly=True):
3119        self.SetCellBackgroundColour(r,c,color)
3120        self.SetReadOnly(r,c,isReadOnly=readonly)
3121       
3122    def GetSelection(self):
3123        #this is to satisfy structure drawing stuff in G2plt when focus changes
3124        return None
3125
3126    def InstallGridToolTip(self, rowcolhintcallback,
3127                           colLblCallback=None,rowLblCallback=None):
3128        '''code to display a tooltip for each item on a grid
3129        from http://wiki.wxpython.org/wxGrid%20ToolTips (buggy!), expanded to
3130        column and row labels using hints from
3131        https://groups.google.com/forum/#!topic/wxPython-users/bm8OARRVDCs
3132
3133        :param function rowcolhintcallback: a routine that returns a text
3134          string depending on the selected row and column, to be used in
3135          explaining grid entries.
3136        :param function colLblCallback: a routine that returns a text
3137          string depending on the selected column, to be used in
3138          explaining grid columns (if None, the default), column labels
3139          do not get a tooltip.
3140        :param function rowLblCallback: a routine that returns a text
3141          string depending on the selected row, to be used in
3142          explaining grid rows (if None, the default), row labels
3143          do not get a tooltip.
3144        '''
3145        prev_rowcol = [None,None,None]
3146        def OnMouseMotion(event):
3147            # event.GetRow() and event.GetCol() would be nice to have here,
3148            # but as this is a mouse event, not a grid event, they are not
3149            # available and we need to compute them by hand.
3150            x, y = self.CalcUnscrolledPosition(event.GetPosition())
3151            row = self.YToRow(y)
3152            col = self.XToCol(x)
3153            hinttext = ''
3154            win = event.GetEventObject()
3155            if [row,col,win] == prev_rowcol: # no change from last position
3156                if event: event.Skip()
3157                return
3158            if win == self.GetGridWindow() and row >= 0 and col >= 0:
3159                hinttext = rowcolhintcallback(row, col)
3160            elif win == self.GetGridColLabelWindow() and col >= 0:
3161                if colLblCallback: hinttext = colLblCallback(col)
3162            elif win == self.GetGridRowLabelWindow() and row >= 0:
3163                if rowLblCallback: hinttext = rowLblCallback(row)
3164            else: # this should be the upper left corner, which is empty
3165                if event: event.Skip()
3166                return
3167            if hinttext is None: hinttext = ''
3168            win.SetToolTipString(hinttext)
3169            prev_rowcol[:] = [row,col,win]
3170            if event: event.Skip()
3171
3172        wx.EVT_MOTION(self.GetGridWindow(), OnMouseMotion)
3173        if colLblCallback: wx.EVT_MOTION(self.GetGridColLabelWindow(), OnMouseMotion)
3174        if rowLblCallback: wx.EVT_MOTION(self.GetGridRowLabelWindow(), OnMouseMotion)
3175                                                   
3176################################################################################           
3177class Table(wg.PyGridTableBase):
3178    '''Basic data table for use with GSgrid
3179    '''
3180    def __init__(self, data=[], rowLabels=None, colLabels=None, types = None):
3181        wg.PyGridTableBase.__init__(self)
3182        self.colLabels = colLabels
3183        self.rowLabels = rowLabels
3184        self.dataTypes = types
3185        self.data = data
3186       
3187    def AppendRows(self, numRows=1):
3188        self.data.append([])
3189        return True
3190       
3191    def CanGetValueAs(self, row, col, typeName):
3192        if self.dataTypes:
3193            colType = self.dataTypes[col].split(':')[0]
3194            if typeName == colType:
3195                return True
3196            else:
3197                return False
3198        else:
3199            return False
3200
3201    def CanSetValueAs(self, row, col, typeName):
3202        return self.CanGetValueAs(row, col, typeName)
3203
3204    def DeleteRow(self,pos):
3205        data = self.GetData()
3206        self.SetData([])
3207        new = []
3208        for irow,row in enumerate(data):
3209            if irow <> pos:
3210                new.append(row)
3211        self.SetData(new)
3212       
3213    def GetColLabelValue(self, col):
3214        if self.colLabels:
3215            return self.colLabels[col]
3216           
3217    def GetData(self):
3218        data = []
3219        for row in range(self.GetNumberRows()):
3220            data.append(self.GetRowValues(row))
3221        return data
3222       
3223    def GetNumberCols(self):
3224        try:
3225            return len(self.colLabels)
3226        except TypeError:
3227            return None
3228       
3229    def GetNumberRows(self):
3230        return len(self.data)
3231       
3232    def GetRowLabelValue(self, row):
3233        if self.rowLabels:
3234            return self.rowLabels[row]
3235       
3236    def GetColValues(self, col):
3237        data = []
3238        for row in range(self.GetNumberRows()):
3239            data.append(self.GetValue(row, col))
3240        return data
3241       
3242    def GetRowValues(self, row):
3243        data = []
3244        for col in range(self.GetNumberCols()):
3245            data.append(self.GetValue(row, col))
3246        return data
3247       
3248    def GetTypeName(self, row, col):
3249        try:
3250            if self.data[row][col] is None: return None
3251            return self.dataTypes[col]
3252        except (TypeError,IndexError):
3253            return None
3254
3255    def GetValue(self, row, col):
3256        try:
3257            if self.data[row][col] is None: return ""
3258            return self.data[row][col]
3259        except IndexError:
3260            return None
3261           
3262    def InsertRows(self, pos, rows):
3263        for row in range(rows):
3264            self.data.insert(pos,[])
3265            pos += 1
3266       
3267    def IsEmptyCell(self,row,col):
3268        try:
3269            return not self.data[row][col]
3270        except IndexError:
3271            return True
3272       
3273    def OnKeyPress(self, event):
3274        dellist = self.GetSelectedRows()
3275        if event.GetKeyCode() == wx.WXK_DELETE and dellist:
3276            grid = self.GetView()
3277            for i in dellist: grid.DeleteRow(i)
3278               
3279    def SetColLabelValue(self, col, label):
3280        numcols = self.GetNumberCols()
3281        if col > numcols-1:
3282            self.colLabels.append(label)
3283        else:
3284            self.colLabels[col]=label
3285       
3286    def SetData(self,data):
3287        for row in range(len(data)):
3288            self.SetRowValues(row,data[row])
3289               
3290    def SetRowLabelValue(self, row, label):
3291        self.rowLabels[row]=label
3292           
3293    def SetRowValues(self,row,data):
3294        self.data[row] = data
3295           
3296    def SetValue(self, row, col, value):
3297        def innerSetValue(row, col, value):
3298            try:
3299                self.data[row][col] = value
3300            except TypeError:
3301                return
3302            except IndexError: # has this been tested?
3303                #print row,col,value
3304                # add a new row
3305                if row > self.GetNumberRows():
3306                    self.data.append([''] * self.GetNumberCols())
3307                elif col > self.GetNumberCols():
3308                    for row in range(self.GetNumberRows()): # bug fixed here
3309                        self.data[row].append('')
3310                #print self.data
3311                self.data[row][col] = value
3312        innerSetValue(row, col, value)
3313
3314################################################################################
3315class GridFractionEditor(wg.PyGridCellEditor):
3316    '''A grid cell editor class that allows entry of values as fractions as well
3317    as sine and cosine values [as s() and c()]
3318    '''
3319    def __init__(self,grid):
3320        wg.PyGridCellEditor.__init__(self)
3321
3322    def Create(self, parent, id, evtHandler):
3323        self._tc = wx.TextCtrl(parent, id, "")
3324        self._tc.SetInsertionPoint(0)
3325        self.SetControl(self._tc)
3326
3327        if evtHandler:
3328            self._tc.PushEventHandler(evtHandler)
3329
3330        self._tc.Bind(wx.EVT_CHAR, self.OnChar)
3331
3332    def SetSize(self, rect):
3333        self._tc.SetDimensions(rect.x, rect.y, rect.width+2, rect.height+2,
3334                               wx.SIZE_ALLOW_MINUS_ONE)
3335
3336    def BeginEdit(self, row, col, grid):
3337        self.startValue = grid.GetTable().GetValue(row, col)
3338        self._tc.SetValue(str(self.startValue))
3339        self._tc.SetInsertionPointEnd()
3340        self._tc.SetFocus()
3341        self._tc.SetSelection(0, self._tc.GetLastPosition())
3342
3343    def EndEdit(self, row, col, grid, oldVal=None):
3344        changed = False
3345
3346        self.nextval = self.startValue
3347        val = self._tc.GetValue().lower().strip()
3348        if val != self.startValue:
3349            changed = True
3350            neg = False
3351            if val.startswith('-'):
3352                neg = True
3353                val = val[1:]
3354            # allow old GSAS s20 and c20 etc for sind(20) and cosd(20)
3355            if val.startswith('s') and '(' not in val:
3356                val = 'sind('+val.strip('s')+')'
3357            elif val.startswith('c') and '(' not in val:
3358                val = 'cosd('+val.strip('c')+')'
3359            if neg:
3360                val = '-' + val
3361            val = G2py3.FormulaEval(val)
3362            if val is not None:
3363                self.nextval = val
3364            else:
3365                return None
3366            if oldVal is None: # this arg appears in 2.9+; before, we should go ahead & change the table
3367                grid.GetTable().SetValue(row, col, val) # update the table
3368            # otherwise self.ApplyEdit gets called
3369
3370        self.startValue = ''
3371        self._tc.SetValue('')
3372        return changed
3373   
3374    def ApplyEdit(self, row, col, grid):
3375        """ Called only in wx >= 2.9
3376        Save the value of the control into the grid if EndEdit() returns as True
3377        """
3378        grid.GetTable().SetValue(row, col, self.nextval) # update the table
3379
3380    def Reset(self):
3381        self._tc.SetValue(self.startValue)
3382        self._tc.SetInsertionPointEnd()
3383
3384    def Clone(self,grid):
3385        return GridFractionEditor(grid)
3386
3387    def StartingKey(self, evt):
3388        self.OnChar(evt)
3389        if evt.GetSkipped():
3390            self._tc.EmulateKeyPress(evt)
3391
3392    def OnChar(self, evt):
3393        key = evt.GetKeyCode()
3394        if key < 32 or key >= 127:
3395            evt.Skip()
3396        elif chr(key).lower() in '.+-*/0123456789cosind()':
3397            evt.Skip()
3398        else:
3399            evt.StopPropagation()
3400           
3401################################################################################
3402#####  Customized Notebook
3403################################################################################           
3404class GSNoteBook(wx.aui.AuiNotebook):
3405    '''Notebook used in various locations; implemented with wx.aui extension
3406    '''
3407    def __init__(self, parent, name='',size = None,style=wx.aui.AUI_NB_TOP |
3408        wx.aui.AUI_NB_SCROLL_BUTTONS):
3409        wx.aui.AuiNotebook.__init__(self, parent, style=style)
3410        if size: self.SetSize(size)
3411        self.parent = parent
3412        self.PageChangeHandler = None
3413       
3414    def PageChangeEvent(self,event):
3415        pass
3416#        G2frame = self.parent.G2frame
3417#        page = event.GetSelection()
3418#        if self.PageChangeHandler:
3419#            if log.LogInfo['Logging']:
3420#                log.MakeTabLog(
3421#                    G2frame.dataWindow.GetTitle(),
3422#                    G2frame.dataDisplay.GetPageText(page)
3423#                    )
3424#            self.PageChangeHandler(event)
3425           
3426#    def Bind(self,eventtype,handler,*args,**kwargs):
3427#        '''Override the Bind() function so that page change events can be trapped
3428#        '''
3429#        if eventtype == wx.aui.EVT_AUINOTEBOOK_PAGE_CHANGED:
3430#            self.PageChangeHandler = handler
3431#            wx.aui.AuiNotebook.Bind(self,eventtype,self.PageChangeEvent)
3432#            return
3433#        wx.aui.AuiNotebook.Bind(self,eventtype,handler,*args,**kwargs)
3434                                                     
3435    def Clear(self):       
3436        GSNoteBook.DeleteAllPages(self)
3437       
3438    def FindPage(self,name):
3439        numPage = self.GetPageCount()
3440        for page in range(numPage):
3441            if self.GetPageText(page) == name:
3442                return page
3443
3444    def ChangeSelection(self,page):
3445        # in wx.Notebook ChangeSelection is like SetSelection, but it
3446        # does not invoke the event related to pressing the tab button
3447        # I don't see a way to do that in aui.
3448        oldPage = self.GetSelection()
3449        self.SetSelection(page)
3450        return oldPage
3451
3452    # def __getattribute__(self,name):
3453    #     '''This method provides a way to print out a message every time
3454    #     that a method in a class is called -- to see what all the calls
3455    #     might be, or where they might be coming from.
3456    #     Cute trick for debugging!
3457    #     '''
3458    #     attr = object.__getattribute__(self, name)
3459    #     if hasattr(attr, '__call__'):
3460    #         def newfunc(*args, **kwargs):
3461    #             print('GSauiNoteBook calling %s' %attr.__name__)
3462    #             result = attr(*args, **kwargs)
3463    #             return result
3464    #         return newfunc
3465    #     else:
3466    #         return attr
3467           
3468################################################################################
3469#### Help support routines
3470################################################################################
3471class MyHelp(wx.Menu):
3472    '''
3473    A class that creates the contents of a help menu.
3474    The menu will start with two entries:
3475
3476    * 'Help on <helpType>': where helpType is a reference to an HTML page to
3477      be opened
3478    * About: opens an About dialog using OnHelpAbout. N.B. on the Mac this
3479      gets moved to the App menu to be consistent with Apple style.
3480
3481    NOTE: for this to work properly with respect to system menus, the title
3482    for the menu must be &Help, or it will not be processed properly:
3483
3484    ::
3485
3486       menu.Append(menu=MyHelp(self,...),title="&Help")
3487
3488    '''
3489    def __init__(self,frame,includeTree=False,morehelpitems=[]):
3490        wx.Menu.__init__(self,'')
3491        self.HelpById = {}
3492        self.frame = frame
3493        self.Append(help='', id=wx.ID_ABOUT, kind=wx.ITEM_NORMAL,
3494            text='&About GSAS-II')
3495        frame.Bind(wx.EVT_MENU, self.OnHelpAbout, id=wx.ID_ABOUT)
3496        if GSASIIpath.whichsvn():
3497            helpobj = self.Append(
3498                help='', id=wx.ID_ANY, kind=wx.ITEM_NORMAL,
3499                text='&Check for updates')
3500            frame.Bind(wx.EVT_MENU, self.OnCheckUpdates, helpobj)
3501            helpobj = self.Append(
3502                help='', id=wx.ID_ANY, kind=wx.ITEM_NORMAL,
3503                text='&Regress to an old GSAS-II version')
3504            frame.Bind(wx.EVT_MENU, self.OnSelectVersion, helpobj)
3505            if GSASIIpath.svnTestBranch():
3506                msg = "&Switch back to standard GSAS-II version"
3507            else:
3508                msg = "&Switch to test (2frame) GSAS-II version"
3509            helpobj = self.Append(
3510                help='', id=wx.ID_ANY, kind=wx.ITEM_NORMAL,text=msg)
3511            frame.Bind(wx.EVT_MENU, self.OnSelectBranch, helpobj)
3512        # provide special help topic names for extra items in help menu
3513        for lbl,indx in morehelpitems:
3514            helpobj = self.Append(text=lbl,
3515                id=wx.ID_ANY, kind=wx.ITEM_NORMAL)
3516            frame.Bind(wx.EVT_MENU, self.OnHelpById, helpobj)
3517            self.HelpById[helpobj.GetId()] = indx
3518        # add help lookup(s) in gsasii.html
3519        self.AppendSeparator()
3520        if includeTree:
3521            helpobj = self.Append(text='Help on Data tree',
3522                                  id=wx.ID_ANY, kind=wx.ITEM_NORMAL)
3523            frame.Bind(wx.EVT_MENU, self.OnHelpById, id=helpobj.GetId())
3524            self.HelpById[helpobj.GetId()] = 'Data tree'
3525        helpobj = self.Append(text='Help on current data tree item',id=wx.ID_ANY, kind=wx.ITEM_NORMAL)
3526        frame.Bind(wx.EVT_MENU, self.OnHelpById, id=helpobj.GetId())
3527       
3528    def OnHelpById(self,event):
3529        '''Called when Help on... is pressed in a menu. Brings up a web page
3530        for documentation. Uses the helpKey value from the dataWindow window
3531        unless a special help key value has been defined for this menu id in
3532        self.HelpById
3533
3534        Note that self should now (2frame) be child of the main window (G2frame)
3535        '''
3536        if hasattr(self.frame,'dataWindow'):  # Debug code: check this is called from menu in G2frame
3537            # should always be true in 2 Frame version
3538            dW = self.frame.dataWindow
3539        else:
3540            print('help error: not called from standard menu?')
3541            print self
3542            return           
3543        try:
3544            helpKey = dW.helpKey # look up help from helpKey in data window
3545            #if GSASIIpath.GetConfigValue('debug'): print 'dataWindow help: key=',helpKey
3546        except AttributeError:
3547            helpKey = ''
3548            if GSASIIpath.GetConfigValue('debug'): print('No helpKey for current dataWindow!')
3549        helpType = self.HelpById.get(event.GetId(),helpKey) # see if there is a special help topic
3550        #if GSASIIpath.GetConfigValue('debug'): print 'helpKey=',helpKey,'  helpType=',helpType
3551        if helpType == 'Tutorials':
3552            dlg = OpenTutorial(self.frame)
3553            dlg.ShowModal()
3554            dlg.Destroy()
3555            return
3556        else:
3557            ShowHelp(helpType,self.frame)
3558
3559    def OnHelpAbout(self, event):
3560        "Display an 'About GSAS-II' box"
3561        import GSASII
3562        info = wx.AboutDialogInfo()
3563        info.Name = 'GSAS-II'
3564        ver = GSASIIpath.svnGetRev()
3565        if ver: 
3566            info.Version = 'Revision '+str(ver)+' (svn), version '+GSASII.__version__
3567        else:
3568            info.Version = 'Revision '+str(GSASIIpath.GetVersionNumber())+' (.py files), version '+GSASII.__version__
3569        #info.Developers = ['Robert B. Von Dreele','Brian H. Toby']
3570        info.Copyright = ('(c) ' + time.strftime('%Y') +
3571''' Argonne National Laboratory
3572This product includes software developed
3573by the UChicago Argonne, LLC, as
3574Operator of Argonne National Laboratory.''')
3575        info.Description = '''General Structure Analysis System-II (GSAS-II)
3576Robert B. Von Dreele and Brian H. Toby
3577
3578Please cite as:
3579  B.H. Toby & R.B. Von Dreele, J. Appl. Cryst. 46, 544-549 (2013)
3580For small angle use cite:
3581  R.B. Von Dreele, J. Appl. Cryst. 47, 1748-9 (2014)
3582For DIFFaX use cite:
3583  M.M.J. Treacy, J.M. Newsam & M.W. Deem,
3584  Proc. Roy. Soc. Lond. A 433, 499-520 (1991)
3585'''
3586        info.WebSite = ("https://subversion.xray.aps.anl.gov/trac/pyGSAS","GSAS-II home page")
3587        wx.AboutBox(info)
3588
3589    def OnCheckUpdates(self,event):
3590        '''Check if the GSAS-II repository has an update for the current source files
3591        and perform that update if requested.
3592        '''           
3593        if not GSASIIpath.whichsvn():
3594            dlg = wx.MessageDialog(self.frame,
3595                                   'No Subversion','Cannot update GSAS-II because subversion (svn) was not found.',
3596                                   wx.OK)
3597            dlg.ShowModal()
3598            dlg.Destroy()
3599            return
3600        wx.BeginBusyCursor()
3601        local = GSASIIpath.svnGetRev()
3602        if local is None: 
3603            wx.EndBusyCursor()
3604            dlg = wx.MessageDialog(self.frame,
3605                                   'Unable to run subversion on the GSAS-II current directory. Is GSAS-II installed correctly?',
3606                                   'Subversion error',
3607                                   wx.OK)
3608            dlg.ShowModal()
3609            dlg.Destroy()
3610            return
3611        print 'Installed GSAS-II version: '+local
3612        repos = GSASIIpath.svnGetRev(local=False)
3613        wx.EndBusyCursor()
3614        # has the current branch disappeared? If so, switch to the trunk -- not fully tested
3615        if (repos is None and "not found" in GSASIIpath.svnLastError.lower()
3616            and "path" in GSASIIpath.svnLastError.lower()):
3617            print('Repository is gone, will switch to trunk')
3618            GSASIIpath.svnSwitch2branch()
3619            return
3620        elif repos is None: 
3621            dlg = wx.MessageDialog(self.frame,
3622                                   'Unable to access the GSAS-II server. Is this computer on the internet?',
3623                                   'Server unavailable',
3624                                   wx.OK)
3625            dlg.ShowModal()
3626            dlg.Destroy()
3627            return
3628        print 'GSAS-II version on server: '+repos
3629        if local == repos:
3630            dlg = wx.MessageDialog(self.frame,
3631                                   'GSAS-II is up-to-date. Version '+local+' is already loaded.',
3632                                   'GSAS-II Up-to-date',
3633                                   wx.OK)
3634            dlg.ShowModal()
3635            dlg.Destroy()
3636            return
3637        mods = GSASIIpath.svnFindLocalChanges()
3638        if mods:
3639            dlg = wx.MessageDialog(self.frame,
3640                                   'You have version '+local+
3641                                   ' of GSAS-II installed, but the current version is '+repos+
3642                                   '. However, '+str(len(mods))+
3643                                   ' file(s) on your local computer have been modified.'
3644                                   ' Updating will attempt to merge your local changes with '
3645                                   'the latest GSAS-II version, but if '
3646                                   'conflicts arise, local changes will be '
3647                                   'discarded. It is also possible that the '
3648                                   'local changes my prevent GSAS-II from running. '
3649                                   'Press OK to start an update if this is acceptable:',
3650                                   'Local GSAS-II Mods',
3651                                   wx.OK|wx.CANCEL)
3652            if dlg.ShowModal() != wx.ID_OK:
3653                dlg.Destroy()
3654                return
3655            else:
3656                dlg.Destroy()
3657        else:
3658            dlg = wx.MessageDialog(self.frame,
3659                                   'You have version '+local+
3660                                   ' of GSAS-II installed, but the current version is '+repos+
3661                                   '. Press OK to start an update:',
3662                                   'GSAS-II Updates',
3663                                   wx.OK|wx.CANCEL)
3664            if dlg.ShowModal() != wx.ID_OK:
3665                dlg.Destroy()
3666                return
3667            dlg.Destroy()
3668        print 'start updates'
3669        dlg = wx.MessageDialog(self.frame,
3670                               'Your project will now be saved, GSAS-II will exit and an update '
3671                               'will be performed and GSAS-II will restart. Press Cancel to '
3672                               'abort the update',
3673                               'Start update?',
3674                               wx.OK|wx.CANCEL)
3675        if dlg.ShowModal() != wx.ID_OK:
3676            dlg.Destroy()
3677            return
3678        dlg.Destroy()
3679        self.frame.OnFileSave(event)
3680        GPX = self.frame.GSASprojectfile
3681        GSASIIpath.svnUpdateProcess(projectfile=GPX)
3682        return
3683
3684    def OnSelectVersion(self,event):
3685        '''Allow the user to select a specific version of GSAS-II
3686        '''
3687        if not GSASIIpath.whichsvn():
3688            dlg = wx.MessageDialog(self,'No Subversion','Cannot update GSAS-II because subversion (svn) '+
3689                                   'was not found.'
3690                                   ,wx.OK)
3691            dlg.ShowModal()
3692            return
3693        local = GSASIIpath.svnGetRev()
3694        if local is None: 
3695            dlg = wx.MessageDialog(self.frame,
3696                                   'Unable to run subversion on the GSAS-II current directory. Is GSAS-II installed correctly?',
3697                                   'Subversion error',
3698                                   wx.OK)
3699            dlg.ShowModal()
3700            dlg.Destroy()
3701            return
3702        mods = GSASIIpath.svnFindLocalChanges()
3703        if mods:
3704            dlg = wx.MessageDialog(self.frame,
3705                                   'You have version '+local+
3706                                   ' of GSAS-II installed'
3707                                   '. However, '+str(len(mods))+
3708                                   ' file(s) on your local computer have been modified.'
3709                                   ' Downdating will attempt to merge your local changes with '
3710                                   'the selected GSAS-II version. '
3711                                   'Downdating is not encouraged because '
3712                                   'if merging is not possible, your local changes will be '
3713                                   'discarded. It is also possible that the '
3714                                   'local changes my prevent GSAS-II from running. '
3715                                   'Press OK to continue anyway.',
3716                                   'Local GSAS-II Mods',
3717                                   wx.OK|wx.CANCEL)
3718            if dlg.ShowModal() != wx.ID_OK:
3719                dlg.Destroy()
3720                return
3721            dlg.Destroy()
3722        if GSASIIpath.svnGetRev(local=False) is None:
3723            dlg = wx.MessageDialog(self.frame,
3724                                   'Error obtaining current GSAS-II version. Is internet access working correctly?',
3725                                   'Subversion error',
3726                                   wx.OK)
3727            dlg.ShowModal()
3728            dlg.Destroy()
3729            return
3730        dlg = downdate(parent=self.frame)
3731        if dlg.ShowModal() == wx.ID_OK:
3732            ver = dlg.getVersion()
3733        else:
3734            dlg.Destroy()
3735            return
3736        dlg.Destroy()
3737        print('start regress to '+str(ver))
3738        self.frame.OnFileSave(event)
3739        GPX = self.frame.GSASprojectfile
3740        GSASIIpath.svnUpdateProcess(projectfile=GPX,version=str(ver))
3741        return
3742
3743    def OnSelectBranch(self,event):
3744        '''Allow the user to select branch of GSAS-II or return to trunk
3745        N.B. Name of branch to use is hard-coded here. Must contain a slash
3746        '''
3747        testbranch = '/branch/2frame'
3748        if not GSASIIpath.svnTestBranch():
3749            dlg = wx.MessageDialog(self.frame,
3750                                   'Switching to test GSAS-II version',
3751                                   'Confirm Switch',
3752                                   wx.OK|wx.CANCEL)
3753            if dlg.ShowModal() != wx.ID_OK: return
3754            branch = testbranch
3755        else:
3756            dlg = wx.MessageDialog(self.frame,
3757                                   'Switching back to standard GSAS-II version',
3758                                   'Confirm Switch',
3759                                   wx.OK|wx.CANCEL)
3760            if dlg.ShowModal() != wx.ID_OK: return
3761            branch = 'trunk'
3762        print('start switch')
3763        self.frame.OnFileSave(event)
3764        GPX = self.frame.GSASprojectfile
3765        GSASIIpath.svnUpdateProcess(projectfile=GPX,branch=branch)
3766
3767################################################################################
3768class HelpButton(wx.Button):
3769    '''Create a help button that displays help information.
3770    The text is displayed in a modal message window.
3771
3772    TODO: it might be nice if it were non-modal: e.g. it stays around until
3773    the parent is deleted or the user closes it, but this did not work for
3774    me.
3775
3776    :param parent: the panel which will be the parent of the button
3777    :param str msg: the help text to be displayed
3778    '''
3779    def __init__(self,parent,msg):
3780        if sys.platform == "darwin": 
3781            wx.Button.__init__(self,parent,wx.ID_HELP)
3782        else:
3783            wx.Button.__init__(self,parent,wx.ID_ANY,'?',style=wx.BU_EXACTFIT)
3784        self.Bind(wx.EVT_BUTTON,self._onPress)
3785        self.msg=StripIndents(msg)
3786        self.parent = parent
3787    def _onClose(self,event):
3788        self.dlg.EndModal(wx.ID_CANCEL)
3789    def _onPress(self,event):
3790        'Respond to a button press by displaying the requested text'
3791        #dlg = wx.MessageDialog(self.parent,self.msg,'Help info',wx.OK)
3792        self.dlg = wx.Dialog(self.parent,wx.ID_ANY,'Help information', 
3793                        style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)
3794        #self.dlg.SetBackgroundColour(wx.WHITE)
3795        mainSizer = wx.BoxSizer(wx.VERTICAL)
3796        txt = wx.StaticText(self.dlg,wx.ID_ANY,self.msg)
3797        mainSizer.Add(txt,1,wx.ALL|wx.EXPAND,10)
3798        txt.SetBackgroundColour(wx.WHITE)
3799
3800        btnsizer = wx.BoxSizer(wx.HORIZONTAL)
3801        btn = wx.Button(self.dlg, wx.ID_CLOSE) 
3802        btn.Bind(wx.EVT_BUTTON,self._onClose)
3803        btnsizer.Add(btn)
3804        mainSizer.Add(btnsizer, 0, wx.ALIGN_CENTER|wx.ALL, 5)
3805        self.dlg.SetSizer(mainSizer)
3806        mainSizer.Fit(self.dlg)
3807        self.dlg.CenterOnParent()
3808        self.dlg.ShowModal()
3809        self.dlg.Destroy()
3810################################################################################
3811class MyHtmlPanel(wx.Panel):
3812    '''Defines a panel to display HTML help information, as an alternative to
3813    displaying help information in a web browser.
3814    '''
3815    def __init__(self, frame, id):
3816        self.frame = frame
3817        wx.Panel.__init__(self, frame, id)
3818        sizer = wx.BoxSizer(wx.VERTICAL)
3819        back = wx.Button(self, -1, "Back")
3820        back.Bind(wx.EVT_BUTTON, self.OnBack)
3821        self.htmlwin = G2HtmlWindow(self, id, size=(750,450))
3822        sizer.Add(self.htmlwin, 1,wx.EXPAND)
3823        sizer.Add(back, 0, wx.ALIGN_LEFT, 0)
3824        self.SetSizer(sizer)
3825        sizer.Fit(frame)       
3826        self.Bind(wx.EVT_SIZE,self.OnHelpSize)
3827    def OnHelpSize(self,event):         #does the job but weirdly!!
3828        anchor = self.htmlwin.GetOpenedAnchor()
3829        if anchor:           
3830            self.htmlwin.ScrollToAnchor(anchor)
3831            wx.CallAfter(self.htmlwin.ScrollToAnchor,anchor)
3832            if event: event.Skip()
3833    def OnBack(self, event):
3834        self.htmlwin.HistoryBack()
3835    def LoadFile(self,file):
3836        pos = file.rfind('#')
3837        if pos != -1:
3838            helpfile = file[:pos]
3839            helpanchor = file[pos+1:]
3840        else:
3841            helpfile = file
3842            helpanchor = None
3843        self.htmlwin.LoadPage(helpfile)
3844        if helpanchor is not None:
3845            self.htmlwin.ScrollToAnchor(helpanchor)
3846            xs,ys = self.htmlwin.GetViewStart()
3847            self.htmlwin.Scroll(xs,ys-1)
3848################################################################################
3849class G2HtmlWindow(wx.html.HtmlWindow):
3850    '''Displays help information in a primitive HTML browser type window
3851    '''
3852    def __init__(self, parent, *args, **kwargs):
3853        self.parent = parent
3854        wx.html.HtmlWindow.__init__(self, parent, *args, **kwargs)
3855    def LoadPage(self, *args, **kwargs):
3856        wx.html.HtmlWindow.LoadPage(self, *args, **kwargs)
3857        self.TitlePage()
3858    def OnLinkClicked(self, *args, **kwargs):
3859        wx.html.HtmlWindow.OnLinkClicked(self, *args, **kwargs)
3860        xs,ys = self.GetViewStart()
3861        self.Scroll(xs,ys-1)
3862        self.TitlePage()
3863    def HistoryBack(self, *args, **kwargs):
3864        wx.html.HtmlWindow.HistoryBack(self, *args, **kwargs)
3865        self.TitlePage()
3866    def TitlePage(self):
3867        self.parent.frame.SetTitle(self.GetOpenedPage() + ' -- ' + 
3868            self.GetOpenedPageTitle())
3869
3870################################################################################
3871def StripIndents(msg):
3872    'Strip indentation from multiline strings'
3873    msg1 = msg.replace('\n ','\n')
3874    while msg != msg1:
3875        msg = msg1
3876        msg1 = msg.replace('\n ','\n')
3877    return msg.replace('\n\t','\n')
3878
3879def StripUnicode(string,subs='.'):
3880    '''Strip non-ASCII characters from strings
3881   
3882    :param str string: string to strip Unicode characters from
3883    :param str subs: character(s) to place into string in place of each
3884      Unicode character. Defaults to '.'
3885
3886    :returns: a new string with only ASCII characters
3887    '''
3888    s = ''
3889    for c in string:
3890        if ord(c) < 128:
3891            s += c
3892        else:
3893            s += subs
3894    return s.encode('ascii','replace')
3895       
3896################################################################################
3897# configuration routines (for editing config.py)
3898def SaveGPXdirectory(path):
3899    if GSASIIpath.GetConfigValue('Starting_directory') == path: return
3900    vars = GetConfigValsDocs()
3901    try:
3902        vars['Starting_directory'][1] = path
3903        if GSASIIpath.GetConfigValue('debug'): print('Saving GPX path: '+path)
3904        SaveConfigVars(vars)
3905    except KeyError:
3906        pass
3907
3908def SaveImportDirectory(path):
3909    if GSASIIpath.GetConfigValue('Import_directory') == path: return
3910    vars = GetConfigValsDocs()
3911    try:
3912        vars['Import_directory'][1] = path
3913        if GSASIIpath.GetConfigValue('debug'): print('Saving Import path: '+path)
3914        SaveConfigVars(vars)
3915    except KeyError:
3916        pass
3917
3918def GetConfigValsDocs():
3919    '''Reads the module referenced in fname (often <module>.__file__) and
3920    return a dict with names of global variables as keys.
3921    For each global variable, the value contains four items:
3922
3923    :returns: a dict where keys are names defined in module config_example.py
3924      where the value is a list of four items, as follows:
3925
3926         * item 0: the default value
3927         * item 1: the current value
3928         * item 2: the initial value (starts same as item 1)
3929         * item 3: the "docstring" that follows variable definition
3930
3931    '''
3932    import config_example
3933    import ast
3934    fname = os.path.splitext(config_example.__file__)[0]+'.py' # convert .pyc to .py
3935    with open(fname, 'r') as f:
3936        fstr = f.read()
3937    fstr = fstr.replace('\r\n', '\n').replace('\r', '\n')
3938    if not fstr.endswith('\n'):
3939        fstr += '\n'
3940    tree = ast.parse(fstr)
3941    d = {}
3942    key = None
3943    for node in ast.walk(tree):
3944        if isinstance(node,ast.Assign):
3945            key = node.targets[0].id
3946            d[key] = [config_example.__dict__.get(key),
3947                      GSASIIpath.configDict.get(key),
3948                      GSASIIpath.configDict.get(key),'']
3949        elif isinstance(node,ast.Expr) and key:
3950            d[key][3] = node.value.s.strip()
3951        else:
3952            key = None
3953    return d
3954
3955def SaveConfigVars(vars,parent=None):
3956    '''Write the current config variable values to config.py
3957
3958    :params dict vars: a dictionary of variable settings and meanings as
3959      created in :func:`GetConfigValsDocs`.
3960    :param parent: wx.Frame object or None (default) for parent
3961      of error message if no file can be written.
3962    :returns: True if unable to write the file, None otherwise
3963    '''
3964    # try to write to where an old config file is located
3965    try:
3966        import config
3967        savefile = config.__file__
3968    except ImportError: # no config.py file yet
3969        savefile = os.path.join(GSASIIpath.path2GSAS2,'config.py')
3970    # try to open file for write
3971    try:
3972        savefile = os.path.splitext(savefile)[0]+'.py' # convert .pyc to .py
3973        fp = open(savefile,'w')
3974    except IOError:  # can't write there, write in local mods directory
3975        # create a local mods directory, if needed
3976        if not os.path.exists(os.path.expanduser('~/.G2local/')):
3977            print('Creating directory '+os.path.expanduser('~/.G2local/'))
3978            os.mkdir(os.path.expanduser('~/.G2local/'))
3979            sys.path.insert(0,os.path.expanduser('~/.G2local/'))
3980        savefile = os.path.join(os.path.expanduser('~/.G2local/'),'config.py')
3981        try:
3982            fp = open(savefile,'w')
3983        except IOError:
3984            if parent:
3985                G2MessageBox(parent,'Error trying to write configuration to '+savefile,
3986                    'Unable to save')
3987            else:
3988                print('Error trying to write configuration to '+savefile)
3989            return True
3990    import datetime
3991    fp.write("'''\n")
3992    fp.write("*config.py: Configuration options*\n----------------------------------\n")
3993    fp.write("This file created in SelectConfigSetting on {:%d %b %Y %H:%M}\n".
3994             format(datetime.datetime.now()))
3995    fp.write("'''\n\n")
3996    fp.write("import os.path\n")
3997    fp.write("import GSASIIpath\n\n")
3998    for var in sorted(vars.keys(),key=lambda s: s.lower()):
3999        if vars[var][1] is None: continue
4000        if vars[var][1] == '': continue
4001        if vars[var][0] == vars[var][1]: continue
4002        try:
4003            float(vars[var][1]) # test for number
4004            fp.write(var + ' = ' + str(vars[var][1])+'\n')
4005        except:
4006            try:
4007                eval(vars[var][1]) # test for an expression
4008                fp.write(var + ' = ' + str(vars[var][1])+'\n')
4009            except: # must be a string
4010                varstr = vars[var][1]
4011                if '\\' in varstr:
4012                    fp.write(var + ' = os.path.normpath("' + varstr.replace('\\','/') +'")\n')
4013                else:
4014                    fp.write(var + ' = "' + str(varstr)+'"\n')
4015        if vars[var][3]:
4016            fp.write("'''" + str(vars[var][3]) + "\n'''\n\n")
4017    fp.close()
4018    print('wrote file '+savefile)
4019
4020class SelectConfigSetting(wx.Dialog):
4021    '''Dialog to select configuration variables and set associated values.
4022    '''
4023    def __init__(self,parent=None):
4024        style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER
4025        wx.Dialog.__init__(self, parent, wx.ID_ANY, 'Set Config Variable', style=style)
4026        self.sizer = wx.BoxSizer(wx.VERTICAL)
4027        self.vars = GetConfigValsDocs()
4028       
4029        label = wx.StaticText(
4030            self,  wx.ID_ANY,
4031            'Select a GSAS-II configuration variable to change'
4032            )
4033        self.sizer.Add(label, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
4034        self.choice = {}
4035        btn = G2ChoiceButton(self, sorted(self.vars.keys(),key=lambda s: s.lower()),
4036            strLoc=self.choice,strKey=0,onChoice=self.OnSelection)
4037        btn.SetLabel("")
4038        self.sizer.Add(btn)
4039
4040        self.varsizer = wx.BoxSizer(wx.VERTICAL)
4041        self.sizer.Add(self.varsizer,1,wx.ALL|wx.EXPAND,1)
4042       
4043        self.doclbl = wx.StaticBox(self, wx.ID_ANY, "")
4044        self.doclblsizr = wx.StaticBoxSizer(self.doclbl)
4045        self.docinfo = wx.StaticText(self,  wx.ID_ANY, "")
4046        self.doclblsizr.Add(self.docinfo, 0, wx.ALIGN_LEFT|wx.ALL, 5)
4047        self.sizer.Add(self.doclblsizr, 0, wx.ALIGN_LEFT|wx.EXPAND|wx.ALL, 5)
4048        btnsizer = wx.BoxSizer(wx.HORIZONTAL)
4049        self.saveBtn = wx.Button(self,-1,"Save current settings")
4050        btnsizer.Add(self.saveBtn, 0, wx.ALL, 2) 
4051        self.saveBtn.Bind(wx.EVT_BUTTON, self.OnSave)
4052        self.saveBtn.Enable(False)
4053        self.applyBtn = wx.Button(self,-1,"Use current (no save)")
4054        btnsizer.Add(self.applyBtn, 0, wx.ALL, 2) 
4055        self.applyBtn.Bind(wx.EVT_BUTTON, self.OnApplyChanges)
4056        self.applyBtn.Enable(False)
4057       
4058        btn = wx.Button(self,wx.ID_CANCEL)
4059        btnsizer.Add(btn, 0, wx.ALL, 2) 
4060        self.sizer.Add(btnsizer, 0, wx.ALIGN_CENTRE|wx.ALL, 5) 
4061               
4062        self.SetSizer(self.sizer)
4063        self.sizer.Fit(self)
4064        self.CenterOnParent()
4065       
4066    def OnChange(self,event=None):
4067        ''' Check if anything been changed. Turn the save button on/off.
4068        '''
4069        for var in self.vars:
4070            if self.vars[var][0] is None and self.vars[var][1] is not None:
4071                # make blank strings into None, if that is the default
4072                if self.vars[var][1].strip() == '': self.vars[var][1] = None
4073            if self.vars[var][1] != self.vars[var][2]:
4074                #print 'changed',var,self.vars[var][:3]
4075                self.saveBtn.Enable(True)
4076                self.applyBtn.Enable(True)
4077                break
4078        else:
4079            self.saveBtn.Enable(False)
4080            self.applyBtn.Enable(False)
4081        try:
4082            self.resetBtn.Enable(True)
4083        except:
4084            pass
4085       
4086    def OnApplyChanges(self,event=None):
4087        'Set config variables to match the current settings'
4088        GSASIIpath.SetConfigValue(self.vars)
4089        self.EndModal(wx.ID_OK)
4090       
4091    def OnSave(self,event):
4092        '''Write the config variables to config.py and then set them
4093        as the current settings
4094        '''
4095        if not SaveConfigVars(self.vars,parent=self):
4096            self.OnApplyChanges() # force a reload of the config settings
4097            self.EndModal(wx.ID_OK)
4098
4099    def OnBoolSelect(self,event):
4100        'Respond to a change in a True/False variable'
4101        rb = event.GetEventObject()
4102        var = self.choice[0]
4103        self.vars[var][1] = (rb.GetSelection() == 0)
4104        self.OnChange()
4105        wx.CallAfter(self.OnSelection)
4106       
4107    def onSelDir(self,event):
4108        'Select a directory from a menu'
4109        dlg = wx.DirDialog(self, "Choose a directory:",style=wx.DD_DEFAULT_STYLE)
4110        if dlg.ShowModal() == wx.ID_OK:
4111            var = self.choice[0]
4112            self.vars[var][1] = dlg.GetPath()
4113            self.strEd.SetValue(self.vars[var][1])
4114            self.OnChange()
4115        dlg.Destroy()
4116       
4117    def OnSelection(self):
4118        'show a selected variable'
4119        def OnNewColorBar(event):
4120            self.vars['Contour_color'][1] = self.colSel.GetValue()
4121            self.OnChange(event)
4122
4123        self.varsizer.DeleteWindows()
4124        var = self.choice[0]
4125        showdef = True
4126        if var not in self.vars:
4127            raise Exception,"How did this happen?"
4128        if type(self.vars[var][0]) is int:
4129            ed = ValidatedTxtCtrl(self,self.vars[var],1,typeHint=int,OKcontrol=self.OnChange)
4130            self.varsizer.Add(ed, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
4131        elif type(self.vars[var][0]) is float:
4132            ed = ValidatedTxtCtrl(self,self.vars[var],1,typeHint=float,OKcontrol=self.OnChange)
4133            self.varsizer.Add(ed, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
4134        elif type(self.vars[var][0]) is bool:
4135            showdef = False
4136            lbl = "value for "+var
4137            ch = []
4138            for i,v in enumerate((True,False)):
4139                s = str(v)
4140                if v == self.vars[var][0]:
4141                    defopt = i
4142                    s += ' (default)'
4143                ch += [s]
4144            rb = wx.RadioBox(self, wx.ID_ANY, lbl, wx.DefaultPosition, wx.DefaultSize,
4145                ch, 1, wx.RA_SPECIFY_COLS)
4146            # set initial value
4147            if self.vars[var][1] is None:
4148                rb.SetSelection(defopt)
4149            elif self.vars[var][1]:
4150                rb.SetSelection(0)
4151            else:
4152                rb.SetSelection(1)
4153            rb.Bind(wx.EVT_RADIOBOX,self.OnBoolSelect)
4154            self.varsizer.Add(rb, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
4155        else:
4156            if var.endswith('_directory') or var.endswith('_location'):
4157                btn = wx.Button(self,wx.ID_ANY,'Select from dialog...')
4158                sz = (400,-1)
4159            else:
4160                btn = None
4161                sz = (250,-1)
4162            if var == 'Contour_color':
4163                if self.vars[var][1] is None:
4164                    self.vars[var][1] = 'paired'
4165                colorList = sorted([m for m in mpl.cm.datad.keys() ],key=lambda s: s.lower())   #if not m.endswith("_r")
4166                self.colSel = wx.ComboBox(self,value=self.vars[var][1],choices=colorList,
4167                    style=wx.CB_READONLY|wx.CB_DROPDOWN)
4168                self.colSel.Bind(wx.EVT_COMBOBOX, OnNewColorBar)
4169                self.varsizer.Add(self.colSel, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
4170            else:
4171                self.strEd = ValidatedTxtCtrl(self,self.vars[var],1,typeHint=str,
4172                    OKcontrol=self.OnChange,size=sz)
4173                if self.vars[var][1] is not None:
4174                    self.strEd.SetValue(self.vars[var][1])
4175                self.varsizer.Add(self.strEd, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
4176            if btn:
4177                btn.Bind(wx.EVT_BUTTON,self.onSelDir)
4178                self.varsizer.Add(btn, 0, wx.ALIGN_CENTRE|wx.ALL, 5) 
4179        # button for reset to default value
4180        lbl = "Reset to Default"
4181        if showdef: # spell out default when needed
4182            lbl += ' (='+str(self.vars[var][0])+')'
4183            #label = wx.StaticText(self,  wx.ID_ANY, 'Default value = '+str(self.vars[var][0]))
4184            #self.varsizer.Add(label, 0, wx.ALIGN_LEFT|wx.ALL, 5)
4185        self.resetBtn = wx.Button(self,-1,lbl)
4186        self.resetBtn.Bind(wx.EVT_BUTTON, self.OnClear)
4187        if self.vars[var][1] is not None and self.vars[var][1] != '': # show current value, if one
4188            #label = wx.StaticText(self,  wx.ID_ANY, 'Current value = '+str(self.vars[var][1]))
4189            #self.varsizer.Add(label, 0, wx.ALIGN_LEFT|wx.ALL, 5)
4190            self.resetBtn.Enable(True)
4191        else:
4192            self.resetBtn.Enable(False)
4193        self.varsizer.Add(self.resetBtn, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
4194        # show meaning, if defined
4195        self.doclbl.SetLabel("Description of "+str(var)) 
4196        if self.vars[var][3]:
4197            self.docinfo.SetLabel(self.vars[var][3])
4198        else:
4199            self.docinfo.SetLabel("(not documented)")
4200        self.sizer.Fit(self)
4201        self.CenterOnParent()
4202        wx.CallAfter(self.SendSizeEvent)
4203
4204    def OnClear(self, event):
4205        var = self.choice[0]
4206        self.vars[var][1] = self.vars[var][0]
4207        self.OnChange()
4208        wx.CallAfter(self.OnSelection)
4209       
4210################################################################################
4211class downdate(wx.Dialog):
4212    '''Dialog to allow a user to select a version of GSAS-II to install
4213    '''
4214    def __init__(self,parent=None):
4215        style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER
4216        wx.Dialog.__init__(self, parent, wx.ID_ANY, 'Select Version', style=style)
4217        pnl = wx.Panel(self)
4218        sizer = wx.BoxSizer(wx.VERTICAL)
4219        insver = GSASIIpath.svnGetRev(local=True)
4220        curver = int(GSASIIpath.svnGetRev(local=False))
4221        label = wx.StaticText(
4222            pnl,  wx.ID_ANY,
4223            'Select a specific GSAS-II version to install'
4224            )
4225        sizer.Add(label, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
4226        sizer1 = wx.BoxSizer(wx.HORIZONTAL)
4227        sizer1.Add(
4228            wx.StaticText(pnl,  wx.ID_ANY,
4229                          'Currently installed version: '+str(insver)),
4230            0, wx.ALIGN_CENTRE|wx.ALL, 5)
4231        sizer.Add(sizer1)
4232        sizer1 = wx.BoxSizer(wx.HORIZONTAL)
4233        sizer1.Add(
4234            wx.StaticText(pnl,  wx.ID_ANY,
4235                          'Select GSAS-II version to install: '),
4236            0, wx.ALIGN_CENTRE|wx.ALL, 5)
4237        self.spin = wx.SpinCtrl(pnl, wx.ID_ANY,size=(150,-1))
4238        self.spin.SetRange(1, curver)
4239        self.spin.SetValue(curver)
4240        self.Bind(wx.EVT_SPINCTRL, self._onSpin, self.spin)
4241        self.Bind(wx.EVT_KILL_FOCUS, self._onSpin, self.spin)
4242        sizer1.Add(self.spin)
4243        sizer.Add(sizer1)
4244
4245        line = wx.StaticLine(pnl,-1, size=(-1,3), style=wx.LI_HORIZONTAL)
4246        sizer.Add(line, 0, wx.EXPAND|wx.ALIGN_CENTER|wx.ALL, 10)
4247
4248        self.text = wx.StaticText(pnl,  wx.ID_ANY, "")
4249        sizer.Add(self.text, 0, wx.ALIGN_LEFT|wx.EXPAND|wx.ALL, 5)
4250
4251        line = wx.StaticLine(pnl,-1, size=(-1,3), style=wx.LI_HORIZONTAL)
4252        sizer.Add(line, 0, wx.EXPAND|wx.ALIGN_CENTER|wx.ALL, 10)
4253        sizer.Add(
4254            wx.StaticText(
4255                pnl,  wx.ID_ANY,
4256                'If "Install" is pressed, your project will be saved;\n'
4257                'GSAS-II will exit; The specified version will be loaded\n'
4258                'and GSAS-II will restart. Press "Cancel" to abort.'),
4259            0, wx.EXPAND|wx.ALL, 10)
4260        btnsizer = wx.StdDialogButtonSizer()
4261        btn = wx.Button(pnl, wx.ID_OK, "Install")
4262        btn.SetDefault()
4263        btnsizer.AddButton(btn)
4264        btn = wx.Button(pnl, wx.ID_CANCEL)
4265        btnsizer.AddButton(btn)
4266        btnsizer.Realize()
4267        sizer.Add(btnsizer, 0, wx.ALIGN_CENTER|wx.ALL, 5)
4268        pnl.SetSizer(sizer)
4269        sizer.Fit(self)
4270        self.topsizer=sizer
4271        self.CenterOnParent()
4272        self._onSpin(None)
4273
4274    def _onSpin(self,event):
4275        'Called to load info about the selected version in the dialog'
4276        if event: event.Skip()
4277        ver = self.spin.GetValue()
4278        d = GSASIIpath.svnGetLog(version=ver)
4279        date = d.get('date','?').split('T')[0]
4280        s = '(Version '+str(ver)+' created '+date
4281        s += ' by '+d.get('author','?')+')'
4282        msg = d.get('msg')
4283        if msg: s += '\n\nComment: '+msg
4284        self.text.SetLabel(s)
4285        self.topsizer.Fit(self)
4286
4287    def getVersion(self):
4288        'Get the version number in the dialog'
4289        return self.spin.GetValue()
4290
4291################################################################################
4292#### Display Help information
4293################################################################################
4294# define some globals
4295htmlPanel = None
4296htmlFrame = None
4297htmlFirstUse = True
4298#helpLocDict = {}  # to be implemented if we ever split gsasii.html over multiple files
4299path2GSAS2 = os.path.dirname(os.path.realpath(__file__)) # save location of this file
4300def ShowHelp(helpType,frame):
4301    '''Called to bring up a web page for documentation.'''
4302    global htmlFirstUse,htmlPanel,htmlFrame
4303    # no defined link to use, create a default based on key
4304    helplink = 'gsasII.html'
4305    if helpType:
4306        helplink += '#'+helpType.replace(')','').replace('(','_').replace(' ','_')
4307    # determine if a web browser or the internal viewer should be used for help info
4308    if GSASIIpath.GetConfigValue('Help_mode'):
4309        helpMode = GSASIIpath.GetConfigValue('Help_mode')
4310    else:
4311        helpMode = 'browser'
4312    if helpMode == 'internal':
4313        helplink = os.path.join(path2GSAS2,'help',helplink)
4314        try:
4315            htmlPanel.LoadFile(helplink)
4316            htmlFrame.Raise()
4317        except:
4318            htmlFrame = wx.Frame(frame, -1, size=(610, 510))
4319            htmlFrame.Show(True)
4320            htmlFrame.SetTitle("HTML Window") # N.B. reset later in LoadFile
4321            htmlPanel = MyHtmlPanel(htmlFrame,-1)
4322            htmlPanel.LoadFile(helplink)
4323    else:
4324        if sys.platform == "darwin": # for Mac, force use of safari to preserve anchors on file URLs
4325            wb = webbrowser.MacOSXOSAScript('safari')
4326        else:
4327            wb = webbrowser
4328        helplink = os.path.join(path2GSAS2,'help',helplink)
4329        pfx = "file://"
4330        if sys.platform.lower().startswith('win'):
4331            pfx = ''
4332        #if GSASIIpath.GetConfigValue('debug'): print 'Help link=',pfx+helplink
4333        if htmlFirstUse:
4334            wb.open_new(pfx+helplink)
4335            htmlFirstUse = False
4336        else:
4337            wb.open(pfx+helplink, new=0, autoraise=True)
4338
4339def ShowWebPage(URL,frame):
4340    '''Called to show a tutorial web page.
4341    '''
4342    global htmlFirstUse,htmlPanel,htmlFrame
4343    # determine if a web browser or the internal viewer should be used for help info
4344    if GSASIIpath.GetConfigValue('Help_mode'):
4345        helpMode = GSASIIpath.GetConfigValue('Help_mode')
4346    else:
4347        helpMode = 'browser'
4348    if helpMode == 'internal':
4349        try:
4350            htmlPanel.LoadFile(URL)
4351            htmlFrame.Raise()
4352        except:
4353            htmlFrame = wx.Frame(frame, -1, size=(610, 510))
4354            htmlFrame.Show(True)
4355            htmlFrame.SetTitle("HTML Window") # N.B. reset later in LoadFile
4356            htmlPanel = MyHtmlPanel(htmlFrame,-1)
4357            htmlPanel.LoadFile(URL)
4358    else:
4359        if URL.startswith('http'): 
4360            pfx = ''
4361        elif sys.platform.lower().startswith('win'):
4362            pfx = ''
4363        else:
4364            pfx = "file://"
4365        if htmlFirstUse:
4366            webbrowser.open_new(pfx+URL)
4367            htmlFirstUse = False
4368        else:
4369            webbrowser.open(pfx+URL, new=0, autoraise=True)
4370
4371################################################################################
4372#### Tutorials support
4373################################################################################
4374G2BaseURL = "https://subversion.xray.aps.anl.gov/pyGSAS"
4375# N.B. tutorialCatalog is generated by routine catalog.py, which also generates the appropriate
4376# empty directories (.../MT/* .../trunk/GSASII/* *=[help,Exercises])
4377tutorialCatalog = (
4378    # tutorial dir,      web page file name,      title for page
4379
4380    ['StartingGSASII', 'Starting GSAS.htm', 'Starting GSAS-II'],
4381       
4382    ['LabData', 'Laboratory X.htm', 'Fitting laboratory X-ray powder data for fluoroapatite'],
4383    ['CWNeutron', 'Neutron CW Powder Data.htm', 'CW Neutron Powder fit for Yttrium-Iron Garnet'],
4384
4385    ['FitPeaks', 'Fit Peaks.htm', 'Fitting individual peaks & autoindexing'],
4386    ['CFjadarite', 'Charge Flipping in GSAS.htm', '     Charge Flipping structure solution for jadarite'],
4387    ['CFsucrose', 'Charge Flipping - sucrose.htm','     Charge Flipping structure solution for sucrose'],
4388    ['BkgFit', 'FitBkgTut.htm',  'Fitting the Starting Background using Fixed Points'],
4389       
4390    ['CWCombined', 'Combined refinement.htm', 'Combined X-ray/CW-neutron refinement of PbSO4'],
4391    ['TOF-CW Joint Refinement', 'TOF combined XN Rietveld refinement in GSAS.htm', 'Combined X-ray/TOF-neutron Rietveld refinement'],
4392    ['SeqRefine', 'SequentialTutorial.htm', 'Sequential refinement of multiple datasets'],
4393    ['SeqParametric', 'ParametricFitting.htm', '     Parametric Fitting and Pseudo Variables for Sequential Fits'],
4394       
4395    ['StackingFaults-I', 'Stacking Faults-I.htm', 'Stacking fault simulations for diamond'],
4396    ['StackingFaults-II', 'Stacking Faults II.htm', 'Stacking fault simulations for Keokuk kaolinite'],
4397    ['StackingFaults-III', 'Stacking Faults-III.htm', 'Stacking fault simulations for Georgia kaolinite'],
4398       
4399    ['CFXraySingleCrystal', 'CFSingleCrystal.htm', 'Charge Flipping structure solution with Xray single crystal data'],       
4400    ['TOF Charge Flipping', 'Charge Flipping with TOF single crystal data in GSASII.htm', 'Charge flipping with neutron TOF single crystal data'],
4401    ['MCsimanneal', 'MCSA in GSAS.htm', 'Monte-Carlo simulated annealing structure determination'],
4402       
4403    ['MerohedralTwins', 'Merohedral twin refinement in GSAS.htm', 'Merohedral twin refinements'],
4404
4405    ['2DCalibration', 'Calibration of an area detector in GSAS.htm', 'Calibration of an area detector'],
4406    ['2DIntegration', 'Integration of area detector data in GSAS.htm', '     Integration of area detector data'],
4407    ['TOF Calibration', 'Calibration of a TOF powder diffractometer.htm', 'Calibration of a Neutron TOF diffractometer'],
4408    ['TOF Single Crystal Refinement', 'TOF single crystal refinement in GSAS.htm', 'Single crystal refinement from TOF data'],
4409       
4410    ['2DStrain', 'Strain fitting of 2D data in GSAS-II.htm', 'Strain fitting of 2D data'],
4411    ['2DTexture', 'Texture analysis of 2D data in GSAS-II.htm', 'Texture analysis of 2D data'],
4412             
4413    ['SAsize', 'Small Angle Size Distribution.htm', 'Small angle x-ray data size distribution (alumina powder)'],
4414    ['SAfit', 'Fitting Small Angle Scattering Data.htm', '     Fitting small angle x-ray data (alumina powder)'],
4415    ['SAimages', 'Small Angle Image Processing.htm', 'Image Processing of small angle x-ray data'],
4416    ['SAseqref', 'Sequential Refinement of Small Angle Scattering Data.htm', 'Sequential refinement with small angle scattering data'],
4417   
4418    #['ExampleDir', 'ExamplePage.html', 'Example Tutorial Title'],
4419    )
4420
4421class OpenTutorial(wx.Dialog):
4422    '''Open a tutorial web page, optionally copying the web page, screen images and
4423    data file(s) to the local disk.
4424    '''
4425   
4426    def __init__(self,parent):
4427        style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER
4428        wx.Dialog.__init__(self, parent, wx.ID_ANY, 'Open Tutorial', style=style)
4429        self.G2frame = self.frame = parent
4430        pnl = wx.Panel(self)
4431        sizer = wx.BoxSizer(wx.VERTICAL)
4432        sizer1 = wx.BoxSizer(wx.HORIZONTAL)       
4433        label = wx.StaticText(
4434            pnl,  wx.ID_ANY,
4435            'Select the tutorial to be run and the mode of access'
4436            )
4437        msg = '''GSAS-II tutorials and their sample data files
4438        require a fair amount of storage space; few users will
4439        use all of them. This dialog allows users to load selected
4440        tutorials (along with their sample data) to their computer;
4441        optionally all tutorials can be downloaded.
4442
4443        Downloaded tutorials can be viewed and run without internet
4444        access. Tutorials can also be viewed without download, but
4445        users will need to download the sample data files manually.
4446
4447        The location used to download tutorials is set using the
4448        "Set download location" which is saved as the "Tutorial_location"
4449        configuration option see File/Preference or the
4450        config_example.py file. System managers can select to have
4451        tutorial files installed at a shared location.
4452        '''
4453        self.SetTutorialPath()
4454        hlp = HelpButton(pnl,msg)
4455        sizer1.Add((-1,-1),1, wx.EXPAND, 0)
4456        sizer1.Add(label, 0, wx.ALIGN_CENTRE|wx.ALL, 0)
4457        sizer1.Add((-1,-1),1, wx.EXPAND, 0)
4458        sizer1.Add(hlp,0,wx.ALIGN_RIGHT|wx.ALL)
4459        sizer.Add(sizer1,0,wx.EXPAND|wx.ALL,0)
4460        sizer.Add((10,10))
4461        sizer0 = wx.BoxSizer(wx.HORIZONTAL)       
4462        sizer1a = wx.BoxSizer(wx.VERTICAL)
4463        sizer1b = wx.BoxSizer(wx.VERTICAL)
4464        btn = wx.Button(pnl, wx.ID_ANY, "Download a tutorial and view")
4465        btn.Bind(wx.EVT_BUTTON, self.SelectAndDownload)
4466        sizer1a.Add(btn,0,WACV)
4467        btn = wx.Button(pnl, wx.ID_ANY, "Select from downloaded tutorials")
4468        btn.Bind(wx.EVT_BUTTON, self.onSelectDownloaded)
4469        sizer1a.Add(btn,0,WACV)
4470        btn = wx.Button(pnl, wx.ID_ANY, "Browse tutorial on web")
4471        btn.Bind(wx.EVT_BUTTON, self.onWebBrowse)
4472        sizer1a.Add(btn,0,WACV)
4473        btn = wx.Button(pnl, wx.ID_ANY, "Update downloaded tutorials")
4474        btn.Bind(wx.EVT_BUTTON, self.UpdateDownloaded)
4475        sizer1b.Add(btn,0,WACV)
4476        btn = wx.Button(pnl, wx.ID_ANY, "Download all tutorials")
4477        btn.Bind(wx.EVT_BUTTON, self.DownloadAll)
4478        sizer1b.Add(btn,0,WACV)
4479        sizer0.Add(sizer1a,0,wx.EXPAND|wx.ALL,0)
4480        sizer0.Add(sizer1b,0,wx.EXPAND|wx.ALL,0)
4481        sizer.Add(sizer0,5,wx.EXPAND|wx.ALL,5)
4482       
4483        sizer.Add((10,10))
4484        sizer1 = wx.BoxSizer(wx.HORIZONTAL)
4485        btn = wx.Button(pnl, wx.ID_ANY, "Set download location")
4486        btn.Bind(wx.EVT_BUTTON, self.SelectDownloadLoc)
4487        sizer1.Add(btn,0,WACV)
4488        self.dataLoc = wx.StaticText(pnl, wx.ID_ANY,self.tutorialPath)
4489        sizer1.Add(self.dataLoc,0,WACV)
4490        sizer.Add(sizer1)
4491       
4492        btnsizer = wx.StdDialogButtonSizer()
4493        btn = wx.Button(pnl, wx.ID_CANCEL,"Done")
4494        btnsizer.AddButton(btn)
4495        btnsizer.Realize()
4496        sizer.Add(btnsizer, 0, wx.ALIGN_CENTER|wx.ALL, 5)
4497        pnl.SetSizer(sizer)
4498        sizer.Fit(self)
4499        self.topsizer=sizer
4500        self.CenterOnParent()
4501
4502    def SetTutorialPath(self):
4503        '''Get the tutorial location if set; if not pick a default
4504        directory in a logical place
4505        '''
4506        if GSASIIpath.GetConfigValue('Tutorial_location'):
4507            self.tutorialPath = os.path.abspath(GSASIIpath.GetConfigValue('Tutorial_location'))
4508        elif (sys.platform.lower().startswith('win') and
4509              os.path.exists(os.path.abspath(os.path.expanduser('~/My Documents')))):
4510            self.tutorialPath = os.path.abspath(os.path.expanduser('~/My Documents/G2tutorials'))
4511        elif (sys.platform.lower().startswith('win') and
4512              os.path.exists(os.path.abspath(os.path.expanduser('~/Documents')))):
4513            self.tutorialPath = os.path.abspath(os.path.expanduser('~/Documents/G2tutorials'))
4514        else:
4515            self.tutorialPath = os.path.abspath(os.path.expanduser('~/G2tutorials'))
4516
4517    def SelectAndDownload(self,event):
4518        '''Make a list of all tutorials on web and allow user to choose one to
4519        download and then view
4520        '''
4521        indices = [j for j,i in enumerate(tutorialCatalog)
4522            if not os.path.exists(os.path.join(self.tutorialPath,i[0],i[1]))]
4523        if not indices:
4524            G2MessageBox(self,'All tutorials are downloaded','None to download')
4525            return
4526        choices = [tutorialCatalog[i][2] for i in indices]
4527        selected = self.ChooseTutorial(choices)
4528        if selected is None: return
4529        j = indices[selected]
4530        fullpath = os.path.join(self.tutorialPath,tutorialCatalog[j][0],tutorialCatalog[j][1])
4531        fulldir = os.path.join(self.tutorialPath,tutorialCatalog[j][0])
4532        URL = G2BaseURL+'/Tutorials/'+tutorialCatalog[j][0]+'/'
4533        if GSASIIpath.svnInstallDir(URL,fulldir):
4534            ShowWebPage(fullpath,self.frame)
4535        else:
4536            G2MessageBox(self,'Error downloading tutorial','Download error')
4537        self.EndModal(wx.ID_OK)
4538        self.G2frame.TutorialImportDir = os.path.join(self.tutorialPath,tutorialCatalog[j][0],'data')
4539
4540    def onSelectDownloaded(self,event):
4541        '''Select a previously downloaded tutorial
4542        '''
4543        indices = [j for j,i in enumerate(tutorialCatalog)
4544            if os.path.exists(os.path.join(self.tutorialPath,i[0],i[1]))]
4545        if not indices:
4546            G2MessageBox(self,
4547                         'There are no downloaded tutorials in '+self.tutorialPath,
4548                         'None downloaded')
4549            return
4550        choices = [tutorialCatalog[i][2] for i in indices]
4551        selected = self.ChooseTutorial(choices)
4552        if selected is None: return
4553        j = indices[selected]
4554        fullpath = os.path.join(self.tutorialPath,tutorialCatalog[j][0],tutorialCatalog[j][1])
4555        self.EndModal(wx.ID_OK)
4556        ShowWebPage(fullpath,self.frame)
4557        self.G2frame.TutorialImportDir = os.path.join(self.tutorialPath,tutorialCatalog[j][0],'data')
4558       
4559    def onWebBrowse(self,event):
4560        '''Make a list of all tutorials on web and allow user to view one.
4561        '''
4562        choices = [i[2] for i in tutorialCatalog]
4563        selected = self.ChooseTutorial(choices)
4564        if selected is None: return       
4565        tutdir = tutorialCatalog[selected][0]
4566        tutfil = tutorialCatalog[selected][1]
4567        # open web page remotely, don't worry about data
4568        URL = G2BaseURL+'/Tutorials/'+tutdir+'/'+tutfil
4569        self.EndModal(wx.ID_OK)
4570        ShowWebPage(URL,self.frame)
4571       
4572    def ChooseTutorial(self,choices):
4573        'choose a tutorial from a list'
4574        def onDoubleClick(event):
4575            'double-click closes the dialog'
4576            dlg.EndModal(wx.ID_OK)
4577        dlg = wx.Dialog(self,wx.ID_ANY,
4578                        'Select a tutorial to view. NB: indented ones require prerequisite',
4579                        style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)
4580        pnl = wx.Panel(dlg)
4581        sizer = wx.BoxSizer(wx.VERTICAL)
4582        listbox = wx.ListBox(pnl, wx.ID_ANY, choices=choices,
4583                             size=(450, 100),
4584                             style=wx.LB_SINGLE)
4585        sizer.Add(listbox,1,WACV|wx.EXPAND|wx.ALL,1)
4586        listbox.Bind(wx.EVT_LISTBOX_DCLICK, onDoubleClick)
4587        sizer.Add((10,10))
4588        btnsizer = wx.StdDialogButtonSizer()
4589        btn = wx.Button(pnl, wx.ID_CANCEL)
4590        btnsizer.AddButton(btn)
4591        OKbtn = wx.Button(pnl, wx.ID_OK)
4592        OKbtn.SetDefault()
4593        btnsizer.AddButton(OKbtn)
4594        btnsizer.Realize()
4595        sizer.Add((-1,5))
4596        sizer.Add(btnsizer,0,wx.ALIGN_RIGHT,50)
4597       
4598        pnl.SetSizer(sizer)
4599        sizer.Fit(dlg)
4600        self.CenterOnParent()
4601        if dlg.ShowModal() != wx.ID_OK:
4602            dlg.Destroy()
4603            return
4604        selected = listbox.GetSelection()
4605        dlg.Destroy()
4606        wx.Yield() # close window right away so user sees something happen
4607        if selected < 0: return
4608        return selected
4609
4610    def UpdateDownloaded(self,event):
4611        'Find the downloaded tutorials and run an svn update on them'
4612        updated = 0
4613        for i in tutorialCatalog:
4614            if not os.path.exists(os.path.join(self.tutorialPath,i[0],i[1])): continue
4615            print('Updating '+i[0])
4616            GSASIIpath.svnUpdateDir(os.path.join(self.tutorialPath,i[0]))
4617            updated += 0
4618        if not updated:
4619            G2MessageBox(self,'Warning, you have no downloaded tutorials','None Downloaded')
4620        self.EndModal(wx.ID_OK)
4621       
4622    def DownloadAll(self,event):
4623        'Download or update all tutorials'
4624        fail = ''
4625        for i in tutorialCatalog:
4626            if os.path.exists(os.path.join(self.tutorialPath,i[0],i[1])):
4627                print('Updating '+i[0])
4628                GSASIIpath.svnUpdateDir(os.path.join(self.tutorialPath,i[0]))
4629            else:
4630                fulldir = os.path.join(self.tutorialPath,i[0])
4631                URL = G2BaseURL+'/Tutorials/'+i[0]+'/'
4632                if not GSASIIpath.svnInstallDir(URL,fulldir):
4633                    if fail: fail += ', '
4634                    fail += i[0]
4635        if fail: 
4636            G2MessageBox(self,'Error downloading tutorial(s)\n\t'+fail,'Download error')
4637        self.EndModal(wx.ID_OK)
4638                   
4639    def SelectDownloadLoc(self,event):
4640        '''Select a download location,
4641        Cancel resets to the default
4642        '''
4643        dlg = wx.DirDialog(self, "Choose a directory for tutorial downloads:",
4644                           defaultPath=self.tutorialPath)#,style=wx.DD_DEFAULT_STYLE)
4645                           #)
4646        try:
4647            if dlg.ShowModal() != wx.ID_OK:
4648                return
4649            pth = dlg.GetPath()
4650        finally:
4651            dlg.Destroy()
4652
4653        if not os.path.exists(pth):
4654            try:
4655                os.makedirs(pth)    #failing for no obvious reason
4656            except OSError:
4657                msg = 'The selected directory is not valid.\n\t'
4658                msg += pth
4659                msg += '\n\nAn attempt to create the directory failed'
4660                G2MessageBox(self.frame,msg)
4661                return
4662        if os.path.exists(os.path.join(pth,"help")) and os.path.exists(os.path.join(pth,"Exercises")):
4663            print("Note that you may have old tutorial files in the following directories")
4664            print('\t'+os.path.join(pth,"help"))
4665            print('\t'+os.path.join(pth,"Exercises"))
4666            print('Subdirectories in the above can be deleted to save space\n\n')
4667        self.tutorialPath = pth
4668        self.dataLoc.SetLabel(self.tutorialPath)
4669        if GSASIIpath.GetConfigValue('Tutorial_location') == pth: return
4670        vars = GetConfigValsDocs()
4671        try:
4672            vars['Tutorial_location'][1] = pth
4673            if GSASIIpath.GetConfigValue('debug'): print('Saving Tutorial_location: '+pth)
4674            GSASIIpath.SetConfigValue(vars)
4675            SaveConfigVars(vars)
4676        except KeyError:
4677            pass
4678           
4679if __name__ == '__main__':
4680    app = wx.PySimpleApp()
4681    GSASIIpath.InvokeDebugOpts()
4682    frm = wx.Frame(None) # create a frame
4683    frm.Show(True)
4684   
4685    #======================================================================
4686    # test Grid with GridFractionEditor
4687    #======================================================================
4688    # tbl = [[1.,2.,3.],[1.1,2.1,3.1]]
4689    # colTypes = 3*[wg.GRID_VALUE_FLOAT+':10,5',]
4690    # Gtbl = Table(tbl,types=colTypes,rowLabels=['a','b'],colLabels=['1','2','3'])
4691    # Grid = GSGrid(frm)
4692    # Grid.SetTable(Gtbl,True)
4693    # for i in (0,1,2):
4694    #     attr = wx.grid.GridCellAttr()
4695    #     attr.IncRef()
4696    #     attr.SetEditor(GridFractionEditor(Grid))
4697    #     Grid.SetColAttr(i, attr)
4698    # frm.SetSize((400,200))
4699    # app.MainLoop()
4700    # sys.exit()
4701    #======================================================================
4702    # test Tutorial access
4703    #======================================================================
4704    # dlg = OpenTutorial(frm)
4705    # if dlg.ShowModal() == wx.ID_OK:
4706    #     print "OK"
4707    # else:
4708    #     print "Cancel"
4709    # dlg.Destroy()
4710    # sys.exit()
4711    #======================================================================
4712    # test ScrolledMultiEditor
4713    #======================================================================
4714    # Data1 = {
4715    #      'Order':1,
4716    #      'omega':'string',
4717    #      'chi':2.0,
4718    #      'phi':'',
4719    #      }
4720    # elemlst = sorted(Data1.keys())
4721    # prelbl = sorted(Data1.keys())
4722    # dictlst = len(elemlst)*[Data1,]
4723    #Data2 = [True,False,False,True]
4724    #Checkdictlst = len(Data2)*[Data2,]
4725    #Checkelemlst = range(len(Checkdictlst))
4726    # print 'before',Data1,'\n',Data2
4727    # dlg = ScrolledMultiEditor(
4728    #     frm,dictlst,elemlst,prelbl,
4729    #     checkdictlst=Checkdictlst,checkelemlst=Checkelemlst,
4730    #     checklabel="Refine?",
4731    #     header="test")
4732    # if dlg.ShowModal() == wx.ID_OK:
4733    #     print "OK"
4734    # else:
4735    #     print "Cancel"
4736    # print 'after',Data1,'\n',Data2
4737    # dlg.Destroy()
4738    Data3 = {
4739         'Order':1.0,
4740         'omega':1.1,
4741         'chi':2.0,
4742         'phi':2.3,
4743         'Order1':1.0,
4744         'omega1':1.1,
4745         'chi1':2.0,
4746         'phi1':2.3,
4747         'Order2':1.0,
4748         'omega2':1.1,
4749         'chi2':2.0,
4750         'phi2':2.3,
4751         }
4752    elemlst = sorted(Data3.keys())
4753    dictlst = len(elemlst)*[Data3,]
4754    prelbl = elemlst[:]
4755    prelbl[0]="this is a much longer label to stretch things out"
4756    Data2 = len(elemlst)*[False,]
4757    Data2[1] = Data2[3] = True
4758    Checkdictlst = len(elemlst)*[Data2,]
4759    Checkelemlst = range(len(Checkdictlst))
4760    #print 'before',Data3,'\n',Data2
4761    #print dictlst,"\n",elemlst
4762    #print Checkdictlst,"\n",Checkelemlst
4763    # dlg = ScrolledMultiEditor(
4764    #     frm,dictlst,elemlst,prelbl,
4765    #     checkdictlst=Checkdictlst,checkelemlst=Checkelemlst,
4766    #     checklabel="Refine?",
4767    #     header="test",CopyButton=True)
4768    # if dlg.ShowModal() == wx.ID_OK:
4769    #     print "OK"
4770    # else:
4771    #     print "Cancel"
4772    #print 'after',Data3,'\n',Data2
4773
4774    # Data2 = list(range(100))
4775    # elemlst += range(2,6)
4776    # postlbl += range(2,6)
4777    # dictlst += len(range(2,6))*[Data2,]
4778
4779    # prelbl = range(len(elemlst))
4780    # postlbl[1] = "a very long label for the 2nd item to force a horiz. scrollbar"
4781    # header="""This is a longer\nmultiline and perhaps silly header"""
4782    # dlg = ScrolledMultiEditor(frm,dictlst,elemlst,prelbl,postlbl,
4783    #                           header=header,CopyButton=True)
4784    # print Data1
4785    # if dlg.ShowModal() == wx.ID_OK:
4786    #     for d,k in zip(dictlst,elemlst):
4787    #         print k,d[k]
4788    # dlg.Destroy()
4789    # if CallScrolledMultiEditor(frm,dictlst,elemlst,prelbl,postlbl,
4790    #                            header=header):
4791    #     for d,k in zip(dictlst,elemlst):
4792    #         print k,d[k]
4793
4794    #======================================================================
4795    # test G2MultiChoiceDialog
4796    #======================================================================
4797    choices = []
4798    for i in range(21):
4799        choices.append("option_"+str(i))
4800    od = {
4801        'label_1':'This is a bool','value_1':True,
4802        'label_2':'This is a int','value_2':-1,
4803        'label_3':'This is a float','value_3':1.0,
4804        'label_4':'This is a string','value_4':'test',}
4805    dlg = G2MultiChoiceDialog(frm, 'Sequential refinement',
4806                              'Select dataset to include',
4807                              choices,extraOpts=od)
4808    sel = range(2,11,2)
4809    dlg.SetSelections(sel)
4810    dlg.SetSelections((1,5))
4811    if dlg.ShowModal() == wx.ID_OK:
4812        for sel in dlg.GetSelections():
4813            print sel,choices[sel]
4814    print od
4815    od = {}
4816    dlg = G2MultiChoiceDialog(frm, 'Sequential refinement',
4817                              'Select dataset to include',
4818                              choices,extraOpts=od)
4819    sel = range(2,11,2)
4820    dlg.SetSelections(sel)
4821    dlg.SetSelections((1,5))
4822    if dlg.ShowModal() == wx.ID_OK: pass
4823    #======================================================================
4824    # test wx.MultiChoiceDialog
4825    #======================================================================
4826    # dlg = wx.MultiChoiceDialog(frm, 'Sequential refinement',
4827    #                           'Select dataset to include',
4828    #                           choices)
4829    # sel = range(2,11,2)
4830    # dlg.SetSelections(sel)
4831    # dlg.SetSelections((1,5))
4832    # if dlg.ShowModal() == wx.ID_OK:
4833    #     for sel in dlg.GetSelections():
4834    #         print sel,choices[sel]
4835
4836    # pnl = wx.Panel(frm)
4837    # siz = wx.BoxSizer(wx.VERTICAL)
4838
4839    # td = {'Goni':200.,'a':1.,'calc':1./3.,'string':'s'}
4840    # for key in sorted(td):
4841    #     txt = ValidatedTxtCtrl(pnl,td,key)
4842    #     siz.Add(txt)
4843    # pnl.SetSizer(siz)
4844    # siz.Fit(frm)
4845    # app.MainLoop()
4846    # print td
Note: See TracBrowser for help on using the repository browser.