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

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

massive changes of wxID_ ids & misc to fix undefined vars wanings

  • Property svn:eol-style set to native
  • Property svn:keywords set to Date Author Revision URL Id
File size: 199.6 KB
Line 
1# -*- coding: utf-8 -*-
2#GSASIIctrlGUI - Custom GSAS-II GUI controls
3########### SVN repository information ###################
4# $Date: 2017-07-07 21:45:18 +0000 (Fri, 07 Jul 2017) $
5# $Author: toby $
6# $Revision: 2917 $
7# $URL: branch/2frame/GSASIIctrlGUI.py $
8# $Id: GSASIIctrlGUI.py 2917 2017-07-07 21:45:18Z toby $
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: 2917 $")
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        # provide special help topic names for extra items in help menu
3506        for lbl,indx in morehelpitems:
3507            helpobj = self.Append(text=lbl,
3508                id=wx.ID_ANY, kind=wx.ITEM_NORMAL)
3509            frame.Bind(wx.EVT_MENU, self.OnHelpById, helpobj)
3510            self.HelpById[helpobj.GetId()] = indx
3511        # add help lookup(s) in gsasii.html
3512        self.AppendSeparator()
3513        if includeTree:
3514            helpobj = self.Append(text='Help on Data tree',
3515                                  id=wx.ID_ANY, kind=wx.ITEM_NORMAL)
3516            frame.Bind(wx.EVT_MENU, self.OnHelpById, id=helpobj.GetId())
3517            self.HelpById[helpobj.GetId()] = 'Data tree'
3518        helpobj = self.Append(text='Help on current data tree item',id=wx.ID_ANY, kind=wx.ITEM_NORMAL)
3519        frame.Bind(wx.EVT_MENU, self.OnHelpById, id=helpobj.GetId())
3520       
3521    def OnHelpById(self,event):
3522        '''Called when Help on... is pressed in a menu. Brings up a web page
3523        for documentation. Uses the helpKey value from the dataWindow window
3524        unless a special help key value has been defined for this menu id in
3525        self.HelpById
3526
3527        Note that self should now (2frame) be child of the main window (G2frame)
3528        '''
3529        if hasattr(self.frame,'dataWindow'):  # Debug code: check this is called from menu in G2frame
3530            # should always be true in 2 Frame version
3531            dW = self.frame.dataWindow
3532        else:
3533            print('help error: not called from standard menu?')
3534            print self
3535            return           
3536        try:
3537            helpKey = dW.helpKey # look up help from helpKey in data window
3538            #if GSASIIpath.GetConfigValue('debug'): print 'dataWindow help: key=',helpKey
3539        except AttributeError:
3540            helpKey = ''
3541            if GSASIIpath.GetConfigValue('debug'): print('No helpKey for current dataWindow!')
3542        helpType = self.HelpById.get(event.GetId(),helpKey) # see if there is a special help topic
3543        #if GSASIIpath.GetConfigValue('debug'): print 'helpKey=',helpKey,'  helpType=',helpType
3544        if helpType == 'Tutorials':
3545            dlg = OpenTutorial(self.frame)
3546            dlg.ShowModal()
3547            dlg.Destroy()
3548            return
3549        else:
3550            ShowHelp(helpType,self.frame)
3551
3552    def OnHelpAbout(self, event):
3553        "Display an 'About GSAS-II' box"
3554        import GSASII
3555        info = wx.AboutDialogInfo()
3556        info.Name = 'GSAS-II'
3557        ver = GSASIIpath.svnGetRev()
3558        if ver: 
3559            info.Version = 'Revision '+str(ver)+' (svn), version '+GSASII.__version__
3560        else:
3561            info.Version = 'Revision '+str(GSASIIpath.GetVersionNumber())+' (.py files), version '+GSASII.__version__
3562        #info.Developers = ['Robert B. Von Dreele','Brian H. Toby']
3563        info.Copyright = ('(c) ' + time.strftime('%Y') +
3564''' Argonne National Laboratory
3565This product includes software developed
3566by the UChicago Argonne, LLC, as
3567Operator of Argonne National Laboratory.''')
3568        info.Description = '''General Structure Analysis System-II (GSAS-II)
3569Robert B. Von Dreele and Brian H. Toby
3570
3571Please cite as:
3572  B.H. Toby & R.B. Von Dreele, J. Appl. Cryst. 46, 544-549 (2013)
3573For small angle use cite:
3574  R.B. Von Dreele, J. Appl. Cryst. 47, 1748-9 (2014)
3575For DIFFaX use cite:
3576  M.M.J. Treacy, J.M. Newsam & M.W. Deem,
3577  Proc. Roy. Soc. Lond. A 433, 499-520 (1991)
3578'''
3579        info.WebSite = ("https://subversion.xray.aps.anl.gov/trac/pyGSAS","GSAS-II home page")
3580        wx.AboutBox(info)
3581
3582    def OnCheckUpdates(self,event):
3583        '''Check if the GSAS-II repository has an update for the current source files
3584        and perform that update if requested.
3585        '''           
3586        if not GSASIIpath.whichsvn():
3587            dlg = wx.MessageDialog(self.frame,
3588                                   'No Subversion','Cannot update GSAS-II because subversion (svn) was not found.',
3589                                   wx.OK)
3590            dlg.ShowModal()
3591            dlg.Destroy()
3592            return
3593        wx.BeginBusyCursor()
3594        local = GSASIIpath.svnGetRev()
3595        if local is None: 
3596            wx.EndBusyCursor()
3597            dlg = wx.MessageDialog(self.frame,
3598                                   'Unable to run subversion on the GSAS-II current directory. Is GSAS-II installed correctly?',
3599                                   'Subversion error',
3600                                   wx.OK)
3601            dlg.ShowModal()
3602            dlg.Destroy()
3603            return
3604        print 'Installed GSAS-II version: '+local
3605        repos = GSASIIpath.svnGetRev(local=False)
3606        wx.EndBusyCursor()
3607        if repos is None: 
3608            dlg = wx.MessageDialog(self.frame,
3609                                   'Unable to access the GSAS-II server. Is this computer on the internet?',
3610                                   'Server unavailable',
3611                                   wx.OK)
3612            dlg.ShowModal()
3613            dlg.Destroy()
3614            return
3615        print 'GSAS-II version on server: '+repos
3616        if local == repos:
3617            dlg = wx.MessageDialog(self.frame,
3618                                   'GSAS-II is up-to-date. Version '+local+' is already loaded.',
3619                                   'GSAS-II Up-to-date',
3620                                   wx.OK)
3621            dlg.ShowModal()
3622            dlg.Destroy()
3623            return
3624        mods = GSASIIpath.svnFindLocalChanges()
3625        if mods:
3626            dlg = wx.MessageDialog(self.frame,
3627                                   'You have version '+local+
3628                                   ' of GSAS-II installed, but the current version is '+repos+
3629                                   '. However, '+str(len(mods))+
3630                                   ' file(s) on your local computer have been modified.'
3631                                   ' Updating will attempt to merge your local changes with '
3632                                   'the latest GSAS-II version, but if '
3633                                   'conflicts arise, local changes will be '
3634                                   'discarded. It is also possible that the '
3635                                   'local changes my prevent GSAS-II from running. '
3636                                   'Press OK to start an update if this is acceptable:',
3637                                   'Local GSAS-II Mods',
3638                                   wx.OK|wx.CANCEL)
3639            if dlg.ShowModal() != wx.ID_OK:
3640                dlg.Destroy()
3641                return
3642            else:
3643                dlg.Destroy()
3644        else:
3645            dlg = wx.MessageDialog(self.frame,
3646                                   'You have version '+local+
3647                                   ' of GSAS-II installed, but the current version is '+repos+
3648                                   '. Press OK to start an update:',
3649                                   'GSAS-II Updates',
3650                                   wx.OK|wx.CANCEL)
3651            if dlg.ShowModal() != wx.ID_OK:
3652                dlg.Destroy()
3653                return
3654            dlg.Destroy()
3655        print 'start updates'
3656        dlg = wx.MessageDialog(self.frame,
3657                               'Your project will now be saved, GSAS-II will exit and an update '
3658                               'will be performed and GSAS-II will restart. Press Cancel to '
3659                               'abort the update',
3660                               'Start update?',
3661                               wx.OK|wx.CANCEL)
3662        if dlg.ShowModal() != wx.ID_OK:
3663            dlg.Destroy()
3664            return
3665        dlg.Destroy()
3666        try:
3667            self.frame.OnFileSave(event)
3668            GPX = self.frame.GSASprojectfile
3669        except AttributeError:
3670            self.frame.G2frame.OnFileSave(event)
3671            GPX = self.frame.G2frame.GSASprojectfile
3672        GSASIIpath.svnUpdateProcess(projectfile=GPX)
3673        return
3674
3675    def OnSelectVersion(self,event):
3676        '''Allow the user to select a specific version of GSAS-II
3677        '''
3678        if not GSASIIpath.whichsvn():
3679            dlg = wx.MessageDialog(self,'No Subversion','Cannot update GSAS-II because subversion (svn) '+
3680                                   'was not found.'
3681                                   ,wx.OK)
3682            dlg.ShowModal()
3683            return
3684        local = GSASIIpath.svnGetRev()
3685        if local is None: 
3686            dlg = wx.MessageDialog(self.frame,
3687                                   'Unable to run subversion on the GSAS-II current directory. Is GSAS-II installed correctly?',
3688                                   'Subversion error',
3689                                   wx.OK)
3690            dlg.ShowModal()
3691            dlg.Destroy()
3692            return
3693        mods = GSASIIpath.svnFindLocalChanges()
3694        if mods:
3695            dlg = wx.MessageDialog(self.frame,
3696                                   'You have version '+local+
3697                                   ' of GSAS-II installed'
3698                                   '. However, '+str(len(mods))+
3699                                   ' file(s) on your local computer have been modified.'
3700                                   ' Downdating will attempt to merge your local changes with '
3701                                   'the selected GSAS-II version. '
3702                                   'Downdating is not encouraged because '
3703                                   'if merging is not possible, your local changes will be '
3704                                   'discarded. It is also possible that the '
3705                                   'local changes my prevent GSAS-II from running. '
3706                                   'Press OK to continue anyway.',
3707                                   'Local GSAS-II Mods',
3708                                   wx.OK|wx.CANCEL)
3709            if dlg.ShowModal() != wx.ID_OK:
3710                dlg.Destroy()
3711                return
3712            dlg.Destroy()
3713        if GSASIIpath.svnGetRev(local=False) is None:
3714            dlg = wx.MessageDialog(self.frame,
3715                                   'Error obtaining current GSAS-II version. Is internet access working correctly?',
3716                                   'Subversion error',
3717                                   wx.OK)
3718            dlg.ShowModal()
3719            dlg.Destroy()
3720            return
3721        dlg = downdate(parent=self.frame)
3722        if dlg.ShowModal() == wx.ID_OK:
3723            ver = dlg.getVersion()
3724        else:
3725            dlg.Destroy()
3726            return
3727        dlg.Destroy()
3728        print('start regress to '+str(ver))
3729        try:
3730            self.frame.OnFileSave(event)
3731            GPX = self.frame.GSASprojectfile
3732        except AttributeError:
3733            self.frame.G2frame.OnFileSave(event)
3734            GPX = self.frame.G2frame.GSASprojectfile
3735        GSASIIpath.svnUpdateProcess(projectfile=GPX,version=str(ver))
3736        return
3737
3738################################################################################
3739class HelpButton(wx.Button):
3740    '''Create a help button that displays help information.
3741    The text is displayed in a modal message window.
3742
3743    TODO: it might be nice if it were non-modal: e.g. it stays around until
3744    the parent is deleted or the user closes it, but this did not work for
3745    me.
3746
3747    :param parent: the panel which will be the parent of the button
3748    :param str msg: the help text to be displayed
3749    '''
3750    def __init__(self,parent,msg):
3751        if sys.platform == "darwin": 
3752            wx.Button.__init__(self,parent,wx.ID_HELP)
3753        else:
3754            wx.Button.__init__(self,parent,wx.ID_ANY,'?',style=wx.BU_EXACTFIT)
3755        self.Bind(wx.EVT_BUTTON,self._onPress)
3756        self.msg=StripIndents(msg)
3757        self.parent = parent
3758    def _onClose(self,event):
3759        self.dlg.EndModal(wx.ID_CANCEL)
3760    def _onPress(self,event):
3761        'Respond to a button press by displaying the requested text'
3762        #dlg = wx.MessageDialog(self.parent,self.msg,'Help info',wx.OK)
3763        self.dlg = wx.Dialog(self.parent,wx.ID_ANY,'Help information', 
3764                        style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)
3765        #self.dlg.SetBackgroundColour(wx.WHITE)
3766        mainSizer = wx.BoxSizer(wx.VERTICAL)
3767        txt = wx.StaticText(self.dlg,wx.ID_ANY,self.msg)
3768        mainSizer.Add(txt,1,wx.ALL|wx.EXPAND,10)
3769        txt.SetBackgroundColour(wx.WHITE)
3770
3771        btnsizer = wx.BoxSizer(wx.HORIZONTAL)
3772        btn = wx.Button(self.dlg, wx.ID_CLOSE) 
3773        btn.Bind(wx.EVT_BUTTON,self._onClose)
3774        btnsizer.Add(btn)
3775        mainSizer.Add(btnsizer, 0, wx.ALIGN_CENTER|wx.ALL, 5)
3776        self.dlg.SetSizer(mainSizer)
3777        mainSizer.Fit(self.dlg)
3778        self.dlg.CenterOnParent()
3779        self.dlg.ShowModal()
3780        self.dlg.Destroy()
3781################################################################################
3782class MyHtmlPanel(wx.Panel):
3783    '''Defines a panel to display HTML help information, as an alternative to
3784    displaying help information in a web browser.
3785    '''
3786    def __init__(self, frame, id):
3787        self.frame = frame
3788        wx.Panel.__init__(self, frame, id)
3789        sizer = wx.BoxSizer(wx.VERTICAL)
3790        back = wx.Button(self, -1, "Back")
3791        back.Bind(wx.EVT_BUTTON, self.OnBack)
3792        self.htmlwin = G2HtmlWindow(self, id, size=(750,450))
3793        sizer.Add(self.htmlwin, 1,wx.EXPAND)
3794        sizer.Add(back, 0, wx.ALIGN_LEFT, 0)
3795        self.SetSizer(sizer)
3796        sizer.Fit(frame)       
3797        self.Bind(wx.EVT_SIZE,self.OnHelpSize)
3798    def OnHelpSize(self,event):         #does the job but weirdly!!
3799        anchor = self.htmlwin.GetOpenedAnchor()
3800        if anchor:           
3801            self.htmlwin.ScrollToAnchor(anchor)
3802            wx.CallAfter(self.htmlwin.ScrollToAnchor,anchor)
3803            if event: event.Skip()
3804    def OnBack(self, event):
3805        self.htmlwin.HistoryBack()
3806    def LoadFile(self,file):
3807        pos = file.rfind('#')
3808        if pos != -1:
3809            helpfile = file[:pos]
3810            helpanchor = file[pos+1:]
3811        else:
3812            helpfile = file
3813            helpanchor = None
3814        self.htmlwin.LoadPage(helpfile)
3815        if helpanchor is not None:
3816            self.htmlwin.ScrollToAnchor(helpanchor)
3817            xs,ys = self.htmlwin.GetViewStart()
3818            self.htmlwin.Scroll(xs,ys-1)
3819################################################################################
3820class G2HtmlWindow(wx.html.HtmlWindow):
3821    '''Displays help information in a primitive HTML browser type window
3822    '''
3823    def __init__(self, parent, *args, **kwargs):
3824        self.parent = parent
3825        wx.html.HtmlWindow.__init__(self, parent, *args, **kwargs)
3826    def LoadPage(self, *args, **kwargs):
3827        wx.html.HtmlWindow.LoadPage(self, *args, **kwargs)
3828        self.TitlePage()
3829    def OnLinkClicked(self, *args, **kwargs):
3830        wx.html.HtmlWindow.OnLinkClicked(self, *args, **kwargs)
3831        xs,ys = self.GetViewStart()
3832        self.Scroll(xs,ys-1)
3833        self.TitlePage()
3834    def HistoryBack(self, *args, **kwargs):
3835        wx.html.HtmlWindow.HistoryBack(self, *args, **kwargs)
3836        self.TitlePage()
3837    def TitlePage(self):
3838        self.parent.frame.SetTitle(self.GetOpenedPage() + ' -- ' + 
3839            self.GetOpenedPageTitle())
3840
3841################################################################################
3842def StripIndents(msg):
3843    'Strip indentation from multiline strings'
3844    msg1 = msg.replace('\n ','\n')
3845    while msg != msg1:
3846        msg = msg1
3847        msg1 = msg.replace('\n ','\n')
3848    return msg.replace('\n\t','\n')
3849
3850def StripUnicode(string,subs='.'):
3851    '''Strip non-ASCII characters from strings
3852   
3853    :param str string: string to strip Unicode characters from
3854    :param str subs: character(s) to place into string in place of each
3855      Unicode character. Defaults to '.'
3856
3857    :returns: a new string with only ASCII characters
3858    '''
3859    s = ''
3860    for c in string:
3861        if ord(c) < 128:
3862            s += c
3863        else:
3864            s += subs
3865    return s.encode('ascii','replace')
3866       
3867################################################################################
3868# configuration routines (for editing config.py)
3869def SaveGPXdirectory(path):
3870    if GSASIIpath.GetConfigValue('Starting_directory') == path: return
3871    vars = GetConfigValsDocs()
3872    try:
3873        vars['Starting_directory'][1] = path
3874        if GSASIIpath.GetConfigValue('debug'): print('Saving GPX path: '+path)
3875        SaveConfigVars(vars)
3876    except KeyError:
3877        pass
3878
3879def SaveImportDirectory(path):
3880    if GSASIIpath.GetConfigValue('Import_directory') == path: return
3881    vars = GetConfigValsDocs()
3882    try:
3883        vars['Import_directory'][1] = path
3884        if GSASIIpath.GetConfigValue('debug'): print('Saving Import path: '+path)
3885        SaveConfigVars(vars)
3886    except KeyError:
3887        pass
3888
3889def GetConfigValsDocs():
3890    '''Reads the module referenced in fname (often <module>.__file__) and
3891    return a dict with names of global variables as keys.
3892    For each global variable, the value contains four items:
3893
3894    :returns: a dict where keys are names defined in module config_example.py
3895      where the value is a list of four items, as follows:
3896
3897         * item 0: the default value
3898         * item 1: the current value
3899         * item 2: the initial value (starts same as item 1)
3900         * item 3: the "docstring" that follows variable definition
3901
3902    '''
3903    import config_example
3904    import ast
3905    fname = os.path.splitext(config_example.__file__)[0]+'.py' # convert .pyc to .py
3906    with open(fname, 'r') as f:
3907        fstr = f.read()
3908    fstr = fstr.replace('\r\n', '\n').replace('\r', '\n')
3909    if not fstr.endswith('\n'):
3910        fstr += '\n'
3911    tree = ast.parse(fstr)
3912    d = {}
3913    key = None
3914    for node in ast.walk(tree):
3915        if isinstance(node,ast.Assign):
3916            key = node.targets[0].id
3917            d[key] = [config_example.__dict__.get(key),
3918                      GSASIIpath.configDict.get(key),
3919                      GSASIIpath.configDict.get(key),'']
3920        elif isinstance(node,ast.Expr) and key:
3921            d[key][3] = node.value.s.strip()
3922        else:
3923            key = None
3924    return d
3925
3926def SaveConfigVars(vars,parent=None):
3927    '''Write the current config variable values to config.py
3928
3929    :params dict vars: a dictionary of variable settings and meanings as
3930      created in :func:`GetConfigValsDocs`.
3931    :param parent: wx.Frame object or None (default) for parent
3932      of error message if no file can be written.
3933    :returns: True if unable to write the file, None otherwise
3934    '''
3935    # try to write to where an old config file is located
3936    try:
3937        import config
3938        savefile = config.__file__
3939    except ImportError: # no config.py file yet
3940        savefile = os.path.join(GSASIIpath.path2GSAS2,'config.py')
3941    # try to open file for write
3942    try:
3943        savefile = os.path.splitext(savefile)[0]+'.py' # convert .pyc to .py
3944        fp = open(savefile,'w')
3945    except IOError:  # can't write there, write in local mods directory
3946        # create a local mods directory, if needed
3947        if not os.path.exists(os.path.expanduser('~/.G2local/')):
3948            print('Creating directory '+os.path.expanduser('~/.G2local/'))
3949            os.mkdir(os.path.expanduser('~/.G2local/'))
3950            sys.path.insert(0,os.path.expanduser('~/.G2local/'))
3951        savefile = os.path.join(os.path.expanduser('~/.G2local/'),'config.py')
3952        try:
3953            fp = open(savefile,'w')
3954        except IOError:
3955            if parent:
3956                G2MessageBox(parent,'Error trying to write configuration to '+savefile,
3957                    'Unable to save')
3958            else:
3959                print('Error trying to write configuration to '+savefile)
3960            return True
3961    import datetime
3962    fp.write("'''\n")
3963    fp.write("*config.py: Configuration options*\n----------------------------------\n")
3964    fp.write("This file created in SelectConfigSetting on {:%d %b %Y %H:%M}\n".
3965             format(datetime.datetime.now()))
3966    fp.write("'''\n\n")
3967    fp.write("import os.path\n")
3968    fp.write("import GSASIIpath\n\n")
3969    for var in sorted(vars.keys(),key=lambda s: s.lower()):
3970        if vars[var][1] is None: continue
3971        if vars[var][1] == '': continue
3972        if vars[var][0] == vars[var][1]: continue
3973        try:
3974            float(vars[var][1]) # test for number
3975            fp.write(var + ' = ' + str(vars[var][1])+'\n')
3976        except:
3977            try:
3978                eval(vars[var][1]) # test for an expression
3979                fp.write(var + ' = ' + str(vars[var][1])+'\n')
3980            except: # must be a string
3981                varstr = vars[var][1]
3982                if '\\' in varstr:
3983                    fp.write(var + ' = os.path.normpath("' + varstr.replace('\\','/') +'")\n')
3984                else:
3985                    fp.write(var + ' = "' + str(varstr)+'"\n')
3986        if vars[var][3]:
3987            fp.write("'''" + str(vars[var][3]) + "\n'''\n\n")
3988    fp.close()
3989    print('wrote file '+savefile)
3990
3991class SelectConfigSetting(wx.Dialog):
3992    '''Dialog to select configuration variables and set associated values.
3993    '''
3994    def __init__(self,parent=None):
3995        style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER
3996        wx.Dialog.__init__(self, parent, wx.ID_ANY, 'Set Config Variable', style=style)
3997        self.sizer = wx.BoxSizer(wx.VERTICAL)
3998        self.vars = GetConfigValsDocs()
3999       
4000        label = wx.StaticText(
4001            self,  wx.ID_ANY,
4002            'Select a GSAS-II configuration variable to change'
4003            )
4004        self.sizer.Add(label, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
4005        self.choice = {}
4006        btn = G2ChoiceButton(self, sorted(self.vars.keys(),key=lambda s: s.lower()),
4007            strLoc=self.choice,strKey=0,onChoice=self.OnSelection)
4008        btn.SetLabel("")
4009        self.sizer.Add(btn)
4010
4011        self.varsizer = wx.BoxSizer(wx.VERTICAL)
4012        self.sizer.Add(self.varsizer,1,wx.ALL|wx.EXPAND,1)
4013       
4014        self.doclbl = wx.StaticBox(self, wx.ID_ANY, "")
4015        self.doclblsizr = wx.StaticBoxSizer(self.doclbl)
4016        self.docinfo = wx.StaticText(self,  wx.ID_ANY, "")
4017        self.doclblsizr.Add(self.docinfo, 0, wx.ALIGN_LEFT|wx.ALL, 5)
4018        self.sizer.Add(self.doclblsizr, 0, wx.ALIGN_LEFT|wx.EXPAND|wx.ALL, 5)
4019        btnsizer = wx.BoxSizer(wx.HORIZONTAL)
4020        self.saveBtn = wx.Button(self,-1,"Save current settings")
4021        btnsizer.Add(self.saveBtn, 0, wx.ALL, 2) 
4022        self.saveBtn.Bind(wx.EVT_BUTTON, self.OnSave)
4023        self.saveBtn.Enable(False)
4024        self.applyBtn = wx.Button(self,-1,"Use current (no save)")
4025        btnsizer.Add(self.applyBtn, 0, wx.ALL, 2) 
4026        self.applyBtn.Bind(wx.EVT_BUTTON, self.OnApplyChanges)
4027        self.applyBtn.Enable(False)
4028       
4029        btn = wx.Button(self,wx.ID_CANCEL)
4030        btnsizer.Add(btn, 0, wx.ALL, 2) 
4031        self.sizer.Add(btnsizer, 0, wx.ALIGN_CENTRE|wx.ALL, 5) 
4032               
4033        self.SetSizer(self.sizer)
4034        self.sizer.Fit(self)
4035        self.CenterOnParent()
4036       
4037    def OnChange(self,event=None):
4038        ''' Check if anything been changed. Turn the save button on/off.
4039        '''
4040        for var in self.vars:
4041            if self.vars[var][0] is None and self.vars[var][1] is not None:
4042                # make blank strings into None, if that is the default
4043                if self.vars[var][1].strip() == '': self.vars[var][1] = None
4044            if self.vars[var][1] != self.vars[var][2]:
4045                #print 'changed',var,self.vars[var][:3]
4046                self.saveBtn.Enable(True)
4047                self.applyBtn.Enable(True)
4048                break
4049        else:
4050            self.saveBtn.Enable(False)
4051            self.applyBtn.Enable(False)
4052        try:
4053            self.resetBtn.Enable(True)
4054        except:
4055            pass
4056       
4057    def OnApplyChanges(self,event=None):
4058        'Set config variables to match the current settings'
4059        GSASIIpath.SetConfigValue(self.vars)
4060        self.EndModal(wx.ID_OK)
4061       
4062    def OnSave(self,event):
4063        '''Write the config variables to config.py and then set them
4064        as the current settings
4065        '''
4066        if not SaveConfigVars(self.vars,parent=self):
4067            self.OnApplyChanges() # force a reload of the config settings
4068            self.EndModal(wx.ID_OK)
4069
4070    def OnBoolSelect(self,event):
4071        'Respond to a change in a True/False variable'
4072        rb = event.GetEventObject()
4073        var = self.choice[0]
4074        self.vars[var][1] = (rb.GetSelection() == 0)
4075        self.OnChange()
4076        wx.CallAfter(self.OnSelection)
4077       
4078    def onSelDir(self,event):
4079        'Select a directory from a menu'
4080        dlg = wx.DirDialog(self, "Choose a directory:",style=wx.DD_DEFAULT_STYLE)
4081        if dlg.ShowModal() == wx.ID_OK:
4082            var = self.choice[0]
4083            self.vars[var][1] = dlg.GetPath()
4084            self.strEd.SetValue(self.vars[var][1])
4085            self.OnChange()
4086        dlg.Destroy()
4087       
4088    def OnSelection(self):
4089        'show a selected variable'
4090        def OnNewColorBar(event):
4091            self.vars['Contour_color'][1] = self.colSel.GetValue()
4092            self.OnChange(event)
4093
4094        self.varsizer.DeleteWindows()
4095        var = self.choice[0]
4096        showdef = True
4097        if var not in self.vars:
4098            raise Exception,"How did this happen?"
4099        if type(self.vars[var][0]) is int:
4100            ed = ValidatedTxtCtrl(self,self.vars[var],1,typeHint=int,OKcontrol=self.OnChange)
4101            self.varsizer.Add(ed, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
4102        elif type(self.vars[var][0]) is float:
4103            ed = ValidatedTxtCtrl(self,self.vars[var],1,typeHint=float,OKcontrol=self.OnChange)
4104            self.varsizer.Add(ed, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
4105        elif type(self.vars[var][0]) is bool:
4106            showdef = False
4107            lbl = "value for "+var
4108            ch = []
4109            for i,v in enumerate((True,False)):
4110                s = str(v)
4111                if v == self.vars[var][0]:
4112                    defopt = i
4113                    s += ' (default)'
4114                ch += [s]
4115            rb = wx.RadioBox(self, wx.ID_ANY, lbl, wx.DefaultPosition, wx.DefaultSize,
4116                ch, 1, wx.RA_SPECIFY_COLS)
4117            # set initial value
4118            if self.vars[var][1] is None:
4119                rb.SetSelection(defopt)
4120            elif self.vars[var][1]:
4121                rb.SetSelection(0)
4122            else:
4123                rb.SetSelection(1)
4124            rb.Bind(wx.EVT_RADIOBOX,self.OnBoolSelect)
4125            self.varsizer.Add(rb, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
4126        else:
4127            if var.endswith('_directory') or var.endswith('_location'):
4128                btn = wx.Button(self,wx.ID_ANY,'Select from dialog...')
4129                sz = (400,-1)
4130            else:
4131                btn = None
4132                sz = (250,-1)
4133            if var == 'Contour_color':
4134                if self.vars[var][1] is None:
4135                    self.vars[var][1] = 'paired'
4136                colorList = sorted([m for m in mpl.cm.datad.keys() ],key=lambda s: s.lower())   #if not m.endswith("_r")
4137                self.colSel = wx.ComboBox(self,value=self.vars[var][1],choices=colorList,
4138                    style=wx.CB_READONLY|wx.CB_DROPDOWN)
4139                self.colSel.Bind(wx.EVT_COMBOBOX, OnNewColorBar)
4140                self.varsizer.Add(self.colSel, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
4141            else:
4142                self.strEd = ValidatedTxtCtrl(self,self.vars[var],1,typeHint=str,
4143                    OKcontrol=self.OnChange,size=sz)
4144                if self.vars[var][1] is not None:
4145                    self.strEd.SetValue(self.vars[var][1])
4146                self.varsizer.Add(self.strEd, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
4147            if btn:
4148                btn.Bind(wx.EVT_BUTTON,self.onSelDir)
4149                self.varsizer.Add(btn, 0, wx.ALIGN_CENTRE|wx.ALL, 5) 
4150        # button for reset to default value
4151        lbl = "Reset to Default"
4152        if showdef: # spell out default when needed
4153            lbl += ' (='+str(self.vars[var][0])+')'
4154            #label = wx.StaticText(self,  wx.ID_ANY, 'Default value = '+str(self.vars[var][0]))
4155            #self.varsizer.Add(label, 0, wx.ALIGN_LEFT|wx.ALL, 5)
4156        self.resetBtn = wx.Button(self,-1,lbl)
4157        self.resetBtn.Bind(wx.EVT_BUTTON, self.OnClear)
4158        if self.vars[var][1] is not None and self.vars[var][1] != '': # show current value, if one
4159            #label = wx.StaticText(self,  wx.ID_ANY, 'Current value = '+str(self.vars[var][1]))
4160            #self.varsizer.Add(label, 0, wx.ALIGN_LEFT|wx.ALL, 5)
4161            self.resetBtn.Enable(True)
4162        else:
4163            self.resetBtn.Enable(False)
4164        self.varsizer.Add(self.resetBtn, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
4165        # show meaning, if defined
4166        self.doclbl.SetLabel("Description of "+str(var)) 
4167        if self.vars[var][3]:
4168            self.docinfo.SetLabel(self.vars[var][3])
4169        else:
4170            self.docinfo.SetLabel("(not documented)")
4171        self.sizer.Fit(self)
4172        self.CenterOnParent()
4173        wx.CallAfter(self.SendSizeEvent)
4174
4175    def OnClear(self, event):
4176        var = self.choice[0]
4177        self.vars[var][1] = self.vars[var][0]
4178        self.OnChange()
4179        wx.CallAfter(self.OnSelection)
4180       
4181################################################################################
4182class downdate(wx.Dialog):
4183    '''Dialog to allow a user to select a version of GSAS-II to install
4184    '''
4185    def __init__(self,parent=None):
4186        style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER
4187        wx.Dialog.__init__(self, parent, wx.ID_ANY, 'Select Version', style=style)
4188        pnl = wx.Panel(self)
4189        sizer = wx.BoxSizer(wx.VERTICAL)
4190        insver = GSASIIpath.svnGetRev(local=True)
4191        curver = int(GSASIIpath.svnGetRev(local=False))
4192        label = wx.StaticText(
4193            pnl,  wx.ID_ANY,
4194            'Select a specific GSAS-II version to install'
4195            )
4196        sizer.Add(label, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
4197        sizer1 = wx.BoxSizer(wx.HORIZONTAL)
4198        sizer1.Add(
4199            wx.StaticText(pnl,  wx.ID_ANY,
4200                          'Currently installed version: '+str(insver)),
4201            0, wx.ALIGN_CENTRE|wx.ALL, 5)
4202        sizer.Add(sizer1)
4203        sizer1 = wx.BoxSizer(wx.HORIZONTAL)
4204        sizer1.Add(
4205            wx.StaticText(pnl,  wx.ID_ANY,
4206                          'Select GSAS-II version to install: '),
4207            0, wx.ALIGN_CENTRE|wx.ALL, 5)
4208        self.spin = wx.SpinCtrl(pnl, wx.ID_ANY,size=(150,-1))
4209        self.spin.SetRange(1, curver)
4210        self.spin.SetValue(curver)
4211        self.Bind(wx.EVT_SPINCTRL, self._onSpin, self.spin)
4212        self.Bind(wx.EVT_KILL_FOCUS, self._onSpin, self.spin)
4213        sizer1.Add(self.spin)
4214        sizer.Add(sizer1)
4215
4216        line = wx.StaticLine(pnl,-1, size=(-1,3), style=wx.LI_HORIZONTAL)
4217        sizer.Add(line, 0, wx.EXPAND|wx.ALIGN_CENTER|wx.ALL, 10)
4218
4219        self.text = wx.StaticText(pnl,  wx.ID_ANY, "")
4220        sizer.Add(self.text, 0, wx.ALIGN_LEFT|wx.EXPAND|wx.ALL, 5)
4221
4222        line = wx.StaticLine(pnl,-1, size=(-1,3), style=wx.LI_HORIZONTAL)
4223        sizer.Add(line, 0, wx.EXPAND|wx.ALIGN_CENTER|wx.ALL, 10)
4224        sizer.Add(
4225            wx.StaticText(
4226                pnl,  wx.ID_ANY,
4227                'If "Install" is pressed, your project will be saved;\n'
4228                'GSAS-II will exit; The specified version will be loaded\n'
4229                'and GSAS-II will restart. Press "Cancel" to abort.'),
4230            0, wx.EXPAND|wx.ALL, 10)
4231        btnsizer = wx.StdDialogButtonSizer()
4232        btn = wx.Button(pnl, wx.ID_OK, "Install")
4233        btn.SetDefault()
4234        btnsizer.AddButton(btn)
4235        btn = wx.Button(pnl, wx.ID_CANCEL)
4236        btnsizer.AddButton(btn)
4237        btnsizer.Realize()
4238        sizer.Add(btnsizer, 0, wx.ALIGN_CENTER|wx.ALL, 5)
4239        pnl.SetSizer(sizer)
4240        sizer.Fit(self)
4241        self.topsizer=sizer
4242        self.CenterOnParent()
4243        self._onSpin(None)
4244
4245    def _onSpin(self,event):
4246        'Called to load info about the selected version in the dialog'
4247        if event: event.Skip()
4248        ver = self.spin.GetValue()
4249        d = GSASIIpath.svnGetLog(version=ver)
4250        date = d.get('date','?').split('T')[0]
4251        s = '(Version '+str(ver)+' created '+date
4252        s += ' by '+d.get('author','?')+')'
4253        msg = d.get('msg')
4254        if msg: s += '\n\nComment: '+msg
4255        self.text.SetLabel(s)
4256        self.topsizer.Fit(self)
4257
4258    def getVersion(self):
4259        'Get the version number in the dialog'
4260        return self.spin.GetValue()
4261
4262################################################################################
4263#### Display Help information
4264################################################################################
4265# define some globals
4266htmlPanel = None
4267htmlFrame = None
4268htmlFirstUse = True
4269#helpLocDict = {}  # to be implemented if we ever split gsasii.html over multiple files
4270path2GSAS2 = os.path.dirname(os.path.realpath(__file__)) # save location of this file
4271def ShowHelp(helpType,frame):
4272    '''Called to bring up a web page for documentation.'''
4273    global htmlFirstUse,htmlPanel,htmlFrame
4274    # no defined link to use, create a default based on key
4275    helplink = 'gsasII.html'
4276    if helpType:
4277        helplink += '#'+helpType.replace(')','').replace('(','_').replace(' ','_')
4278    # determine if a web browser or the internal viewer should be used for help info
4279    if GSASIIpath.GetConfigValue('Help_mode'):
4280        helpMode = GSASIIpath.GetConfigValue('Help_mode')
4281    else:
4282        helpMode = 'browser'
4283    if helpMode == 'internal':
4284        helplink = os.path.join(path2GSAS2,'help',helplink)
4285        try:
4286            htmlPanel.LoadFile(helplink)
4287            htmlFrame.Raise()
4288        except:
4289            htmlFrame = wx.Frame(frame, -1, size=(610, 510))
4290            htmlFrame.Show(True)
4291            htmlFrame.SetTitle("HTML Window") # N.B. reset later in LoadFile
4292            htmlPanel = MyHtmlPanel(htmlFrame,-1)
4293            htmlPanel.LoadFile(helplink)
4294    else:
4295        if sys.platform == "darwin": # for Mac, force use of safari to preserve anchors on file URLs
4296            wb = webbrowser.MacOSXOSAScript('safari')
4297        else:
4298            wb = webbrowser
4299        helplink = os.path.join(path2GSAS2,'help',helplink)
4300        pfx = "file://"
4301        if sys.platform.lower().startswith('win'):
4302            pfx = ''
4303        #if GSASIIpath.GetConfigValue('debug'): print 'Help link=',pfx+helplink
4304        if htmlFirstUse:
4305            wb.open_new(pfx+helplink)
4306            htmlFirstUse = False
4307        else:
4308            wb.open(pfx+helplink, new=0, autoraise=True)
4309
4310def ShowWebPage(URL,frame):
4311    '''Called to show a tutorial web page.
4312    '''
4313    global htmlFirstUse,htmlPanel,htmlFrame
4314    # determine if a web browser or the internal viewer should be used for help info
4315    if GSASIIpath.GetConfigValue('Help_mode'):
4316        helpMode = GSASIIpath.GetConfigValue('Help_mode')
4317    else:
4318        helpMode = 'browser'
4319    if helpMode == 'internal':
4320        try:
4321            htmlPanel.LoadFile(URL)
4322            htmlFrame.Raise()
4323        except:
4324            htmlFrame = wx.Frame(frame, -1, size=(610, 510))
4325            htmlFrame.Show(True)
4326            htmlFrame.SetTitle("HTML Window") # N.B. reset later in LoadFile
4327            htmlPanel = MyHtmlPanel(htmlFrame,-1)
4328            htmlPanel.LoadFile(URL)
4329    else:
4330        if URL.startswith('http'): 
4331            pfx = ''
4332        elif sys.platform.lower().startswith('win'):
4333            pfx = ''
4334        else:
4335            pfx = "file://"
4336        if htmlFirstUse:
4337            webbrowser.open_new(pfx+URL)
4338            htmlFirstUse = False
4339        else:
4340            webbrowser.open(pfx+URL, new=0, autoraise=True)
4341
4342################################################################################
4343#### Tutorials support
4344################################################################################
4345G2BaseURL = "https://subversion.xray.aps.anl.gov/pyGSAS"
4346# N.B. tutorialCatalog is generated by routine catalog.py, which also generates the appropriate
4347# empty directories (.../MT/* .../trunk/GSASII/* *=[help,Exercises])
4348tutorialCatalog = (
4349    # tutorial dir,      web page file name,      title for page
4350
4351    ['StartingGSASII', 'Starting GSAS.htm', 'Starting GSAS-II'],
4352       
4353    ['LabData', 'Laboratory X.htm', 'Fitting laboratory X-ray powder data for fluoroapatite'],
4354    ['CWNeutron', 'Neutron CW Powder Data.htm', 'CW Neutron Powder fit for Yttrium-Iron Garnet'],
4355
4356    ['FitPeaks', 'Fit Peaks.htm', 'Fitting individual peaks & autoindexing'],
4357    ['CFjadarite', 'Charge Flipping in GSAS.htm', '     Charge Flipping structure solution for jadarite'],
4358    ['CFsucrose', 'Charge Flipping - sucrose.htm','     Charge Flipping structure solution for sucrose'],
4359    ['BkgFit', 'FitBkgTut.htm',  'Fitting the Starting Background using Fixed Points'],
4360       
4361    ['CWCombined', 'Combined refinement.htm', 'Combined X-ray/CW-neutron refinement of PbSO4'],
4362    ['TOF-CW Joint Refinement', 'TOF combined XN Rietveld refinement in GSAS.htm', 'Combined X-ray/TOF-neutron Rietveld refinement'],
4363    ['SeqRefine', 'SequentialTutorial.htm', 'Sequential refinement of multiple datasets'],
4364    ['SeqParametric', 'ParametricFitting.htm', '     Parametric Fitting and Pseudo Variables for Sequential Fits'],
4365       
4366    ['StackingFaults-I', 'Stacking Faults-I.htm', 'Stacking fault simulations for diamond'],
4367    ['StackingFaults-II', 'Stacking Faults II.htm', 'Stacking fault simulations for Keokuk kaolinite'],
4368    ['StackingFaults-III', 'Stacking Faults-III.htm', 'Stacking fault simulations for Georgia kaolinite'],
4369       
4370    ['CFXraySingleCrystal', 'CFSingleCrystal.htm', 'Charge Flipping structure solution with Xray single crystal data'],       
4371    ['TOF Charge Flipping', 'Charge Flipping with TOF single crystal data in GSASII.htm', 'Charge flipping with neutron TOF single crystal data'],
4372    ['MCsimanneal', 'MCSA in GSAS.htm', 'Monte-Carlo simulated annealing structure determination'],
4373       
4374    ['MerohedralTwins', 'Merohedral twin refinement in GSAS.htm', 'Merohedral twin refinements'],
4375
4376    ['2DCalibration', 'Calibration of an area detector in GSAS.htm', 'Calibration of an area detector'],
4377    ['2DIntegration', 'Integration of area detector data in GSAS.htm', '     Integration of area detector data'],
4378    ['TOF Calibration', 'Calibration of a TOF powder diffractometer.htm', 'Calibration of a Neutron TOF diffractometer'],
4379    ['TOF Single Crystal Refinement', 'TOF single crystal refinement in GSAS.htm', 'Single crystal refinement from TOF data'],
4380       
4381    ['2DStrain', 'Strain fitting of 2D data in GSAS-II.htm', 'Strain fitting of 2D data'],
4382    ['2DTexture', 'Texture analysis of 2D data in GSAS-II.htm', 'Texture analysis of 2D data'],
4383             
4384    ['SAsize', 'Small Angle Size Distribution.htm', 'Small angle x-ray data size distribution (alumina powder)'],
4385    ['SAfit', 'Fitting Small Angle Scattering Data.htm', '     Fitting small angle x-ray data (alumina powder)'],
4386    ['SAimages', 'Small Angle Image Processing.htm', 'Image Processing of small angle x-ray data'],
4387    ['SAseqref', 'Sequential Refinement of Small Angle Scattering Data.htm', 'Sequential refinement with small angle scattering data'],
4388   
4389    #['ExampleDir', 'ExamplePage.html', 'Example Tutorial Title'],
4390    )
4391
4392class OpenTutorial(wx.Dialog):
4393    '''Open a tutorial web page, optionally copying the web page, screen images and
4394    data file(s) to the local disk.
4395    '''
4396   
4397    def __init__(self,parent):
4398        style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER
4399        wx.Dialog.__init__(self, parent, wx.ID_ANY, 'Open Tutorial', style=style)
4400        self.frame = parent
4401        # self.frame can be the tree window frame or the data editing window frame, set G2frame to the
4402        # tree either way
4403        if hasattr(self.frame,'G2frame'):
4404            self.G2frame = self.frame.G2frame
4405        else:
4406            self.G2frame = self.frame
4407        pnl = wx.Panel(self)
4408        sizer = wx.BoxSizer(wx.VERTICAL)
4409        sizer1 = wx.BoxSizer(wx.HORIZONTAL)       
4410        label = wx.StaticText(
4411            pnl,  wx.ID_ANY,
4412            'Select the tutorial to be run and the mode of access'
4413            )
4414        msg = '''GSAS-II tutorials and their sample data files
4415        require a fair amount of storage space; few users will
4416        use all of them. This dialog allows users to load selected
4417        tutorials (along with their sample data) to their computer;
4418        optionally all tutorials can be downloaded.
4419
4420        Downloaded tutorials can be viewed and run without internet
4421        access. Tutorials can also be viewed without download, but
4422        users will need to download the sample data files manually.
4423
4424        The location used to download tutorials is set using the
4425        "Set download location" which is saved as the "Tutorial_location"
4426        configuration option see File/Preference or the
4427        config_example.py file. System managers can select to have
4428        tutorial files installed at a shared location.
4429        '''
4430        self.SetTutorialPath()
4431        hlp = HelpButton(pnl,msg)
4432        sizer1.Add((-1,-1),1, wx.EXPAND, 0)
4433        sizer1.Add(label, 0, wx.ALIGN_CENTRE|wx.ALL, 0)
4434        sizer1.Add((-1,-1),1, wx.EXPAND, 0)
4435        sizer1.Add(hlp,0,wx.ALIGN_RIGHT|wx.ALL)
4436        sizer.Add(sizer1,0,wx.EXPAND|wx.ALL,0)
4437        sizer.Add((10,10))
4438        sizer0 = wx.BoxSizer(wx.HORIZONTAL)       
4439        sizer1a = wx.BoxSizer(wx.VERTICAL)
4440        sizer1b = wx.BoxSizer(wx.VERTICAL)
4441        btn = wx.Button(pnl, wx.ID_ANY, "Download a tutorial and view")
4442        btn.Bind(wx.EVT_BUTTON, self.SelectAndDownload)
4443        sizer1a.Add(btn,0,WACV)
4444        btn = wx.Button(pnl, wx.ID_ANY, "Select from downloaded tutorials")
4445        btn.Bind(wx.EVT_BUTTON, self.onSelectDownloaded)
4446        sizer1a.Add(btn,0,WACV)
4447        btn = wx.Button(pnl, wx.ID_ANY, "Browse tutorial on web")
4448        btn.Bind(wx.EVT_BUTTON, self.onWebBrowse)
4449        sizer1a.Add(btn,0,WACV)
4450        btn = wx.Button(pnl, wx.ID_ANY, "Update downloaded tutorials")
4451        btn.Bind(wx.EVT_BUTTON, self.UpdateDownloaded)
4452        sizer1b.Add(btn,0,WACV)
4453        btn = wx.Button(pnl, wx.ID_ANY, "Download all tutorials")
4454        btn.Bind(wx.EVT_BUTTON, self.DownloadAll)
4455        sizer1b.Add(btn,0,WACV)
4456        sizer0.Add(sizer1a,0,wx.EXPAND|wx.ALL,0)
4457        sizer0.Add(sizer1b,0,wx.EXPAND|wx.ALL,0)
4458        sizer.Add(sizer0,5,wx.EXPAND|wx.ALL,5)
4459       
4460        sizer.Add((10,10))
4461        sizer1 = wx.BoxSizer(wx.HORIZONTAL)
4462        btn = wx.Button(pnl, wx.ID_ANY, "Set download location")
4463        btn.Bind(wx.EVT_BUTTON, self.SelectDownloadLoc)
4464        sizer1.Add(btn,0,WACV)
4465        self.dataLoc = wx.StaticText(pnl, wx.ID_ANY,self.tutorialPath)
4466        sizer1.Add(self.dataLoc,0,WACV)
4467        sizer.Add(sizer1)
4468       
4469        btnsizer = wx.StdDialogButtonSizer()
4470        btn = wx.Button(pnl, wx.ID_CANCEL,"Done")
4471        btnsizer.AddButton(btn)
4472        btnsizer.Realize()
4473        sizer.Add(btnsizer, 0, wx.ALIGN_CENTER|wx.ALL, 5)
4474        pnl.SetSizer(sizer)
4475        sizer.Fit(self)
4476        self.topsizer=sizer
4477        self.CenterOnParent()
4478
4479    def SetTutorialPath(self):
4480        '''Get the tutorial location if set; if not pick a default
4481        directory in a logical place
4482        '''
4483        if GSASIIpath.GetConfigValue('Tutorial_location'):
4484            self.tutorialPath = os.path.abspath(GSASIIpath.GetConfigValue('Tutorial_location'))
4485        elif (sys.platform.lower().startswith('win') and
4486              os.path.exists(os.path.abspath(os.path.expanduser('~/My Documents')))):
4487            self.tutorialPath = os.path.abspath(os.path.expanduser('~/My Documents/G2tutorials'))
4488        elif (sys.platform.lower().startswith('win') and
4489              os.path.exists(os.path.abspath(os.path.expanduser('~/Documents')))):
4490            self.tutorialPath = os.path.abspath(os.path.expanduser('~/Documents/G2tutorials'))
4491        else:
4492            self.tutorialPath = os.path.abspath(os.path.expanduser('~/G2tutorials'))
4493
4494    def SelectAndDownload(self,event):
4495        '''Make a list of all tutorials on web and allow user to choose one to
4496        download and then view
4497        '''
4498        indices = [j for j,i in enumerate(tutorialCatalog)
4499            if not os.path.exists(os.path.join(self.tutorialPath,i[0],i[1]))]
4500        if not indices:
4501            G2MessageBox(self,'All tutorials are downloaded','None to download')
4502            return
4503        choices = [tutorialCatalog[i][2] for i in indices]
4504        selected = self.ChooseTutorial(choices)
4505        if selected is None: return
4506        j = indices[selected]
4507        fullpath = os.path.join(self.tutorialPath,tutorialCatalog[j][0],tutorialCatalog[j][1])
4508        fulldir = os.path.join(self.tutorialPath,tutorialCatalog[j][0])
4509        URL = G2BaseURL+'/Tutorials/'+tutorialCatalog[j][0]+'/'
4510        if GSASIIpath.svnInstallDir(URL,fulldir):
4511            ShowWebPage(fullpath,self.frame)
4512        else:
4513            G2MessageBox(self,'Error downloading tutorial','Download error')
4514        self.EndModal(wx.ID_OK)
4515        self.G2frame.TutorialImportDir = os.path.join(self.tutorialPath,tutorialCatalog[j][0],'data')
4516
4517    def onSelectDownloaded(self,event):
4518        '''Select a previously downloaded tutorial
4519        '''
4520        indices = [j for j,i in enumerate(tutorialCatalog)
4521            if os.path.exists(os.path.join(self.tutorialPath,i[0],i[1]))]
4522        if not indices:
4523            G2MessageBox(self,
4524                         'There are no downloaded tutorials in '+self.tutorialPath,
4525                         'None downloaded')
4526            return
4527        choices = [tutorialCatalog[i][2] for i in indices]
4528        selected = self.ChooseTutorial(choices)
4529        if selected is None: return
4530        j = indices[selected]
4531        fullpath = os.path.join(self.tutorialPath,tutorialCatalog[j][0],tutorialCatalog[j][1])
4532        self.EndModal(wx.ID_OK)
4533        ShowWebPage(fullpath,self.frame)
4534        self.G2frame.TutorialImportDir = os.path.join(self.tutorialPath,tutorialCatalog[j][0],'data')
4535       
4536    def onWebBrowse(self,event):
4537        '''Make a list of all tutorials on web and allow user to view one.
4538        '''
4539        choices = [i[2] for i in tutorialCatalog]
4540        selected = self.ChooseTutorial(choices)
4541        if selected is None: return       
4542        tutdir = tutorialCatalog[selected][0]
4543        tutfil = tutorialCatalog[selected][1]
4544        # open web page remotely, don't worry about data
4545        URL = G2BaseURL+'/Tutorials/'+tutdir+'/'+tutfil
4546        self.EndModal(wx.ID_OK)
4547        ShowWebPage(URL,self.frame)
4548       
4549    def ChooseTutorial(self,choices):
4550        'choose a tutorial from a list'
4551        def onDoubleClick(event):
4552            'double-click closes the dialog'
4553            dlg.EndModal(wx.ID_OK)
4554        dlg = wx.Dialog(self,wx.ID_ANY,
4555                        'Select a tutorial to view. NB: indented ones require prerequisite',
4556                        style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)
4557        pnl = wx.Panel(dlg)
4558        sizer = wx.BoxSizer(wx.VERTICAL)
4559        listbox = wx.ListBox(pnl, wx.ID_ANY, choices=choices,
4560                             size=(450, 100),
4561                             style=wx.LB_SINGLE)
4562        sizer.Add(listbox,1,WACV|wx.EXPAND|wx.ALL,1)
4563        listbox.Bind(wx.EVT_LISTBOX_DCLICK, onDoubleClick)
4564        sizer.Add((10,10))
4565        btnsizer = wx.StdDialogButtonSizer()
4566        btn = wx.Button(pnl, wx.ID_CANCEL)
4567        btnsizer.AddButton(btn)
4568        OKbtn = wx.Button(pnl, wx.ID_OK)
4569        OKbtn.SetDefault()
4570        btnsizer.AddButton(OKbtn)
4571        btnsizer.Realize()
4572        sizer.Add((-1,5))
4573        sizer.Add(btnsizer,0,wx.ALIGN_RIGHT,50)
4574       
4575        pnl.SetSizer(sizer)
4576        sizer.Fit(dlg)
4577        self.CenterOnParent()
4578        if dlg.ShowModal() != wx.ID_OK:
4579            dlg.Destroy()
4580            return
4581        selected = listbox.GetSelection()
4582        dlg.Destroy()
4583        wx.Yield() # close window right away so user sees something happen
4584        if selected < 0: return
4585        return selected
4586
4587    def UpdateDownloaded(self,event):
4588        'Find the downloaded tutorials and run an svn update on them'
4589        updated = 0
4590        for i in tutorialCatalog:
4591            if not os.path.exists(os.path.join(self.tutorialPath,i[0],i[1])): continue
4592            print('Updating '+i[0])
4593            GSASIIpath.svnUpdateDir(os.path.join(self.tutorialPath,i[0]))
4594            updated += 0
4595        if not updated:
4596            G2MessageBox(self,'Warning, you have no downloaded tutorials','None Downloaded')
4597        self.EndModal(wx.ID_OK)
4598       
4599    def DownloadAll(self,event):
4600        'Download or update all tutorials'
4601        fail = ''
4602        for i in tutorialCatalog:
4603            if os.path.exists(os.path.join(self.tutorialPath,i[0],i[1])):
4604                print('Updating '+i[0])
4605                GSASIIpath.svnUpdateDir(os.path.join(self.tutorialPath,i[0]))
4606            else:
4607                fulldir = os.path.join(self.tutorialPath,i[0])
4608                URL = G2BaseURL+'/Tutorials/'+i[0]+'/'
4609                if not GSASIIpath.svnInstallDir(URL,fulldir):
4610                    if fail: fail += ', '
4611                    fail += i[0]
4612        if fail: 
4613            G2MessageBox(self,'Error downloading tutorial(s)\n\t'+fail,'Download error')
4614        self.EndModal(wx.ID_OK)
4615                   
4616    def SelectDownloadLoc(self,event):
4617        '''Select a download location,
4618        Cancel resets to the default
4619        '''
4620        dlg = wx.DirDialog(self, "Choose a directory for tutorial downloads:",
4621                           defaultPath=self.tutorialPath)#,style=wx.DD_DEFAULT_STYLE)
4622                           #)
4623        try:
4624            if dlg.ShowModal() != wx.ID_OK:
4625                return
4626            pth = dlg.GetPath()
4627        finally:
4628            dlg.Destroy()
4629
4630        if not os.path.exists(pth):
4631            try:
4632                os.makedirs(pth)    #failing for no obvious reason
4633            except OSError:
4634                msg = 'The selected directory is not valid.\n\t'
4635                msg += pth
4636                msg += '\n\nAn attempt to create the directory failed'
4637                G2MessageBox(self.frame,msg)
4638                return
4639        if os.path.exists(os.path.join(pth,"help")) and os.path.exists(os.path.join(pth,"Exercises")):
4640            print("Note that you may have old tutorial files in the following directories")
4641            print('\t'+os.path.join(pth,"help"))
4642            print('\t'+os.path.join(pth,"Exercises"))
4643            print('Subdirectories in the above can be deleted to save space\n\n')
4644        self.tutorialPath = pth
4645        self.dataLoc.SetLabel(self.tutorialPath)
4646        if GSASIIpath.GetConfigValue('Tutorial_location') == pth: return
4647        vars = GetConfigValsDocs()
4648        try:
4649            vars['Tutorial_location'][1] = pth
4650            if GSASIIpath.GetConfigValue('debug'): print('Saving Tutorial_location: '+pth)
4651            GSASIIpath.SetConfigValue(vars)
4652            SaveConfigVars(vars)
4653        except KeyError:
4654            pass
4655           
4656if __name__ == '__main__':
4657    app = wx.PySimpleApp()
4658    GSASIIpath.InvokeDebugOpts()
4659    frm = wx.Frame(None) # create a frame
4660    frm.Show(True)
4661   
4662    #======================================================================
4663    # test Grid with GridFractionEditor
4664    #======================================================================
4665    # tbl = [[1.,2.,3.],[1.1,2.1,3.1]]
4666    # colTypes = 3*[wg.GRID_VALUE_FLOAT+':10,5',]
4667    # Gtbl = Table(tbl,types=colTypes,rowLabels=['a','b'],colLabels=['1','2','3'])
4668    # Grid = GSGrid(frm)
4669    # Grid.SetTable(Gtbl,True)
4670    # for i in (0,1,2):
4671    #     attr = wx.grid.GridCellAttr()
4672    #     attr.IncRef()
4673    #     attr.SetEditor(GridFractionEditor(Grid))
4674    #     Grid.SetColAttr(i, attr)
4675    # frm.SetSize((400,200))
4676    # app.MainLoop()
4677    # sys.exit()
4678    #======================================================================
4679    # test Tutorial access
4680    #======================================================================
4681    # dlg = OpenTutorial(frm)
4682    # if dlg.ShowModal() == wx.ID_OK:
4683    #     print "OK"
4684    # else:
4685    #     print "Cancel"
4686    # dlg.Destroy()
4687    # sys.exit()
4688    #======================================================================
4689    # test ScrolledMultiEditor
4690    #======================================================================
4691    # Data1 = {
4692    #      'Order':1,
4693    #      'omega':'string',
4694    #      'chi':2.0,
4695    #      'phi':'',
4696    #      }
4697    # elemlst = sorted(Data1.keys())
4698    # prelbl = sorted(Data1.keys())
4699    # dictlst = len(elemlst)*[Data1,]
4700    #Data2 = [True,False,False,True]
4701    #Checkdictlst = len(Data2)*[Data2,]
4702    #Checkelemlst = range(len(Checkdictlst))
4703    # print 'before',Data1,'\n',Data2
4704    # dlg = ScrolledMultiEditor(
4705    #     frm,dictlst,elemlst,prelbl,
4706    #     checkdictlst=Checkdictlst,checkelemlst=Checkelemlst,
4707    #     checklabel="Refine?",
4708    #     header="test")
4709    # if dlg.ShowModal() == wx.ID_OK:
4710    #     print "OK"
4711    # else:
4712    #     print "Cancel"
4713    # print 'after',Data1,'\n',Data2
4714    # dlg.Destroy()
4715    Data3 = {
4716         'Order':1.0,
4717         'omega':1.1,
4718         'chi':2.0,
4719         'phi':2.3,
4720         'Order1':1.0,
4721         'omega1':1.1,
4722         'chi1':2.0,
4723         'phi1':2.3,
4724         'Order2':1.0,
4725         'omega2':1.1,
4726         'chi2':2.0,
4727         'phi2':2.3,
4728         }
4729    elemlst = sorted(Data3.keys())
4730    dictlst = len(elemlst)*[Data3,]
4731    prelbl = elemlst[:]
4732    prelbl[0]="this is a much longer label to stretch things out"
4733    Data2 = len(elemlst)*[False,]
4734    Data2[1] = Data2[3] = True
4735    Checkdictlst = len(elemlst)*[Data2,]
4736    Checkelemlst = range(len(Checkdictlst))
4737    #print 'before',Data3,'\n',Data2
4738    #print dictlst,"\n",elemlst
4739    #print Checkdictlst,"\n",Checkelemlst
4740    # dlg = ScrolledMultiEditor(
4741    #     frm,dictlst,elemlst,prelbl,
4742    #     checkdictlst=Checkdictlst,checkelemlst=Checkelemlst,
4743    #     checklabel="Refine?",
4744    #     header="test",CopyButton=True)
4745    # if dlg.ShowModal() == wx.ID_OK:
4746    #     print "OK"
4747    # else:
4748    #     print "Cancel"
4749    #print 'after',Data3,'\n',Data2
4750
4751    # Data2 = list(range(100))
4752    # elemlst += range(2,6)
4753    # postlbl += range(2,6)
4754    # dictlst += len(range(2,6))*[Data2,]
4755
4756    # prelbl = range(len(elemlst))
4757    # postlbl[1] = "a very long label for the 2nd item to force a horiz. scrollbar"
4758    # header="""This is a longer\nmultiline and perhaps silly header"""
4759    # dlg = ScrolledMultiEditor(frm,dictlst,elemlst,prelbl,postlbl,
4760    #                           header=header,CopyButton=True)
4761    # print Data1
4762    # if dlg.ShowModal() == wx.ID_OK:
4763    #     for d,k in zip(dictlst,elemlst):
4764    #         print k,d[k]
4765    # dlg.Destroy()
4766    # if CallScrolledMultiEditor(frm,dictlst,elemlst,prelbl,postlbl,
4767    #                            header=header):
4768    #     for d,k in zip(dictlst,elemlst):
4769    #         print k,d[k]
4770
4771    #======================================================================
4772    # test G2MultiChoiceDialog
4773    #======================================================================
4774    choices = []
4775    for i in range(21):
4776        choices.append("option_"+str(i))
4777    od = {
4778        'label_1':'This is a bool','value_1':True,
4779        'label_2':'This is a int','value_2':-1,
4780        'label_3':'This is a float','value_3':1.0,
4781        'label_4':'This is a string','value_4':'test',}
4782    dlg = G2MultiChoiceDialog(frm, 'Sequential refinement',
4783                              'Select dataset to include',
4784                              choices,extraOpts=od)
4785    sel = range(2,11,2)
4786    dlg.SetSelections(sel)
4787    dlg.SetSelections((1,5))
4788    if dlg.ShowModal() == wx.ID_OK:
4789        for sel in dlg.GetSelections():
4790            print sel,choices[sel]
4791    print od
4792    od = {}
4793    dlg = G2MultiChoiceDialog(frm, 'Sequential refinement',
4794                              'Select dataset to include',
4795                              choices,extraOpts=od)
4796    sel = range(2,11,2)
4797    dlg.SetSelections(sel)
4798    dlg.SetSelections((1,5))
4799    if dlg.ShowModal() == wx.ID_OK: pass
4800    #======================================================================
4801    # test wx.MultiChoiceDialog
4802    #======================================================================
4803    # dlg = wx.MultiChoiceDialog(frm, 'Sequential refinement',
4804    #                           'Select dataset to include',
4805    #                           choices)
4806    # sel = range(2,11,2)
4807    # dlg.SetSelections(sel)
4808    # dlg.SetSelections((1,5))
4809    # if dlg.ShowModal() == wx.ID_OK:
4810    #     for sel in dlg.GetSelections():
4811    #         print sel,choices[sel]
4812
4813    # pnl = wx.Panel(frm)
4814    # siz = wx.BoxSizer(wx.VERTICAL)
4815
4816    # td = {'Goni':200.,'a':1.,'calc':1./3.,'string':'s'}
4817    # for key in sorted(td):
4818    #     txt = ValidatedTxtCtrl(pnl,td,key)
4819    #     siz.Add(txt)
4820    # pnl.SetSizer(siz)
4821    # siz.Fit(frm)
4822    # app.MainLoop()
4823    # print td
Note: See TracBrowser for help on using the repository browser.