source: trunk/GSASIIctrls.py @ 2526

Last change on this file since 2526 was 2526, checked in by vondreele, 5 years ago

fix double event problem in ValidatedTxtCtrl?

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