source: trunk/GSASIIctrls.py @ 2622

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

avoid undefined G2frame.dataFrame bug
comment out two save size prints
remove extra SetSize? calls from G2imgGUI & G2pwdGUI

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