source: trunk/GSASIIctrls.py @ 2531

Last change on this file since 2531 was 2531, checked in by toby, 5 years ago

reopen tree items after a refinement

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