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

Last change on this file since 2914 was 2914, checked in by vondreele, 6 years ago

fix G2constrGUI; dataDisplay --> rbBook & fix a parent issue
similarly in G2restrGUI dataDisplay --> restrBook
G2ctrlGUI; missing G2obj import & fix a parent issue on a couple of dialogs
put the wxID definitions into _initMenus - need to be sorted into each section
remove the tree title - redundant info
tree root text changed
G2plot; change dataDisplay to restrBook for torsion & Ramachandran plots
some dataDisplay --> phaseDisplay in various places

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