source: trunk/GSASIIctrls.py @ 2607

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

redo help to use DataFrame? helpKey; define .dataFrame.helpKey; rename MovePatternTreeToGrid? to SelectDataTreeItem? & OnPatternTreeSelChanged? to OnDataTreeSelChanged? to make more sense; key on initial string in data tree names, not presence of 'IMG' etc anywhere in name; cleanup Prefill of menus

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