source: trunk/GSASIIctrls.py @ 1796

Last change on this file since 1796 was 1770, checked in by vondreele, 10 years ago

moved MultipleChoicesDialog? and SingleChoiceDialog? from G2grid to G2ctrls

  • Property svn:eol-style set to native
File size: 112.8 KB
Line 
1# -*- coding: utf-8 -*-
2#GSASIIctrls - Custom GSAS-II GUI controls
3########### SVN repository information ###################
4# $Date: $
5# $Author: $
6# $Revision: $
7# $URL: $
8# $Id: $
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 wx
20# import wx.grid as wg
21# import wx.wizard as wz
22# import wx.aui
23import wx.lib.scrolledpanel as wxscroll
24import time
25import copy
26# import cPickle
27import sys
28import os
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: 1614 $")
37# import GSASIImath as G2mth
38# import GSASIIIO as G2IO
39# import GSASIIstrIO as G2stIO
40# import GSASIIlattice as G2lat
41# import GSASIIplot as G2plt
42import GSASIIpwdGUI as G2pdG
43# import GSASIIimgGUI as G2imG
44# import GSASIIphsGUI as G2phG
45# import GSASIIspc as G2spc
46# import GSASIImapvars as G2mv
47# import GSASIIconstrGUI as G2cnstG
48# import GSASIIrestrGUI as G2restG
49import GSASIIpy3 as G2py3
50# import GSASIIobj as G2obj
51# import GSASIIexprGUI as G2exG
52import GSASIIlog as log
53
54# Define a short names for convenience
55WHITE = (255,255,255)
56DULL_YELLOW = (230,230,190)
57VERY_LIGHT_GREY = wx.Colour(235,235,235)
58WACV = wx.ALIGN_CENTER_VERTICAL
59
60################################################################################
61#### Tree Control
62################################################################################
63class G2TreeCtrl(wx.TreeCtrl):
64    '''Create a wrapper around the standard TreeCtrl so we can "wrap"
65    various events.
66   
67    This logs when a tree item is selected (in :meth:`onSelectionChanged`)
68
69    This also wraps lists and dicts pulled out of the tree to track where
70    they were retrieved from.
71    '''
72    def __init__(self,parent=None,*args,**kwargs):
73        super(self.__class__,self).__init__(parent=parent,*args,**kwargs)
74        self.G2frame = parent.GetParent()
75        self.root = self.AddRoot('Loaded Data: ')
76        self.SelectionChanged = None
77        log.LogInfo['Tree'] = self
78
79    def _getTreeItemsList(self,item):
80        '''Get the full tree hierarchy from a reference to a tree item.
81        Note that this effectively hard-codes phase and histogram names in the
82        returned list. We may want to make these names relative in the future.
83        '''
84        textlist = [self.GetItemText(item)]
85        parent = self.GetItemParent(item)
86        while parent:
87            if parent == self.root: break
88            textlist.insert(0,self.GetItemText(parent))
89            parent = self.GetItemParent(parent)
90        return textlist
91
92    def onSelectionChanged(self,event):
93        '''Log each press on a tree item here.
94        '''
95        if self.SelectionChanged:
96            textlist = self._getTreeItemsList(event.GetItem())
97            if log.LogInfo['Logging'] and event.GetItem() != self.root:
98                textlist[0] = self.GetRelativeHistNum(textlist[0])
99                if textlist[0] == "Phases" and len(textlist) > 1:
100                    textlist[1] = self.GetRelativePhaseNum(textlist[1])
101                log.MakeTreeLog(textlist)
102            self.SelectionChanged(event)
103
104    def Bind(self,eventtype,handler,*args,**kwargs):
105        '''Override the Bind() function so that page change events can be trapped
106        '''
107        if eventtype == wx.EVT_TREE_SEL_CHANGED:
108            self.SelectionChanged = handler
109            wx.TreeCtrl.Bind(self,eventtype,self.onSelectionChanged)
110            return
111        wx.TreeCtrl.Bind(self,eventtype,handler,*args,**kwargs)
112
113    # commented out, disables Logging
114    # def GetItemPyData(self,*args,**kwargs):
115    #    '''Override the standard method to wrap the contents
116    #    so that the source can be logged when changed
117    #    '''
118    #    data = super(self.__class__,self).GetItemPyData(*args,**kwargs)
119    #    textlist = self._getTreeItemsList(args[0])
120    #    if type(data) is dict:
121    #        return log.dictLogged(data,textlist)
122    #    if type(data) is list:
123    #        return log.listLogged(data,textlist)
124    #    if type(data) is tuple: #N.B. tuples get converted to lists
125    #        return log.listLogged(list(data),textlist)
126    #    return data
127
128    def GetRelativeHistNum(self,histname):
129        '''Returns list with a histogram type and a relative number for that
130        histogram, or the original string if not a histogram
131        '''
132        histtype = histname.split()[0]
133        if histtype != histtype.upper(): # histograms (only) have a keyword all in caps
134            return histname
135        item, cookie = self.GetFirstChild(self.root)
136        i = 0
137        while item:
138            itemtext = self.GetItemText(item)
139            if itemtext == histname:
140                return histtype,i
141            elif itemtext.split()[0] == histtype:
142                i += 1
143            item, cookie = self.GetNextChild(self.root, cookie)
144        else:
145            raise Exception("Histogram not found: "+histname)
146
147    def ConvertRelativeHistNum(self,histtype,histnum):
148        '''Converts a histogram type and relative histogram number to a
149        histogram name in the current project
150        '''
151        item, cookie = self.GetFirstChild(self.root)
152        i = 0
153        while item:
154            itemtext = self.GetItemText(item)
155            if itemtext.split()[0] == histtype:
156                if i == histnum: return itemtext
157                i += 1
158            item, cookie = self.GetNextChild(self.root, cookie)
159        else:
160            raise Exception("Histogram #'+str(histnum)+' of type "+histtype+' not found')
161       
162    def GetRelativePhaseNum(self,phasename):
163        '''Returns a phase number if the string matches a phase name
164        or else returns the original string
165        '''
166        item, cookie = self.GetFirstChild(self.root)
167        while item:
168            itemtext = self.GetItemText(item)
169            if itemtext == "Phases":
170                parent = item
171                item, cookie = self.GetFirstChild(parent)
172                i = 0
173                while item:
174                    itemtext = self.GetItemText(item)
175                    if itemtext == phasename:
176                        return i
177                    item, cookie = self.GetNextChild(parent, cookie)
178                    i += 1
179                else:
180                    return phasename # not a phase name
181            item, cookie = self.GetNextChild(self.root, cookie)
182        else:
183            raise Exception("No phases found ")
184
185    def ConvertRelativePhaseNum(self,phasenum):
186        '''Converts relative phase number to a phase name in
187        the current project
188        '''
189        item, cookie = self.GetFirstChild(self.root)
190        while item:
191            itemtext = self.GetItemText(item)
192            if itemtext == "Phases":
193                parent = item
194                item, cookie = self.GetFirstChild(parent)
195                i = 0
196                while item:
197                    if i == phasenum:
198                        return self.GetItemText(item)
199                    item, cookie = self.GetNextChild(parent, cookie)
200                    i += 1
201                else:
202                    raise Exception("Phase "+str(phasenum)+" not found")
203            item, cookie = self.GetNextChild(self.root, cookie)
204        else:
205            raise Exception("No phases found ")
206
207################################################################################
208#### TextCtrl that stores input as entered with optional validation
209################################################################################
210class ValidatedTxtCtrl(wx.TextCtrl):
211    '''Create a TextCtrl widget that uses a validator to prevent the
212    entry of inappropriate characters and changes color to highlight
213    when invalid input is supplied. As valid values are typed,
214    they are placed into the dict or list where the initial value
215    came from. The type of the initial value must be int,
216    float or str or None (see :obj:`key` and :obj:`typeHint`);
217    this type (or the one in :obj:`typeHint`) is preserved.
218
219    Float values can be entered in the TextCtrl as numbers or also
220    as algebraic expressions using operators + - / \* () and \*\*,
221    in addition pi, sind(), cosd(), tand(), and sqrt() can be used,
222    as well as appreviations s, sin, c, cos, t, tan and sq.
223
224    :param wx.Panel parent: name of panel or frame that will be
225      the parent to the TextCtrl. Can be None.
226
227    :param dict/list loc: the dict or list with the initial value to be
228      placed in the TextCtrl.
229
230    :param int/str key: the dict key or the list index for the value to be
231      edited by the TextCtrl. The ``loc[key]`` element must exist, but may
232      have value None. If None, the type for the element is taken from
233      :obj:`typeHint` and the value for the control is set initially
234      blank (and thus invalid.) This is a way to specify a field without a
235      default value: a user must set a valid value.
236      If the value is not None, it must have a base
237      type of int, float, str or unicode; the TextCrtl will be initialized
238      from this value.
239     
240    :param list nDig: number of digits & places ([nDig,nPlc]) after decimal to use
241      for display of float. Alternately, None can be specified which causes
242      numbers to be displayed with approximately 5 significant figures
243      (Default=None).
244
245    :param bool notBlank: if True (default) blank values are invalid
246      for str inputs.
247     
248    :param number min: minimum allowed valid value. If None (default) the
249      lower limit is unbounded.
250
251    :param number max: maximum allowed valid value. If None (default) the
252      upper limit is unbounded
253
254    :param function OKcontrol: specifies a function or method that will be
255      called when the input is validated. The called function is supplied
256      with one argument which is False if the TextCtrl contains an invalid
257      value and True if the value is valid.
258      Note that this function should check all values
259      in the dialog when True, since other entries might be invalid.
260      The default for this is None, which indicates no function should
261      be called.
262
263    :param function OnLeave: specifies a function or method that will be
264      called when the focus for the control is lost.
265      The called function is supplied with (at present) three keyword arguments:
266
267         * invalid: (*bool*) True if the value for the TextCtrl is invalid
268         * value:   (*int/float/str*)  the value contained in the TextCtrl
269         * tc:      (*wx.TextCtrl*)  the TextCtrl name
270
271      The number of keyword arguments may be increased in the future should needs arise,
272      so it is best to code these functions with a \*\*kwargs argument so they will
273      continue to run without errors
274
275      The default for OnLeave is None, which indicates no function should
276      be called.
277
278    :param type typeHint: the value of typeHint is overrides the initial value
279      for the dict/list element ``loc[key]``, if set to
280      int or float, which specifies the type for input to the TextCtrl.
281      Defaults as None, which is ignored.
282
283    :param bool CIFinput: for str input, indicates that only printable
284      ASCII characters may be entered into the TextCtrl. Forces output
285      to be ASCII rather than Unicode. For float and int input, allows
286      use of a single '?' or '.' character as valid input.
287
288    :param dict OnLeaveArgs: a dict with keyword args that are passed to
289      the :attr:`OnLeave` function. Defaults to ``{}``
290
291    :param (other): other optional keyword parameters for the
292      wx.TextCtrl widget such as size or style may be specified.
293
294    '''
295    def __init__(self,parent,loc,key,nDig=None,notBlank=True,min=None,max=None,
296                 OKcontrol=None,OnLeave=None,typeHint=None,
297                 CIFinput=False, OnLeaveArgs={}, **kw):
298        # save passed values needed outside __init__
299        self.result = loc
300        self.key = key
301        self.nDig = nDig
302        self.OKcontrol=OKcontrol
303        self.OnLeave = OnLeave
304        self.OnLeaveArgs = OnLeaveArgs
305        self.CIFinput = CIFinput
306        self.type = str
307        # initialization
308        self.invalid = False   # indicates if the control has invalid contents
309        self.evaluated = False # set to True when the validator recognizes an expression
310        val = loc[key]
311        if isinstance(val,int) or typeHint is int:
312            self.type = int
313            wx.TextCtrl.__init__(
314                self,parent,wx.ID_ANY,
315                validator=NumberValidator(int,result=loc,key=key,
316                                          min=min,max=max,
317                                          OKcontrol=OKcontrol,
318                                          CIFinput=CIFinput),
319                **kw)
320            if val is not None:
321                self._setValue(val)
322            else: # no default is invalid for a number
323                self.invalid = True
324                self._IndicateValidity()
325
326        elif isinstance(val,float) or typeHint is float:
327            self.type = float
328            wx.TextCtrl.__init__(
329                self,parent,wx.ID_ANY,
330                validator=NumberValidator(float,result=loc,key=key,
331                                          min=min,max=max,
332                                          OKcontrol=OKcontrol,
333                                          CIFinput=CIFinput),
334                **kw)
335            if val is not None:
336                self._setValue(val)
337            else:
338                self.invalid = True
339                self._IndicateValidity()
340
341        elif isinstance(val,str) or isinstance(val,unicode):
342            if self.CIFinput:
343                wx.TextCtrl.__init__(
344                    self,parent,wx.ID_ANY,val,
345                    validator=ASCIIValidator(result=loc,key=key),
346                    **kw)
347            else:
348                wx.TextCtrl.__init__(self,parent,wx.ID_ANY,val,**kw)
349            if notBlank:
350                self.Bind(wx.EVT_CHAR,self._onStringKey)
351                self.ShowStringValidity() # test if valid input
352            else:
353                self.invalid = False
354                self.Bind(wx.EVT_CHAR,self._GetStringValue)
355        elif val is None:
356            raise Exception,("ValidatedTxtCtrl error: value of "+str(key)+
357                             " element is None and typeHint not defined as int or float")
358        else:
359            raise Exception,("ValidatedTxtCtrl error: Unknown element ("+str(key)+
360                             ") type: "+str(type(val)))
361        # When the mouse is moved away or the widget loses focus,
362        # display the last saved value, if an expression
363        #self.Bind(wx.EVT_LEAVE_WINDOW, self._onLeaveWindow)
364        self.Bind(wx.EVT_TEXT_ENTER, self._onLoseFocus)
365        self.Bind(wx.EVT_KILL_FOCUS, self._onLoseFocus)
366        # patch for wx 2.9 on Mac
367        i,j= wx.__version__.split('.')[0:2]
368        if int(i)+int(j)/10. > 2.8 and 'wxOSX' in wx.PlatformInfo:
369            self.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)
370
371    def SetValue(self,val):
372        if self.result is not None: # note that this bypasses formatting
373            self.result[self.key] = val
374            log.LogVarChange(self.result,self.key)
375        self._setValue(val)
376
377    def _setValue(self,val):
378        self.invalid = False
379        if self.type is int:
380            try:
381                if int(val) != val:
382                    self.invalid = True
383                else:
384                    val = int(val)
385            except:
386                if self.CIFinput and (val == '?' or val == '.'):
387                    pass
388                else:
389                    self.invalid = True
390            wx.TextCtrl.SetValue(self,str(val))
391        elif self.type is float:
392            try:
393                val = float(val) # convert strings, if needed
394            except:
395                if self.CIFinput and (val == '?' or val == '.'):
396                    pass
397                else:
398                    self.invalid = True
399            if self.nDig:
400                wx.TextCtrl.SetValue(self,str(G2py3.FormatValue(val,self.nDig)))
401            else:
402                wx.TextCtrl.SetValue(self,str(G2py3.FormatSigFigs(val)).rstrip('0'))
403        else:
404            wx.TextCtrl.SetValue(self,str(val))
405            self.ShowStringValidity() # test if valid input
406            return
407       
408        self._IndicateValidity()
409        if self.OKcontrol:
410            self.OKcontrol(not self.invalid)
411
412    def OnKeyDown(self,event):
413        'Special callback for wx 2.9+ on Mac where backspace is not processed by validator'
414        key = event.GetKeyCode()
415        if key in [wx.WXK_BACK, wx.WXK_DELETE]:
416            if self.Validator: wx.CallAfter(self.Validator.TestValid,self)
417        if key == wx.WXK_RETURN:
418            self._onLoseFocus(None)
419        event.Skip()
420                   
421    def _onStringKey(self,event):
422        event.Skip()
423        if self.invalid: # check for validity after processing the keystroke
424            wx.CallAfter(self.ShowStringValidity,True) # was invalid
425        else:
426            wx.CallAfter(self.ShowStringValidity,False) # was valid
427
428    def _IndicateValidity(self):
429        'Set the control colors to show invalid input'
430        if self.invalid:
431            self.SetForegroundColour("red")
432            self.SetBackgroundColour("yellow")
433            self.SetFocus()
434            self.Refresh()
435        else: # valid input
436            self.SetBackgroundColour(
437                wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW))
438            self.SetForegroundColour("black")
439            self.Refresh()
440
441    def ShowStringValidity(self,previousInvalid=True):
442        '''Check if input is valid. Anytime the input is
443        invalid, call self.OKcontrol (if defined) because it is fast.
444        If valid, check for any other invalid entries only when
445        changing from invalid to valid, since that is slower.
446       
447        :param bool previousInvalid: True if the TextCtrl contents were
448          invalid prior to the current change.
449         
450        '''
451        val = self.GetValue().strip()
452        self.invalid = not val
453        self._IndicateValidity()
454        if self.invalid:
455            if self.OKcontrol:
456                self.OKcontrol(False)
457        elif self.OKcontrol and previousInvalid:
458            self.OKcontrol(True)
459        # always store the result
460        if self.CIFinput: # for CIF make results ASCII
461            self.result[self.key] = val.encode('ascii','replace') 
462        else:
463            self.result[self.key] = val
464        log.LogVarChange(self.result,self.key)
465
466    def _GetStringValue(self,event):
467        '''Get string input and store.
468        '''
469        event.Skip() # process keystroke
470        wx.CallAfter(self._SaveStringValue)
471       
472    def _SaveStringValue(self):
473        val = self.GetValue().strip()
474        # always store the result
475        if self.CIFinput: # for CIF make results ASCII
476            self.result[self.key] = val.encode('ascii','replace') 
477        else:
478            self.result[self.key] = val
479        log.LogVarChange(self.result,self.key)
480
481    def _onLoseFocus(self,event):
482        if self.evaluated:
483            self.EvaluateExpression()
484        elif self.result is not None: # show formatted result, as Bob wants
485            self._setValue(self.result[self.key])
486        if self.OnLeave: self.OnLeave(invalid=self.invalid,
487                                      value=self.result[self.key],
488                                      tc=self,
489                                      **self.OnLeaveArgs)
490        if event: event.Skip()
491
492    def EvaluateExpression(self):
493        '''Show the computed value when an expression is entered to the TextCtrl
494        Make sure that the number fits by truncating decimal places and switching
495        to scientific notation, as needed.
496        Called on loss of focus, enter, etc..
497        '''
498        if self.invalid: return # don't substitute for an invalid expression
499        if not self.evaluated: return # true when an expression is evaluated
500        if self.result is not None: # retrieve the stored result
501            self._setValue(self.result[self.key])
502        self.evaluated = False # expression has been recast as value, reset flag
503       
504class NumberValidator(wx.PyValidator):
505    '''A validator to be used with a TextCtrl to prevent
506    entering characters other than digits, signs, and for float
507    input, a period and exponents.
508   
509    The value is checked for validity after every keystroke
510      If an invalid number is entered, the box is highlighted.
511      If the number is valid, it is saved in result[key]
512
513    :param type typ: the base data type. Must be int or float.
514
515    :param bool positiveonly: If True, negative integers are not allowed
516      (default False). This prevents the + or - keys from being pressed.
517      Used with typ=int; ignored for typ=float.
518
519    :param number min: Minimum allowed value. If None (default) the
520      lower limit is unbounded
521
522    :param number max: Maximum allowed value. If None (default) the
523      upper limit is unbounded
524     
525    :param dict/list result: List or dict where value should be placed when valid
526
527    :param any key: key to use for result (int for list)
528
529    :param function OKcontrol: function or class method to control
530      an OK button for a window.
531      Ignored if None (default)
532
533    :param bool CIFinput: allows use of a single '?' or '.' character
534      as valid input.
535     
536    '''
537    def __init__(self, typ, positiveonly=False, min=None, max=None,
538                 result=None, key=None, OKcontrol=None, CIFinput=False):
539        'Create the validator'
540        wx.PyValidator.__init__(self)
541        # save passed parameters
542        self.typ = typ
543        self.positiveonly = positiveonly
544        self.min = min
545        self.max = max
546        self.result = result
547        self.key = key
548        self.OKcontrol = OKcontrol
549        self.CIFinput = CIFinput
550        # set allowed keys by data type
551        self.Bind(wx.EVT_CHAR, self.OnChar)
552        if self.typ == int and self.positiveonly:
553            self.validchars = '0123456789'
554        elif self.typ == int:
555            self.validchars = '0123456789+-'
556        elif self.typ == float:
557            # allow for above and sind, cosd, sqrt, tand, pi, and abbreviations
558            # also addition, subtraction, division, multiplication, exponentiation
559            self.validchars = '0123456789.-+eE/cosindcqrtap()*'
560        else:
561            self.validchars = None
562            return
563        if self.CIFinput:
564            self.validchars += '?.'
565    def Clone(self):
566        'Create a copy of the validator, a strange, but required component'
567        return NumberValidator(typ=self.typ, 
568                               positiveonly=self.positiveonly,
569                               min=self.min, max=self.max,
570                               result=self.result, key=self.key,
571                               OKcontrol=self.OKcontrol,
572                               CIFinput=self.CIFinput)
573    def TransferToWindow(self):
574        'Needed by validator, strange, but required component'
575        return True # Prevent wxDialog from complaining.
576    def TransferFromWindow(self):
577        'Needed by validator, strange, but required component'
578        return True # Prevent wxDialog from complaining.
579    def TestValid(self,tc):
580        '''Check if the value is valid by casting the input string
581        into the current type.
582
583        Set the invalid variable in the TextCtrl object accordingly.
584
585        If the value is valid, save it in the dict/list where
586        the initial value was stored, if appropriate.
587
588        :param wx.TextCtrl tc: A reference to the TextCtrl that the validator
589          is associated with.
590        '''
591        tc.invalid = False # assume valid
592        if self.CIFinput:
593            val = tc.GetValue().strip()
594            if val == '?' or val == '.':
595                self.result[self.key] = val
596                log.LogVarChange(self.result,self.key)
597                return
598        try:
599            val = self.typ(tc.GetValue())
600        except (ValueError, SyntaxError) as e:
601            if self.typ is float: # for float values, see if an expression can be evaluated
602                val = G2py3.FormulaEval(tc.GetValue())
603                if val is None:
604                    tc.invalid = True
605                    return
606                else:
607                    tc.evaluated = True
608            else: 
609                tc.invalid = True
610                return
611        # if self.max != None and self.typ == int:
612        #     if val > self.max:
613        #         tc.invalid = True
614        # if self.min != None and self.typ == int:
615        #     if val < self.min:
616        #         tc.invalid = True  # invalid
617        if self.max != None:
618            if val > self.max:
619                tc.invalid = True
620        if self.min != None:
621            if val < self.min:
622                tc.invalid = True  # invalid
623        if self.key is not None and self.result is not None and not tc.invalid:
624            self.result[self.key] = val
625            log.LogVarChange(self.result,self.key)
626
627    def ShowValidity(self,tc):
628        '''Set the control colors to show invalid input
629
630        :param wx.TextCtrl tc: A reference to the TextCtrl that the validator
631          is associated with.
632
633        '''
634        if tc.invalid:
635            tc.SetForegroundColour("red")
636            tc.SetBackgroundColour("yellow")
637            tc.SetFocus()
638            tc.Refresh()
639            return False
640        else: # valid input
641            tc.SetBackgroundColour(
642                wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW))
643            tc.SetForegroundColour("black")
644            tc.Refresh()
645            return True
646
647    def CheckInput(self,previousInvalid):
648        '''called to test every change to the TextCtrl for validity and
649        to change the appearance of the TextCtrl
650
651        Anytime the input is invalid, call self.OKcontrol
652        (if defined) because it is fast.
653        If valid, check for any other invalid entries only when
654        changing from invalid to valid, since that is slower.
655
656        :param bool previousInvalid: True if the TextCtrl contents were
657          invalid prior to the current change.
658        '''
659        tc = self.GetWindow()
660        self.TestValid(tc)
661        self.ShowValidity(tc)
662        # if invalid
663        if tc.invalid and self.OKcontrol:
664            self.OKcontrol(False)
665        if not tc.invalid and self.OKcontrol and previousInvalid:
666            self.OKcontrol(True)
667
668    def OnChar(self, event):
669        '''Called each type a key is pressed
670        ignores keys that are not allowed for int and float types
671        '''
672        key = event.GetKeyCode()
673        tc = self.GetWindow()
674        if key == wx.WXK_RETURN:
675            if tc.invalid:
676                self.CheckInput(True) 
677            else:
678                self.CheckInput(False) 
679            return
680        if key < wx.WXK_SPACE or key == wx.WXK_DELETE or key > 255: # control characters get processed
681            event.Skip()
682            if tc.invalid:
683                wx.CallAfter(self.CheckInput,True) 
684            else:
685                wx.CallAfter(self.CheckInput,False) 
686            return
687        elif chr(key) in self.validchars: # valid char pressed?
688            event.Skip()
689            if tc.invalid:
690                wx.CallAfter(self.CheckInput,True) 
691            else:
692                wx.CallAfter(self.CheckInput,False) 
693            return
694        if not wx.Validator_IsSilent(): wx.Bell()
695        return  # Returning without calling event.Skip, which eats the keystroke
696
697class ASCIIValidator(wx.PyValidator):
698    '''A validator to be used with a TextCtrl to prevent
699    entering characters other than ASCII characters.
700   
701    The value is checked for validity after every keystroke
702      If an invalid number is entered, the box is highlighted.
703      If the number is valid, it is saved in result[key]
704
705    :param dict/list result: List or dict where value should be placed when valid
706
707    :param any key: key to use for result (int for list)
708
709    '''
710    def __init__(self, result=None, key=None):
711        'Create the validator'
712        import string
713        wx.PyValidator.__init__(self)
714        # save passed parameters
715        self.result = result
716        self.key = key
717        self.validchars = string.ascii_letters + string.digits + string.punctuation + string.whitespace
718        self.Bind(wx.EVT_CHAR, self.OnChar)
719    def Clone(self):
720        'Create a copy of the validator, a strange, but required component'
721        return ASCIIValidator(result=self.result, key=self.key)
722        tc = self.GetWindow()
723        tc.invalid = False # make sure the validity flag is defined in parent
724    def TransferToWindow(self):
725        'Needed by validator, strange, but required component'
726        return True # Prevent wxDialog from complaining.
727    def TransferFromWindow(self):
728        'Needed by validator, strange, but required component'
729        return True # Prevent wxDialog from complaining.
730    def TestValid(self,tc):
731        '''Check if the value is valid by casting the input string
732        into ASCII.
733
734        Save it in the dict/list where the initial value was stored
735
736        :param wx.TextCtrl tc: A reference to the TextCtrl that the validator
737          is associated with.
738        '''
739        self.result[self.key] = tc.GetValue().encode('ascii','replace')
740        log.LogVarChange(self.result,self.key)
741
742    def OnChar(self, event):
743        '''Called each type a key is pressed
744        ignores keys that are not allowed for int and float types
745        '''
746        key = event.GetKeyCode()
747        tc = self.GetWindow()
748        if key == wx.WXK_RETURN:
749            self.TestValid(tc)
750            return
751        if key < wx.WXK_SPACE or key == wx.WXK_DELETE or key > 255: # control characters get processed
752            event.Skip()
753            self.TestValid(tc)
754            return
755        elif chr(key) in self.validchars: # valid char pressed?
756            event.Skip()
757            self.TestValid(tc)
758            return
759        if not wx.Validator_IsSilent():
760            wx.Bell()
761        return  # Returning without calling event.Skip, which eats the keystroke
762
763################################################################################
764#### Edit a large number of values
765################################################################################
766def CallScrolledMultiEditor(parent,dictlst,elemlst,prelbl=[],postlbl=[],
767                 title='Edit items',header='',size=(300,250),
768                             CopyButton=False, **kw):
769    '''Shell routine to call a ScrolledMultiEditor dialog. See
770    :class:`ScrolledMultiEditor` for parameter definitions.
771
772    :returns: True if the OK button is pressed; False if the window is closed
773      with the system menu or the Cancel button.
774
775    '''
776    dlg = ScrolledMultiEditor(parent,dictlst,elemlst,prelbl,postlbl,
777                              title,header,size,
778                              CopyButton, **kw)
779    if dlg.ShowModal() == wx.ID_OK:
780        dlg.Destroy()
781        return True
782    else:
783        dlg.Destroy()
784        return False
785
786class ScrolledMultiEditor(wx.Dialog):
787    '''Define a window for editing a potentially large number of dict- or
788    list-contained values with validation for each item. Edited values are
789    automatically placed in their source location. If invalid entries
790    are provided, the TextCtrl is turned yellow and the OK button is disabled.
791
792    The type for each TextCtrl validation is determined by the
793    initial value of the entry (int, float or string).
794    Float values can be entered in the TextCtrl as numbers or also
795    as algebraic expressions using operators + - / \* () and \*\*,
796    in addition pi, sind(), cosd(), tand(), and sqrt() can be used,
797    as well as appreviations s(), sin(), c(), cos(), t(), tan() and sq().
798
799    :param wx.Frame parent: name of parent window, or may be None
800
801    :param tuple dictlst: a list of dicts or lists containing values to edit
802
803    :param tuple elemlst: a list of keys for each item in a dictlst. Must have the
804      same length as dictlst.
805
806    :param wx.Frame parent: name of parent window, or may be None
807   
808    :param tuple prelbl: a list of labels placed before the TextCtrl for each
809      item (optional)
810   
811    :param tuple postlbl: a list of labels placed after the TextCtrl for each
812      item (optional)
813
814    :param str title: a title to place in the frame of the dialog
815
816    :param str header: text to place at the top of the window. May contain
817      new line characters.
818
819    :param wx.Size size: a size parameter that dictates the
820      size for the scrolled region of the dialog. The default is
821      (300,250).
822
823    :param bool CopyButton: if True adds a small button that copies the
824      value for the current row to all fields below (default is False)
825     
826    :param list minvals: optional list of minimum values for validation
827      of float or int values. Ignored if value is None.
828    :param list maxvals: optional list of maximum values for validation
829      of float or int values. Ignored if value is None.
830    :param list sizevals: optional list of wx.Size values for each input
831      widget. Ignored if value is None.
832     
833    :param tuple checkdictlst: an optional list of dicts or lists containing bool
834      values (similar to dictlst).
835    :param tuple checkelemlst: an optional list of dicts or lists containing bool
836      key values (similar to elemlst). Must be used with checkdictlst.
837    :param string checklabel: a string to use for each checkbutton
838     
839    :returns: the wx.Dialog created here. Use method .ShowModal() to display it.
840   
841    *Example for use of ScrolledMultiEditor:*
842
843    ::
844
845        dlg = <pkg>.ScrolledMultiEditor(frame,dictlst,elemlst,prelbl,postlbl,
846                                        header=header)
847        if dlg.ShowModal() == wx.ID_OK:
848             for d,k in zip(dictlst,elemlst):
849                 print d[k]
850
851    *Example definitions for dictlst and elemlst:*
852
853    ::
854     
855          dictlst = (dict1,list1,dict1,list1)
856          elemlst = ('a', 1, 2, 3)
857
858      This causes items dict1['a'], list1[1], dict1[2] and list1[3] to be edited.
859   
860    Note that these items must have int, float or str values assigned to
861    them. The dialog will force these types to be retained. String values
862    that are blank are marked as invalid.
863    '''
864   
865    def __init__(self,parent,dictlst,elemlst,prelbl=[],postlbl=[],
866                 title='Edit items',header='',size=(300,250),
867                 CopyButton=False,
868                 minvals=[],maxvals=[],sizevals=[],
869                 checkdictlst=[], checkelemlst=[], checklabel=""):
870        if len(dictlst) != len(elemlst):
871            raise Exception,"ScrolledMultiEditor error: len(dictlst) != len(elemlst) "+str(len(dictlst))+" != "+str(len(elemlst))
872        if len(checkdictlst) != len(checkelemlst):
873            raise Exception,"ScrolledMultiEditor error: len(checkdictlst) != len(checkelemlst) "+str(len(checkdictlst))+" != "+str(len(checkelemlst))
874        wx.Dialog.__init__( # create dialog & sizer
875            self,parent,wx.ID_ANY,title,
876            style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)
877        mainSizer = wx.BoxSizer(wx.VERTICAL)
878        self.orig = []
879        self.dictlst = dictlst
880        self.elemlst = elemlst
881        self.checkdictlst = checkdictlst
882        self.checkelemlst = checkelemlst
883        self.StartCheckValues = [checkdictlst[i][checkelemlst[i]] for i in range(len(checkdictlst))]
884        self.ButtonIndex = {}
885        for d,i in zip(dictlst,elemlst):
886            self.orig.append(d[i])
887        # add a header if supplied
888        if header:
889            subSizer = wx.BoxSizer(wx.HORIZONTAL)
890            subSizer.Add((-1,-1),1,wx.EXPAND)
891            subSizer.Add(wx.StaticText(self,wx.ID_ANY,header))
892            subSizer.Add((-1,-1),1,wx.EXPAND)
893            mainSizer.Add(subSizer,0,wx.EXPAND,0)
894        # make OK button now, because we will need it for validation
895        self.OKbtn = wx.Button(self, wx.ID_OK)
896        self.OKbtn.SetDefault()
897        # create scrolled panel and sizer
898        panel = wxscroll.ScrolledPanel(self, wx.ID_ANY,size=size,
899            style = wx.TAB_TRAVERSAL|wx.SUNKEN_BORDER)
900        cols = 4
901        if CopyButton: cols += 1
902        subSizer = wx.FlexGridSizer(cols=cols,hgap=2,vgap=2)
903        self.ValidatedControlsList = [] # make list of TextCtrls
904        self.CheckControlsList = [] # make list of CheckBoxes
905        for i,(d,k) in enumerate(zip(dictlst,elemlst)):
906            if i >= len(prelbl): # label before TextCtrl, or put in a blank
907                subSizer.Add((-1,-1)) 
908            else:
909                subSizer.Add(wx.StaticText(panel,wx.ID_ANY,str(prelbl[i])))
910            kargs = {}
911            if i < len(minvals):
912                if minvals[i] is not None: kargs['min']=minvals[i]
913            if i < len(maxvals):
914                if maxvals[i] is not None: kargs['max']=maxvals[i]
915            if i < len(sizevals):
916                if sizevals[i]: kargs['size']=sizevals[i]
917            if CopyButton:
918                import wx.lib.colourselect as wscs
919                but = wscs.ColourSelect(label='v', # would like to use u'\u2193' or u'\u25BC' but not in WinXP
920                                        # is there a way to test?
921                                        parent=panel,
922                                        colour=(255,255,200),
923                                        size=wx.Size(30,23),
924                                        style=wx.RAISED_BORDER)
925                but.Bind(wx.EVT_BUTTON, self._OnCopyButton)
926                but.SetToolTipString('Press to copy adjacent value to all rows below')
927                self.ButtonIndex[but] = i
928                subSizer.Add(but)
929            # create the validated TextCrtl, store it and add it to the sizer
930            ctrl = ValidatedTxtCtrl(panel,d,k,OKcontrol=self.ControlOKButton,
931                                    **kargs)
932            self.ValidatedControlsList.append(ctrl)
933            subSizer.Add(ctrl)
934            if i < len(postlbl): # label after TextCtrl, or put in a blank
935                subSizer.Add(wx.StaticText(panel,wx.ID_ANY,str(postlbl[i])))
936            else:
937                subSizer.Add((-1,-1))
938            if i < len(checkdictlst):
939                ch = G2CheckBox(panel,checklabel,checkdictlst[i],checkelemlst[i])
940                self.CheckControlsList.append(ch)
941                subSizer.Add(ch)                   
942            else:
943                subSizer.Add((-1,-1))
944        # finish up ScrolledPanel
945        panel.SetSizer(subSizer)
946        panel.SetAutoLayout(1)
947        panel.SetupScrolling()
948        # patch for wx 2.9 on Mac
949        i,j= wx.__version__.split('.')[0:2]
950        if int(i)+int(j)/10. > 2.8 and 'wxOSX' in wx.PlatformInfo:
951            panel.SetMinSize((subSizer.GetSize()[0]+30,panel.GetSize()[1]))       
952        mainSizer.Add(panel,1, wx.ALL|wx.EXPAND,1)
953
954        # Sizer for OK/Close buttons. N.B. on Close changes are discarded
955        # by restoring the initial values
956        btnsizer = wx.BoxSizer(wx.HORIZONTAL)
957        btnsizer.Add(self.OKbtn)
958        btn = wx.Button(self, wx.ID_CLOSE,"Cancel") 
959        btn.Bind(wx.EVT_BUTTON,self._onClose)
960        btnsizer.Add(btn)
961        mainSizer.Add(btnsizer, 0, wx.ALIGN_CENTER|wx.ALL, 5)
962        # size out the window. Set it to be enlarged but not made smaller
963        self.SetSizer(mainSizer)
964        mainSizer.Fit(self)
965        self.SetMinSize(self.GetSize())
966
967    def _OnCopyButton(self,event):
968        'Implements the copy down functionality'
969        but = event.GetEventObject()
970        n = self.ButtonIndex.get(but)
971        if n is None: return
972        for i,(d,k,ctrl) in enumerate(zip(self.dictlst,self.elemlst,self.ValidatedControlsList)):
973            if i < n: continue
974            if i == n:
975                val = d[k]
976                continue
977            d[k] = val
978            ctrl.SetValue(val)
979        for i in range(len(self.checkdictlst)):
980            if i < n: continue
981            self.checkdictlst[i][self.checkelemlst[i]] = self.checkdictlst[n][self.checkelemlst[n]]
982            self.CheckControlsList[i].SetValue(self.checkdictlst[i][self.checkelemlst[i]])
983    def _onClose(self,event):
984        'Used on Cancel: Restore original values & close the window'
985        for d,i,v in zip(self.dictlst,self.elemlst,self.orig):
986            d[i] = v
987        for i in range(len(self.checkdictlst)):
988            self.checkdictlst[i][self.checkelemlst[i]] = self.StartCheckValues[i]
989        self.EndModal(wx.ID_CANCEL)
990       
991    def ControlOKButton(self,setvalue):
992        '''Enable or Disable the OK button for the dialog. Note that this is
993        passed into the ValidatedTxtCtrl for use by validators.
994
995        :param bool setvalue: if True, all entries in the dialog are
996          checked for validity. if False then the OK button is disabled.
997
998        '''
999        if setvalue: # turn button on, do only if all controls show as valid
1000            for ctrl in self.ValidatedControlsList:
1001                if ctrl.invalid:
1002                    self.OKbtn.Disable()
1003                    return
1004            else:
1005                self.OKbtn.Enable()
1006        else:
1007            self.OKbtn.Disable()
1008
1009################################################################################
1010#### Multichoice Dialog with set all, toggle & filter options
1011################################################################################
1012class G2MultiChoiceDialog(wx.Dialog):
1013    '''A dialog similar to MultiChoiceDialog except that buttons are
1014    added to set all choices and to toggle all choices.
1015
1016    :param wx.Frame ParentFrame: reference to parent frame
1017    :param str title: heading above list of choices
1018    :param str header: Title to place on window frame
1019    :param list ChoiceList: a list of choices where one will be selected
1020    :param bool toggle: If True (default) the toggle and select all buttons
1021      are displayed
1022    :param bool monoFont: If False (default), use a variable-spaced font;
1023      if True use a equally-spaced font.
1024    :param bool filterBox: If True (default) an input widget is placed on
1025      the window and only entries matching the entered text are shown.
1026    :param kw: optional keyword parameters for the wx.Dialog may
1027      be included such as size [which defaults to `(320,310)`] and
1028      style (which defaults to `wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER|wx.CENTRE| wx.OK | wx.CANCEL`);
1029      note that `wx.OK` and `wx.CANCEL` controls
1030      the presence of the eponymous buttons in the dialog.
1031    :returns: the name of the created dialog 
1032    '''
1033    def __init__(self,parent, title, header, ChoiceList, toggle=True,
1034                 monoFont=False, filterBox=True, **kw):
1035        # process keyword parameters, notably style
1036        options = {'size':(320,310), # default Frame keywords
1037                   'style':wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER|wx.CENTRE| wx.OK | wx.CANCEL,
1038                   }
1039        options.update(kw)
1040        self.ChoiceList = ChoiceList # list of choices (list of str values)
1041        self.Selections = len(self.ChoiceList) * [False,] # selection status for each choice (list of bools)
1042        self.filterlist = range(len(self.ChoiceList)) # list of the choice numbers that have been filtered (list of int indices)
1043        if options['style'] & wx.OK:
1044            useOK = True
1045            options['style'] ^= wx.OK
1046        else:
1047            useOK = False
1048        if options['style'] & wx.CANCEL:
1049            useCANCEL = True
1050            options['style'] ^= wx.CANCEL
1051        else:
1052            useCANCEL = False       
1053        # create the dialog frame
1054        wx.Dialog.__init__(self,parent,wx.ID_ANY,header,**options)
1055        # fill the dialog
1056        Sizer = wx.BoxSizer(wx.VERTICAL)
1057        topSizer = wx.BoxSizer(wx.HORIZONTAL)
1058        topSizer.Add(
1059            wx.StaticText(self,wx.ID_ANY,title,size=(-1,35)),
1060            1,wx.ALL|wx.EXPAND|WACV,1)
1061        if filterBox:
1062            self.timer = wx.Timer()
1063            self.timer.Bind(wx.EVT_TIMER,self.Filter)
1064            topSizer.Add(wx.StaticText(self,wx.ID_ANY,'Name \nFilter: '),0,wx.ALL|WACV,1)
1065            self.filterBox = wx.TextCtrl(self, wx.ID_ANY, size=(80,-1),style=wx.TE_PROCESS_ENTER)
1066            self.filterBox.Bind(wx.EVT_CHAR,self.onChar)
1067            self.filterBox.Bind(wx.EVT_TEXT_ENTER,self.Filter)
1068            topSizer.Add(self.filterBox,0,wx.ALL|WACV,0)
1069        Sizer.Add(topSizer,0,wx.ALL|wx.EXPAND,8)
1070        self.trigger = False
1071        self.clb = wx.CheckListBox(self, wx.ID_ANY, (30,30), wx.DefaultSize, ChoiceList)
1072        self.clb.Bind(wx.EVT_CHECKLISTBOX,self.OnCheck)
1073        if monoFont:
1074            font1 = wx.Font(self.clb.GetFont().GetPointSize(),
1075                            wx.MODERN, wx.NORMAL, wx.NORMAL, False)
1076            self.clb.SetFont(font1)
1077        Sizer.Add(self.clb,1,wx.LEFT|wx.RIGHT|wx.EXPAND,10)
1078        Sizer.Add((-1,10))
1079        # set/toggle buttons
1080        if toggle:
1081            bSizer = wx.BoxSizer(wx.VERTICAL)
1082            setBut = wx.Button(self,wx.ID_ANY,'Set All')
1083            setBut.Bind(wx.EVT_BUTTON,self._SetAll)
1084            bSizer.Add(setBut,0,wx.ALIGN_CENTER)
1085            bSizer.Add((-1,5))
1086            togBut = wx.Button(self,wx.ID_ANY,'Toggle All')
1087            togBut.Bind(wx.EVT_BUTTON,self._ToggleAll)
1088            bSizer.Add(togBut,0,wx.ALIGN_CENTER)
1089            Sizer.Add(bSizer,0,wx.LEFT,12)
1090        # OK/Cancel buttons
1091        btnsizer = wx.StdDialogButtonSizer()
1092        if useOK:
1093            self.OKbtn = wx.Button(self, wx.ID_OK)
1094            self.OKbtn.SetDefault()
1095            btnsizer.AddButton(self.OKbtn)
1096        if useCANCEL:
1097            btn = wx.Button(self, wx.ID_CANCEL)
1098            btnsizer.AddButton(btn)
1099        btnsizer.Realize()
1100        Sizer.Add((-1,5))
1101        Sizer.Add(btnsizer,0,wx.ALIGN_RIGHT,50)
1102        Sizer.Add((-1,20))
1103        # OK done, let's get outa here
1104        self.SetSizer(Sizer)
1105        self.CenterOnParent()
1106       
1107    def GetSelections(self):
1108        'Returns a list of the indices for the selected choices'
1109        # update self.Selections with settings for displayed items
1110        for i in range(len(self.filterlist)):
1111            self.Selections[self.filterlist[i]] = self.clb.IsChecked(i)
1112        # return all selections, shown or hidden
1113        return [i for i in range(len(self.Selections)) if self.Selections[i]]
1114       
1115    def SetSelections(self,selList):
1116        '''Sets the selection indices in selList as selected. Resets any previous
1117        selections for compatibility with wx.MultiChoiceDialog. Note that
1118        the state for only the filtered items is shown.
1119
1120        :param list selList: indices of items to be selected. These indices
1121          are referenced to the order in self.ChoiceList
1122        '''
1123        self.Selections = len(self.ChoiceList) * [False,] # reset selections
1124        for sel in selList:
1125            self.Selections[sel] = True
1126        self._ShowSelections()
1127
1128    def _ShowSelections(self):
1129        'Show the selection state for displayed items'
1130        self.clb.SetChecked(
1131            [i for i in range(len(self.filterlist)) if self.Selections[self.filterlist[i]]]
1132            ) # Note anything previously checked will be cleared.
1133           
1134    def _SetAll(self,event):
1135        'Set all viewed choices on'
1136        self.clb.SetChecked(range(len(self.filterlist)))
1137       
1138    def _ToggleAll(self,event):
1139        'flip the state of all viewed choices'
1140        for i in range(len(self.filterlist)):
1141            self.clb.Check(i,not self.clb.IsChecked(i))
1142           
1143    def onChar(self,event):
1144        'for keyboard events. self.trigger is used in self.OnCheck below'
1145        self.OKbtn.Enable(False)
1146        if event.GetKeyCode() == wx.WXK_SHIFT:
1147            self.trigger = True
1148        if self.timer.IsRunning():
1149            self.timer.Stop()
1150        self.timer.Start(1000,oneShot=True)
1151        event.Skip()
1152       
1153    def OnCheck(self,event):
1154        '''for CheckListBox events; if Shift key down this sets all unset
1155            entries below the selected one'''
1156        if self.trigger:
1157            id = event.GetSelection()
1158            name = self.clb.GetString(id)           
1159            iB = id-1
1160            if iB < 0:
1161                return
1162            while not self.clb.IsChecked(iB):
1163                self.clb.Check(iB)
1164                iB -= 1
1165                if iB < 0:
1166                    break
1167        self.trigger = not self.trigger
1168       
1169    def Filter(self,event):
1170        if self.timer.IsRunning():
1171            self.timer.Stop()
1172        self.GetSelections() # record current selections
1173        txt = self.filterBox.GetValue()
1174        self.clb.Clear()
1175       
1176        self.Update()
1177        self.filterlist = []
1178        if txt:
1179            txt = txt.lower()
1180            ChoiceList = []
1181            for i,item in enumerate(self.ChoiceList):
1182                if item.lower().find(txt) != -1:
1183                    ChoiceList.append(item)
1184                    self.filterlist.append(i)
1185        else:
1186            self.filterlist = range(len(self.ChoiceList))
1187            ChoiceList = self.ChoiceList
1188        self.clb.AppendItems(ChoiceList)
1189        self._ShowSelections()
1190        self.OKbtn.Enable(True)
1191
1192def SelectEdit1Var(G2frame,array,labelLst,elemKeysLst,dspLst,refFlgElem):
1193    '''Select a variable from a list, then edit it and select histograms
1194    to copy it to.
1195
1196    :param wx.Frame G2frame: main GSAS-II frame
1197    :param dict array: the array (dict or list) where values to be edited are kept
1198    :param list labelLst: labels for each data item
1199    :param list elemKeysLst: a list of lists of keys needed to be applied (see below)
1200      to obtain the value of each parameter
1201    :param list dspLst: list list of digits to be displayed (10,4) is 10 digits
1202      with 4 decimal places. Can be None.
1203    :param list refFlgElem: a list of lists of keys needed to be applied (see below)
1204      to obtain the refine flag for each parameter or None if the parameter
1205      does not have refine flag.
1206
1207    Example::
1208      array = data
1209      labelLst = ['v1','v2']
1210      elemKeysLst = [['v1'], ['v2',0]]
1211      refFlgElem = [None, ['v2',1]]
1212
1213     * The value for v1 will be in data['v1'] and this cannot be refined while,
1214     * The value for v2 will be in data['v2'][0] and its refinement flag is data['v2'][1]
1215    '''
1216    def unkey(dct,keylist):
1217        '''dive into a nested set of dicts/lists applying keys in keylist
1218        consecutively
1219        '''
1220        d = dct
1221        for k in keylist:
1222            d = d[k]
1223        return d
1224
1225    def OnChoice(event):
1226        'Respond when a parameter is selected in the Choice box'
1227        valSizer.DeleteWindows()
1228        lbl = event.GetString()
1229        copyopts['currentsel'] = lbl
1230        i = labelLst.index(lbl)
1231        OKbtn.Enable(True)
1232        ch.SetLabel(lbl)
1233        args = {}
1234        if dspLst[i]:
1235            args = {'nDig':dspLst[i]}
1236        Val = ValidatedTxtCtrl(
1237            dlg,
1238            unkey(array,elemKeysLst[i][:-1]),
1239            elemKeysLst[i][-1],
1240            **args)
1241        copyopts['startvalue'] = unkey(array,elemKeysLst[i])
1242        #unkey(array,elemKeysLst[i][:-1])[elemKeysLst[i][-1]] =
1243        valSizer.Add(Val,0,wx.LEFT,5)
1244        dlg.SendSizeEvent()
1245       
1246    # SelectEdit1Var execution begins here
1247    saveArray = copy.deepcopy(array) # keep original values
1248    TreeItemType = G2frame.PatternTree.GetItemText(G2frame.PickId)
1249    copyopts = {'InTable':False,"startvalue":None,'currentsel':None}       
1250    hst = G2frame.PatternTree.GetItemText(G2frame.PatternId)
1251    histList = G2pdG.GetHistsLikeSelected(G2frame)
1252    if not histList:
1253        G2frame.ErrorDialog('No match','No histograms match '+hst,G2frame.dataFrame)
1254        return
1255    dlg = wx.Dialog(G2frame.dataDisplay,wx.ID_ANY,'Set a parameter value',
1256        style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)
1257    mainSizer = wx.BoxSizer(wx.VERTICAL)
1258    mainSizer.Add((5,5))
1259    subSizer = wx.BoxSizer(wx.HORIZONTAL)
1260    subSizer.Add((-1,-1),1,wx.EXPAND)
1261    subSizer.Add(wx.StaticText(dlg,wx.ID_ANY,'Select a parameter and set a new value'))
1262    subSizer.Add((-1,-1),1,wx.EXPAND)
1263    mainSizer.Add(subSizer,0,wx.EXPAND,0)
1264    mainSizer.Add((0,10))
1265
1266    subSizer = wx.FlexGridSizer(0,2,5,0)
1267    subSizer.Add(wx.StaticText(dlg,wx.ID_ANY,'Parameter: '))
1268    ch = wx.Choice(dlg, wx.ID_ANY, choices = sorted(labelLst))
1269    ch.SetSelection(-1)
1270    ch.Bind(wx.EVT_CHOICE, OnChoice)
1271    subSizer.Add(ch)
1272    subSizer.Add(wx.StaticText(dlg,wx.ID_ANY,'Value: '))
1273    valSizer = wx.BoxSizer(wx.HORIZONTAL)
1274    subSizer.Add(valSizer)
1275    mainSizer.Add(subSizer)
1276
1277    mainSizer.Add((-1,20))
1278    subSizer = wx.BoxSizer(wx.HORIZONTAL)
1279    subSizer.Add(G2CheckBox(dlg, 'Edit in table ', copyopts, 'InTable'))
1280    mainSizer.Add(subSizer)
1281
1282    btnsizer = wx.StdDialogButtonSizer()
1283    OKbtn = wx.Button(dlg, wx.ID_OK,'Continue')
1284    OKbtn.Enable(False)
1285    OKbtn.SetDefault()
1286    OKbtn.Bind(wx.EVT_BUTTON,lambda event: dlg.EndModal(wx.ID_OK))
1287    btnsizer.AddButton(OKbtn)
1288    btn = wx.Button(dlg, wx.ID_CANCEL)
1289    btnsizer.AddButton(btn)
1290    btnsizer.Realize()
1291    mainSizer.Add((-1,5),1,wx.EXPAND,1)
1292    mainSizer.Add(btnsizer,0,wx.ALIGN_CENTER,0)
1293    mainSizer.Add((-1,10))
1294
1295    dlg.SetSizer(mainSizer)
1296    dlg.CenterOnParent()
1297    if dlg.ShowModal() != wx.ID_OK:
1298        array.update(saveArray)
1299        dlg.Destroy()
1300        return
1301    dlg.Destroy()
1302
1303    copyList = []
1304    lbl = copyopts['currentsel']
1305    dlg = G2MultiChoiceDialog(
1306        G2frame.dataFrame, 
1307        'Copy parameter '+lbl+' from\n'+hst,
1308        'Copy parameters', histList)
1309    dlg.CenterOnParent()
1310    try:
1311        if dlg.ShowModal() == wx.ID_OK:
1312            for i in dlg.GetSelections(): 
1313                copyList.append(histList[i])
1314        else:
1315            # reset the parameter since cancel was pressed
1316            array.update(saveArray)
1317            return
1318    finally:
1319        dlg.Destroy()
1320
1321    prelbl = [hst]
1322    i = labelLst.index(lbl)
1323    keyLst = elemKeysLst[i]
1324    refkeys = refFlgElem[i]
1325    dictlst = [unkey(array,keyLst[:-1])]
1326    if refkeys is not None:
1327        refdictlst = [unkey(array,refkeys[:-1])]
1328    else:
1329        refdictlst = None
1330    Id = GetPatternTreeItemId(G2frame,G2frame.root,hst)
1331    hstData = G2frame.PatternTree.GetItemPyData(GetPatternTreeItemId(G2frame,Id,'Instrument Parameters'))[0]
1332    for h in copyList:
1333        Id = GetPatternTreeItemId(G2frame,G2frame.root,h)
1334        instData = G2frame.PatternTree.GetItemPyData(GetPatternTreeItemId(G2frame,Id,'Instrument Parameters'))[0]
1335        if len(hstData) != len(instData) or hstData['Type'][0] != instData['Type'][0]:  #don't mix data types or lam & lam1/lam2 parms!
1336            print h+' not copied - instrument parameters not commensurate'
1337            continue
1338        hData = G2frame.PatternTree.GetItemPyData(GetPatternTreeItemId(G2frame,Id,TreeItemType))
1339        if TreeItemType == 'Instrument Parameters':
1340            hData = hData[0]
1341        #copy the value if it is changed or we will not edit in a table
1342        valNow = unkey(array,keyLst)
1343        if copyopts['startvalue'] != valNow or not copyopts['InTable']:
1344            unkey(hData,keyLst[:-1])[keyLst[-1]] = valNow
1345        prelbl += [h]
1346        dictlst += [unkey(hData,keyLst[:-1])]
1347        if refdictlst is not None:
1348            refdictlst += [unkey(hData,refkeys[:-1])]
1349    if refdictlst is None:
1350        args = {}
1351    else:
1352        args = {'checkdictlst':refdictlst,
1353                'checkelemlst':len(dictlst)*[refkeys[-1]],
1354                'checklabel':'Refine?'}
1355    if copyopts['InTable']:
1356        dlg = ScrolledMultiEditor(
1357            G2frame.dataDisplay,dictlst,
1358            len(dictlst)*[keyLst[-1]],prelbl,
1359            header='Editing parameter '+lbl,
1360            CopyButton=True,**args)
1361        dlg.CenterOnParent()
1362        if dlg.ShowModal() != wx.ID_OK:
1363            array.update(saveArray)
1364        dlg.Destroy()
1365
1366################################################################################
1367#### Single choice Dialog with set all, toggle & filter options
1368################################################################################
1369class G2SingleChoiceDialog(wx.Dialog):
1370    '''A dialog similar to wx.SingleChoiceDialog except that a filter can be
1371    added.
1372
1373    :param wx.Frame ParentFrame: reference to parent frame
1374    :param str title: heading above list of choices
1375    :param str header: Title to place on window frame
1376    :param list ChoiceList: a list of choices where one will be selected
1377    :param bool monoFont: If False (default), use a variable-spaced font;
1378      if True use a equally-spaced font.
1379    :param bool filterBox: If True (default) an input widget is placed on
1380      the window and only entries matching the entered text are shown.
1381    :param kw: optional keyword parameters for the wx.Dialog may
1382      be included such as size [which defaults to `(320,310)`] and
1383      style (which defaults to ``wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER | wx.CENTRE | wx.OK | wx.CANCEL``);
1384      note that ``wx.OK`` and ``wx.CANCEL`` controls
1385      the presence of the eponymous buttons in the dialog.
1386    :returns: the name of the created dialog
1387    '''
1388    def __init__(self,parent, title, header, ChoiceList, 
1389                 monoFont=False, filterBox=True, **kw):
1390        # process keyword parameters, notably style
1391        options = {'size':(320,310), # default Frame keywords
1392                   'style':wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER|wx.CENTRE| wx.OK | wx.CANCEL,
1393                   }
1394        options.update(kw)
1395        self.ChoiceList = ChoiceList
1396        self.filterlist = range(len(self.ChoiceList))
1397        if options['style'] & wx.OK:
1398            useOK = True
1399            options['style'] ^= wx.OK
1400        else:
1401            useOK = False
1402        if options['style'] & wx.CANCEL:
1403            useCANCEL = True
1404            options['style'] ^= wx.CANCEL
1405        else:
1406            useCANCEL = False       
1407        # create the dialog frame
1408        wx.Dialog.__init__(self,parent,wx.ID_ANY,header,**options)
1409        # fill the dialog
1410        Sizer = wx.BoxSizer(wx.VERTICAL)
1411        topSizer = wx.BoxSizer(wx.HORIZONTAL)
1412        topSizer.Add(
1413            wx.StaticText(self,wx.ID_ANY,title,size=(-1,35)),
1414            1,wx.ALL|wx.EXPAND|WACV,1)
1415        if filterBox:
1416            self.timer = wx.Timer()
1417            self.timer.Bind(wx.EVT_TIMER,self.Filter)
1418            topSizer.Add(wx.StaticText(self,wx.ID_ANY,'Filter: '),0,wx.ALL,1)
1419            self.filterBox = wx.TextCtrl(self, wx.ID_ANY, size=(80,-1),
1420                                         style=wx.TE_PROCESS_ENTER)
1421            self.filterBox.Bind(wx.EVT_CHAR,self.onChar)
1422            self.filterBox.Bind(wx.EVT_TEXT_ENTER,self.Filter)
1423        topSizer.Add(self.filterBox,0,wx.ALL,0)
1424        Sizer.Add(topSizer,0,wx.ALL|wx.EXPAND,8)
1425        self.clb = wx.ListBox(self, wx.ID_ANY, (30,30), wx.DefaultSize, ChoiceList)
1426        self.clb.Bind(wx.EVT_LEFT_DCLICK,self.onDoubleClick)
1427        if monoFont:
1428            font1 = wx.Font(self.clb.GetFont().GetPointSize(),
1429                            wx.MODERN, wx.NORMAL, wx.NORMAL, False)
1430            self.clb.SetFont(font1)
1431        Sizer.Add(self.clb,1,wx.LEFT|wx.RIGHT|wx.EXPAND,10)
1432        Sizer.Add((-1,10))
1433        # OK/Cancel buttons
1434        btnsizer = wx.StdDialogButtonSizer()
1435        if useOK:
1436            self.OKbtn = wx.Button(self, wx.ID_OK)
1437            self.OKbtn.SetDefault()
1438            btnsizer.AddButton(self.OKbtn)
1439        if useCANCEL:
1440            btn = wx.Button(self, wx.ID_CANCEL)
1441            btnsizer.AddButton(btn)
1442        btnsizer.Realize()
1443        Sizer.Add((-1,5))
1444        Sizer.Add(btnsizer,0,wx.ALIGN_RIGHT,50)
1445        Sizer.Add((-1,20))
1446        # OK done, let's get outa here
1447        self.SetSizer(Sizer)
1448    def GetSelection(self):
1449        'Returns the index of the selected choice'
1450        i = self.clb.GetSelection()
1451        if i < 0 or i >= len(self.filterlist):
1452            return wx.NOT_FOUND
1453        return self.filterlist[i]
1454    def onChar(self,event):
1455        self.OKbtn.Enable(False)
1456        if self.timer.IsRunning():
1457            self.timer.Stop()
1458        self.timer.Start(1000,oneShot=True)
1459        event.Skip()
1460    def Filter(self,event):
1461        if self.timer.IsRunning():
1462            self.timer.Stop()
1463        txt = self.filterBox.GetValue()
1464        self.clb.Clear()
1465        self.Update()
1466        self.filterlist = []
1467        if txt:
1468            txt = txt.lower()
1469            ChoiceList = []
1470            for i,item in enumerate(self.ChoiceList):
1471                if item.lower().find(txt) != -1:
1472                    ChoiceList.append(item)
1473                    self.filterlist.append(i)
1474        else:
1475            self.filterlist = range(len(self.ChoiceList))
1476            ChoiceList = self.ChoiceList
1477        self.clb.AppendItems(ChoiceList)
1478        self.OKbtn.Enable(True)
1479    def onDoubleClick(self,event):
1480        self.EndModal(wx.ID_OK)
1481
1482################################################################################
1483#### Custom checkbox that saves values into dict/list as used
1484################################################################################
1485class G2CheckBox(wx.CheckBox):
1486    '''A customized version of a CheckBox that automatically initializes
1487    the control to a supplied list or dict entry and updates that
1488    entry as the widget is used.
1489
1490    :param wx.Panel parent: name of panel or frame that will be
1491      the parent to the widget. Can be None.
1492    :param str label: text to put on check button
1493    :param dict/list loc: the dict or list with the initial value to be
1494      placed in the CheckBox.
1495    :param int/str key: the dict key or the list index for the value to be
1496      edited by the CheckBox. The ``loc[key]`` element must exist.
1497      The CheckBox will be initialized from this value.
1498      If the value is anything other that True (or 1), it will be taken as
1499      False.
1500    '''
1501    def __init__(self,parent,label,loc,key):
1502        wx.CheckBox.__init__(self,parent,id=wx.ID_ANY,label=label)
1503        self.loc = loc
1504        self.key = key
1505        self.SetValue(self.loc[self.key]==True)
1506        self.Bind(wx.EVT_CHECKBOX, self._OnCheckBox)
1507    def _OnCheckBox(self,event):
1508        self.loc[self.key] = self.GetValue()
1509        log.LogVarChange(self.loc,self.key)
1510
1511################################################################################
1512####
1513################################################################################
1514class PickTwoDialog(wx.Dialog):
1515    '''This does not seem to be in use
1516    '''
1517    def __init__(self,parent,title,prompt,names,choices):
1518        wx.Dialog.__init__(self,parent,-1,title, 
1519            pos=wx.DefaultPosition,style=wx.DEFAULT_DIALOG_STYLE)
1520        self.panel = wx.Panel(self)         #just a dummy - gets destroyed in Draw!
1521        self.prompt = prompt
1522        self.choices = choices
1523        self.names = names
1524        self.Draw()
1525
1526    def Draw(self):
1527        Indx = {}
1528       
1529        def OnSelection(event):
1530            Obj = event.GetEventObject()
1531            id = Indx[Obj.GetId()]
1532            self.choices[id] = Obj.GetValue().encode()  #to avoid Unicode versions
1533            self.Draw()
1534           
1535        self.panel.DestroyChildren()
1536        self.panel.Destroy()
1537        self.panel = wx.Panel(self)
1538        mainSizer = wx.BoxSizer(wx.VERTICAL)
1539        mainSizer.Add(wx.StaticText(self.panel,-1,self.prompt),0,wx.ALIGN_CENTER)
1540        for isel,name in enumerate(self.choices):
1541            lineSizer = wx.BoxSizer(wx.HORIZONTAL)
1542            lineSizer.Add(wx.StaticText(self.panel,-1,'Reference atom '+str(isel+1)),0,wx.ALIGN_CENTER)
1543            nameList = self.names[:]
1544            if isel:
1545                if self.choices[0] in nameList:
1546                    nameList.remove(self.choices[0])
1547            choice = wx.ComboBox(self.panel,-1,value=name,choices=nameList,
1548                style=wx.CB_READONLY|wx.CB_DROPDOWN)
1549            Indx[choice.GetId()] = isel
1550            choice.Bind(wx.EVT_COMBOBOX, OnSelection)
1551            lineSizer.Add(choice,0,WACV)
1552            mainSizer.Add(lineSizer)
1553        OkBtn = wx.Button(self.panel,-1,"Ok")
1554        OkBtn.Bind(wx.EVT_BUTTON, self.OnOk)
1555        CancelBtn = wx.Button(self.panel,-1,'Cancel')
1556        CancelBtn.Bind(wx.EVT_BUTTON, self.OnCancel)
1557        btnSizer = wx.BoxSizer(wx.HORIZONTAL)
1558        btnSizer.Add((20,20),1)
1559        btnSizer.Add(OkBtn)
1560        btnSizer.Add(CancelBtn)
1561        btnSizer.Add((20,20),1)
1562        mainSizer.Add(btnSizer,0,wx.EXPAND|wx.BOTTOM|wx.TOP, 10)
1563        self.panel.SetSizer(mainSizer)
1564        self.panel.Fit()
1565        self.Fit()
1566       
1567    def GetSelection(self):
1568        return self.choices
1569
1570    def OnOk(self,event):
1571        parent = self.GetParent()
1572        parent.Raise()
1573        self.EndModal(wx.ID_OK)             
1574       
1575    def OnCancel(self,event):
1576        parent = self.GetParent()
1577        parent.Raise()
1578        self.EndModal(wx.ID_CANCEL)
1579
1580################################################################################
1581#### Column-order selection
1582################################################################################
1583
1584def GetItemOrder(parent,keylist,vallookup,posdict):
1585    '''Creates a panel where items can be ordered into columns
1586   
1587    :param list keylist: is a list of keys for column assignments
1588    :param dict vallookup: is a dict keyed by names in keylist where each item is a dict.
1589       Each inner dict contains variable names as keys and their associated values
1590    :param dict posdict: is a dict keyed by names in keylist where each item is a dict.
1591       Each inner dict contains column numbers as keys and their associated
1592       variable name as a value. This is used for both input and output.
1593       
1594    '''
1595    dlg = wx.Dialog(parent,style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)
1596    sizer = wx.BoxSizer(wx.VERTICAL)
1597    spanel = OrderBox(dlg,keylist,vallookup,posdict)
1598    spanel.Fit()
1599    sizer.Add(spanel,1,wx.EXPAND)
1600    btnsizer = wx.StdDialogButtonSizer()
1601    btn = wx.Button(dlg, wx.ID_OK)
1602    btn.SetDefault()
1603    btnsizer.AddButton(btn)
1604    #btn = wx.Button(dlg, wx.ID_CANCEL)
1605    #btnsizer.AddButton(btn)
1606    btnsizer.Realize()
1607    sizer.Add(btnsizer, 0, wx.ALIGN_CENTER_VERTICAL|wx.EXPAND|wx.ALL, 5)
1608    dlg.SetSizer(sizer)
1609    sizer.Fit(dlg)
1610    val = dlg.ShowModal()
1611
1612################################################################################
1613####
1614################################################################################
1615class OrderBox(wxscroll.ScrolledPanel):
1616    '''Creates a panel with scrollbars where items can be ordered into columns
1617   
1618    :param list keylist: is a list of keys for column assignments
1619    :param dict vallookup: is a dict keyed by names in keylist where each item is a dict.
1620      Each inner dict contains variable names as keys and their associated values
1621    :param dict posdict: is a dict keyed by names in keylist where each item is a dict.
1622      Each inner dict contains column numbers as keys and their associated
1623      variable name as a value. This is used for both input and output.
1624     
1625    '''
1626    def __init__(self,parent,keylist,vallookup,posdict,*arg,**kw):
1627        self.keylist = keylist
1628        self.vallookup = vallookup
1629        self.posdict = posdict
1630        self.maxcol = 0
1631        for nam in keylist:
1632            posdict = self.posdict[nam]
1633            if posdict.keys():
1634                self.maxcol = max(self.maxcol, max(posdict))
1635        wxscroll.ScrolledPanel.__init__(self,parent,wx.ID_ANY,*arg,**kw)
1636        self.GBsizer = wx.GridBagSizer(4,4)
1637        self.SetBackgroundColour(WHITE)
1638        self.SetSizer(self.GBsizer)
1639        colList = [str(i) for i in range(self.maxcol+2)]
1640        for i in range(self.maxcol+1):
1641            wid = wx.StaticText(self,wx.ID_ANY,str(i),style=wx.ALIGN_CENTER)
1642            wid.SetBackgroundColour(DULL_YELLOW)
1643            wid.SetMinSize((50,-1))
1644            self.GBsizer.Add(wid,(0,i),flag=wx.ALIGN_CENTER|wx.EXPAND)
1645        self.chceDict = {}
1646        for row,nam in enumerate(self.keylist):
1647            posdict = self.posdict[nam]
1648            for col in posdict:
1649                lbl = posdict[col]
1650                pnl = wx.Panel(self,wx.ID_ANY)
1651                pnl.SetBackgroundColour(VERY_LIGHT_GREY)
1652                insize = wx.BoxSizer(wx.VERTICAL)
1653                wid = wx.Choice(pnl,wx.ID_ANY,choices=colList)
1654                insize.Add(wid,0,wx.EXPAND|wx.BOTTOM,3)
1655                wid.SetSelection(col)
1656                self.chceDict[wid] = (row,col)
1657                wid.Bind(wx.EVT_CHOICE,self.OnChoice)
1658                wid = wx.StaticText(pnl,wx.ID_ANY,lbl)
1659                insize.Add(wid,0,flag=wx.EXPAND)
1660                val = G2py3.FormatSigFigs(self.vallookup[nam][lbl],maxdigits=8)
1661                wid = wx.StaticText(pnl,wx.ID_ANY,'('+val+')')
1662                insize.Add(wid,0,flag=wx.EXPAND)
1663                pnl.SetSizer(insize)
1664                self.GBsizer.Add(pnl,(row+1,col),flag=wx.EXPAND)
1665        self.SetAutoLayout(1)
1666        self.SetupScrolling()
1667        self.SetMinSize((
1668            min(700,self.GBsizer.GetSize()[0]),
1669            self.GBsizer.GetSize()[1]+20))
1670    def OnChoice(self,event):
1671        '''Called when a column is assigned to a variable
1672        '''
1673        row,col = self.chceDict[event.EventObject] # which variable was this?
1674        newcol = event.Selection # where will it be moved?
1675        if newcol == col:
1676            return # no change: nothing to do!
1677        prevmaxcol = self.maxcol # save current table size
1678        key = self.keylist[row] # get the key for the current row
1679        lbl = self.posdict[key][col] # selected variable name
1680        lbl1 = self.posdict[key].get(col+1,'') # next variable name, if any
1681        # if a posXXX variable is selected, and the next variable is posXXX, move them together
1682        repeat = 1
1683        if lbl[:3] == 'pos' and lbl1[:3] == 'int' and lbl[3:] == lbl1[3:]:
1684            repeat = 2
1685        for i in range(repeat): # process the posXXX and then the intXXX (or a single variable)
1686            col += i
1687            newcol += i
1688            if newcol in self.posdict[key]:
1689                # find first non-blank after newcol
1690                for mtcol in range(newcol+1,self.maxcol+2):
1691                    if mtcol not in self.posdict[key]: break
1692                l1 = range(mtcol,newcol,-1)+[newcol]
1693                l = range(mtcol-1,newcol-1,-1)+[col]
1694            else:
1695                l1 = [newcol]
1696                l = [col]
1697            # move all of the items, starting from the last column
1698            for newcol,col in zip(l1,l):
1699                #print 'moving',col,'to',newcol
1700                self.posdict[key][newcol] = self.posdict[key][col]
1701                del self.posdict[key][col]
1702                self.maxcol = max(self.maxcol,newcol)
1703                obj = self.GBsizer.FindItemAtPosition((row+1,col))
1704                self.GBsizer.SetItemPosition(obj.GetWindow(),(row+1,newcol))
1705                for wid in obj.GetWindow().Children:
1706                    if wid in self.chceDict:
1707                        self.chceDict[wid] = (row,newcol)
1708                        wid.SetSelection(self.chceDict[wid][1])
1709        # has the table gotten larger? If so we need new column heading(s)
1710        if prevmaxcol != self.maxcol:
1711            for i in range(prevmaxcol+1,self.maxcol+1):
1712                wid = wx.StaticText(self,wx.ID_ANY,str(i),style=wx.ALIGN_CENTER)
1713                wid.SetBackgroundColour(DULL_YELLOW)
1714                wid.SetMinSize((50,-1))
1715                self.GBsizer.Add(wid,(0,i),flag=wx.ALIGN_CENTER|wx.EXPAND)
1716            colList = [str(i) for i in range(self.maxcol+2)]
1717            for wid in self.chceDict:
1718                wid.SetItems(colList)
1719                wid.SetSelection(self.chceDict[wid][1])
1720        self.GBsizer.Layout()
1721        self.FitInside()
1722
1723################################################################################
1724#### Help support routines
1725################################################################################
1726################################################################################
1727class MyHelp(wx.Menu):
1728    '''
1729    A class that creates the contents of a help menu.
1730    The menu will start with two entries:
1731
1732    * 'Help on <helpType>': where helpType is a reference to an HTML page to
1733      be opened
1734    * About: opens an About dialog using OnHelpAbout. N.B. on the Mac this
1735      gets moved to the App menu to be consistent with Apple style.
1736
1737    NOTE: for this to work properly with respect to system menus, the title
1738    for the menu must be &Help, or it will not be processed properly:
1739
1740    ::
1741
1742       menu.Append(menu=MyHelp(self,...),title="&Help")
1743
1744    '''
1745    def __init__(self,frame,helpType=None,helpLbl=None,morehelpitems=[],title=''):
1746        wx.Menu.__init__(self,title)
1747        self.HelpById = {}
1748        self.frame = frame
1749        self.Append(help='', id=wx.ID_ABOUT, kind=wx.ITEM_NORMAL,
1750            text='&About GSAS-II')
1751        frame.Bind(wx.EVT_MENU, self.OnHelpAbout, id=wx.ID_ABOUT)
1752        if GSASIIpath.whichsvn():
1753            helpobj = self.Append(
1754                help='', id=wx.ID_ANY, kind=wx.ITEM_NORMAL,
1755                text='&Check for updates')
1756            frame.Bind(wx.EVT_MENU, self.OnCheckUpdates, helpobj)
1757            helpobj = self.Append(
1758                help='', id=wx.ID_ANY, kind=wx.ITEM_NORMAL,
1759                text='&Regress to an old GSAS-II version')
1760            frame.Bind(wx.EVT_MENU, self.OnSelectVersion, helpobj)
1761        for lbl,indx in morehelpitems:
1762            helpobj = self.Append(text=lbl,
1763                id=wx.ID_ANY, kind=wx.ITEM_NORMAL)
1764            frame.Bind(wx.EVT_MENU, self.OnHelpById, helpobj)
1765            self.HelpById[helpobj.GetId()] = indx
1766        # add a help item only when helpType is specified
1767        if helpType is not None:
1768            self.AppendSeparator()
1769            if helpLbl is None: helpLbl = helpType
1770            helpobj = self.Append(text='Help on '+helpLbl,
1771                                  id=wx.ID_ANY, kind=wx.ITEM_NORMAL)
1772            frame.Bind(wx.EVT_MENU, self.OnHelpById, helpobj)
1773            self.HelpById[helpobj.GetId()] = helpType
1774       
1775    def OnHelpById(self,event):
1776        '''Called when Help on... is pressed in a menu. Brings up
1777        a web page for documentation.
1778        '''
1779        helpType = self.HelpById.get(event.GetId())
1780        if helpType is None:
1781            print 'Error: help lookup failed!',event.GetEventObject()
1782            print 'id=',event.GetId()
1783        #elif helpType == 'OldTutorials': # this will go away
1784            #ShowHelp(helpType,self.frame)
1785        elif helpType == 'Tutorials': 
1786            dlg = OpenTutorial(self.frame)
1787            dlg.ShowModal()
1788            dlg.Destroy()
1789            return
1790        else:
1791            ShowHelp(helpType,self.frame)
1792
1793    def OnHelpAbout(self, event):
1794        "Display an 'About GSAS-II' box"
1795        import GSASII
1796        info = wx.AboutDialogInfo()
1797        info.Name = 'GSAS-II'
1798        ver = GSASIIpath.svnGetRev()
1799        if ver: 
1800            info.Version = 'Revision '+str(ver)+' (svn), version '+GSASII.__version__
1801        else:
1802            info.Version = 'Revision '+str(GSASIIpath.GetVersionNumber())+' (.py files), version '+GSASII.__version__
1803        #info.Developers = ['Robert B. Von Dreele','Brian H. Toby']
1804        info.Copyright = ('(c) ' + time.strftime('%Y') +
1805''' Argonne National Laboratory
1806This product includes software developed
1807by the UChicago Argonne, LLC, as
1808Operator of Argonne National Laboratory.''')
1809        info.Description = '''General Structure Analysis System-II (GSAS-II)
1810Robert B. Von Dreele and Brian H. Toby
1811
1812Please cite as:
1813B.H. Toby & R.B. Von Dreele, J. Appl. Cryst. 46, 544-549 (2013) '''
1814
1815        info.WebSite = ("https://subversion.xray.aps.anl.gov/trac/pyGSAS","GSAS-II home page")
1816        wx.AboutBox(info)
1817
1818    def OnCheckUpdates(self,event):
1819        '''Check if the GSAS-II repository has an update for the current source files
1820        and perform that update if requested.
1821        '''
1822        if not GSASIIpath.whichsvn():
1823            dlg = wx.MessageDialog(self.frame,
1824                                   'No Subversion','Cannot update GSAS-II because subversion (svn) was not found.',
1825                                   wx.OK)
1826            dlg.ShowModal()
1827            dlg.Destroy()
1828            return
1829        wx.BeginBusyCursor()
1830        local = GSASIIpath.svnGetRev()
1831        if local is None: 
1832            wx.EndBusyCursor()
1833            dlg = wx.MessageDialog(self.frame,
1834                                   'Unable to run subversion on the GSAS-II current directory. Is GSAS-II installed correctly?',
1835                                   'Subversion error',
1836                                   wx.OK)
1837            dlg.ShowModal()
1838            dlg.Destroy()
1839            return
1840        print 'Installed GSAS-II version: '+local
1841        repos = GSASIIpath.svnGetRev(local=False)
1842        wx.EndBusyCursor()
1843        if repos is None: 
1844            dlg = wx.MessageDialog(self.frame,
1845                                   'Unable to access the GSAS-II server. Is this computer on the internet?',
1846                                   'Server unavailable',
1847                                   wx.OK)
1848            dlg.ShowModal()
1849            dlg.Destroy()
1850            return
1851        print 'GSAS-II version on server: '+repos
1852        if local == repos:
1853            dlg = wx.MessageDialog(self.frame,
1854                                   'GSAS-II is up-to-date. Version '+local+' is already loaded.',
1855                                   'GSAS-II Up-to-date',
1856                                   wx.OK)
1857            dlg.ShowModal()
1858            dlg.Destroy()
1859            return
1860        mods = GSASIIpath.svnFindLocalChanges()
1861        if mods:
1862            dlg = wx.MessageDialog(self.frame,
1863                                   'You have version '+local+
1864                                   ' of GSAS-II installed, but the current version is '+repos+
1865                                   '. However, '+str(len(mods))+
1866                                   ' file(s) on your local computer have been modified.'
1867                                   ' Updating will attempt to merge your local changes with '
1868                                   'the latest GSAS-II version, but if '
1869                                   'conflicts arise, local changes will be '
1870                                   'discarded. It is also possible that the '
1871                                   'local changes my prevent GSAS-II from running. '
1872                                   'Press OK to start an update if this is acceptable:',
1873                                   'Local GSAS-II Mods',
1874                                   wx.OK|wx.CANCEL)
1875            if dlg.ShowModal() != wx.ID_OK:
1876                dlg.Destroy()
1877                return
1878            else:
1879                dlg.Destroy()
1880        else:
1881            dlg = wx.MessageDialog(self.frame,
1882                                   'You have version '+local+
1883                                   ' of GSAS-II installed, but the current version is '+repos+
1884                                   '. Press OK to start an update:',
1885                                   'GSAS-II Updates',
1886                                   wx.OK|wx.CANCEL)
1887            if dlg.ShowModal() != wx.ID_OK:
1888                dlg.Destroy()
1889                return
1890            dlg.Destroy()
1891        print 'start updates'
1892        dlg = wx.MessageDialog(self.frame,
1893                               'Your project will now be saved, GSAS-II will exit and an update '
1894                               'will be performed and GSAS-II will restart. Press Cancel to '
1895                               'abort the update',
1896                               'Start update?',
1897                               wx.OK|wx.CANCEL)
1898        if dlg.ShowModal() != wx.ID_OK:
1899            dlg.Destroy()
1900            return
1901        dlg.Destroy()
1902        self.frame.OnFileSave(event)
1903        GSASIIpath.svnUpdateProcess(projectfile=self.frame.GSASprojectfile)
1904        return
1905
1906    def OnSelectVersion(self,event):
1907        '''Allow the user to select a specific version of GSAS-II
1908        '''
1909        if not GSASIIpath.whichsvn():
1910            dlg = wx.MessageDialog(self,'No Subversion','Cannot update GSAS-II because subversion (svn) '+
1911                                   'was not found.'
1912                                   ,wx.OK)
1913            dlg.ShowModal()
1914            return
1915        local = GSASIIpath.svnGetRev()
1916        if local is None: 
1917            dlg = wx.MessageDialog(self.frame,
1918                                   'Unable to run subversion on the GSAS-II current directory. Is GSAS-II installed correctly?',
1919                                   'Subversion error',
1920                                   wx.OK)
1921            dlg.ShowModal()
1922            return
1923        mods = GSASIIpath.svnFindLocalChanges()
1924        if mods:
1925            dlg = wx.MessageDialog(self.frame,
1926                                   'You have version '+local+
1927                                   ' of GSAS-II installed'
1928                                   '. However, '+str(len(mods))+
1929                                   ' file(s) on your local computer have been modified.'
1930                                   ' Downdating will attempt to merge your local changes with '
1931                                   'the selected GSAS-II version. '
1932                                   'Downdating is not encouraged because '
1933                                   'if merging is not possible, your local changes will be '
1934                                   'discarded. It is also possible that the '
1935                                   'local changes my prevent GSAS-II from running. '
1936                                   'Press OK to continue anyway.',
1937                                   'Local GSAS-II Mods',
1938                                   wx.OK|wx.CANCEL)
1939            if dlg.ShowModal() != wx.ID_OK:
1940                dlg.Destroy()
1941                return
1942            dlg.Destroy()
1943        dlg = downdate(parent=self.frame)
1944        if dlg.ShowModal() == wx.ID_OK:
1945            ver = dlg.getVersion()
1946        else:
1947            dlg.Destroy()
1948            return
1949        dlg.Destroy()
1950        print('start regress to '+str(ver))
1951        GSASIIpath.svnUpdateProcess(
1952            projectfile=self.frame.GSASprojectfile,
1953            version=str(ver)
1954            )
1955        self.frame.OnFileSave(event)
1956        return
1957
1958################################################################################
1959class AddHelp(wx.Menu):
1960    '''For the Mac: creates an entry to the help menu of type
1961    'Help on <helpType>': where helpType is a reference to an HTML page to
1962    be opened.
1963
1964    NOTE: when appending this menu (menu.Append) be sure to set the title to
1965    '&Help' so that wx handles it correctly.
1966    '''
1967    def __init__(self,frame,helpType,helpLbl=None,title=''):
1968        wx.Menu.__init__(self,title)
1969        self.frame = frame
1970        if helpLbl is None: helpLbl = helpType
1971        # add a help item only when helpType is specified
1972        helpobj = self.Append(text='Help on '+helpLbl,
1973                              id=wx.ID_ANY, kind=wx.ITEM_NORMAL)
1974        frame.Bind(wx.EVT_MENU, self.OnHelpById, helpobj)
1975        self.HelpById = helpType
1976       
1977    def OnHelpById(self,event):
1978        '''Called when Help on... is pressed in a menu. Brings up
1979        a web page for documentation.
1980        '''
1981        ShowHelp(self.HelpById,self.frame)
1982
1983################################################################################
1984class HelpButton(wx.Button):
1985    '''Create a help button that displays help information.
1986    The text is displayed in a modal message window.
1987
1988    TODO: it might be nice if it were non-modal: e.g. it stays around until
1989    the parent is deleted or the user closes it, but this did not work for
1990    me.
1991
1992    :param parent: the panel which will be the parent of the button
1993    :param str msg: the help text to be displayed
1994    '''
1995    def __init__(self,parent,msg):
1996        if sys.platform == "darwin": 
1997            wx.Button.__init__(self,parent,wx.ID_HELP)
1998        else:
1999            wx.Button.__init__(self,parent,wx.ID_ANY,'?',style=wx.BU_EXACTFIT)
2000        self.Bind(wx.EVT_BUTTON,self._onPress)
2001        self.msg=StripIndents(msg)
2002        self.parent = parent
2003    def _onClose(self,event):
2004        self.dlg.EndModal(wx.ID_CANCEL)
2005    def _onPress(self,event):
2006        'Respond to a button press by displaying the requested text'
2007        #dlg = wx.MessageDialog(self.parent,self.msg,'Help info',wx.OK)
2008        self.dlg = wx.Dialog(self.parent,wx.ID_ANY,'Help information', 
2009                        style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)
2010        #self.dlg.SetBackgroundColour(wx.WHITE)
2011        mainSizer = wx.BoxSizer(wx.VERTICAL)
2012        txt = wx.StaticText(self.dlg,wx.ID_ANY,self.msg)
2013        mainSizer.Add(txt,1,wx.ALL|wx.EXPAND,10)
2014        txt.SetBackgroundColour(wx.WHITE)
2015
2016        btnsizer = wx.BoxSizer(wx.HORIZONTAL)
2017        btn = wx.Button(self.dlg, wx.ID_CLOSE) 
2018        btn.Bind(wx.EVT_BUTTON,self._onClose)
2019        btnsizer.Add(btn)
2020        mainSizer.Add(btnsizer, 0, wx.ALIGN_CENTER|wx.ALL, 5)
2021        self.dlg.SetSizer(mainSizer)
2022        mainSizer.Fit(self.dlg)
2023        self.dlg.CenterOnParent()
2024        self.dlg.ShowModal()
2025        self.dlg.Destroy()
2026################################################################################
2027class MyHtmlPanel(wx.Panel):
2028    '''Defines a panel to display HTML help information, as an alternative to
2029    displaying help information in a web browser.
2030    '''
2031    def __init__(self, frame, id):
2032        self.frame = frame
2033        wx.Panel.__init__(self, frame, id)
2034        sizer = wx.BoxSizer(wx.VERTICAL)
2035        back = wx.Button(self, -1, "Back")
2036        back.Bind(wx.EVT_BUTTON, self.OnBack)
2037        self.htmlwin = G2HtmlWindow(self, id, size=(750,450))
2038        sizer.Add(self.htmlwin, 1,wx.EXPAND)
2039        sizer.Add(back, 0, wx.ALIGN_LEFT, 0)
2040        self.SetSizer(sizer)
2041        sizer.Fit(frame)       
2042        self.Bind(wx.EVT_SIZE,self.OnHelpSize)
2043    def OnHelpSize(self,event):         #does the job but weirdly!!
2044        anchor = self.htmlwin.GetOpenedAnchor()
2045        if anchor:           
2046            self.htmlwin.ScrollToAnchor(anchor)
2047            wx.CallAfter(self.htmlwin.ScrollToAnchor,anchor)
2048            event.Skip()
2049    def OnBack(self, event):
2050        self.htmlwin.HistoryBack()
2051    def LoadFile(self,file):
2052        pos = file.rfind('#')
2053        if pos != -1:
2054            helpfile = file[:pos]
2055            helpanchor = file[pos+1:]
2056        else:
2057            helpfile = file
2058            helpanchor = None
2059        self.htmlwin.LoadPage(helpfile)
2060        if helpanchor is not None:
2061            self.htmlwin.ScrollToAnchor(helpanchor)
2062            xs,ys = self.htmlwin.GetViewStart()
2063            self.htmlwin.Scroll(xs,ys-1)
2064################################################################################
2065class G2HtmlWindow(wx.html.HtmlWindow):
2066    '''Displays help information in a primitive HTML browser type window
2067    '''
2068    def __init__(self, parent, *args, **kwargs):
2069        self.parent = parent
2070        wx.html.HtmlWindow.__init__(self, parent, *args, **kwargs)
2071    def LoadPage(self, *args, **kwargs):
2072        wx.html.HtmlWindow.LoadPage(self, *args, **kwargs)
2073        self.TitlePage()
2074    def OnLinkClicked(self, *args, **kwargs):
2075        wx.html.HtmlWindow.OnLinkClicked(self, *args, **kwargs)
2076        xs,ys = self.GetViewStart()
2077        self.Scroll(xs,ys-1)
2078        self.TitlePage()
2079    def HistoryBack(self, *args, **kwargs):
2080        wx.html.HtmlWindow.HistoryBack(self, *args, **kwargs)
2081        self.TitlePage()
2082    def TitlePage(self):
2083        self.parent.frame.SetTitle(self.GetOpenedPage() + ' -- ' + 
2084            self.GetOpenedPageTitle())
2085
2086################################################################################
2087def StripIndents(msg):
2088    'Strip indentation from multiline strings'
2089    msg1 = msg.replace('\n ','\n')
2090    while msg != msg1:
2091        msg = msg1
2092        msg1 = msg.replace('\n ','\n')
2093    return msg.replace('\n\t','\n')
2094
2095def G2MessageBox(parent,msg,title='Error'):
2096    '''Simple code to display a error or warning message
2097    '''
2098    dlg = wx.MessageDialog(parent,StripIndents(msg), title, wx.OK)
2099    dlg.ShowModal()
2100    dlg.Destroy()
2101       
2102################################################################################
2103class downdate(wx.Dialog):
2104    '''Dialog to allow a user to select a version of GSAS-II to install
2105    '''
2106    def __init__(self,parent=None):
2107        style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER
2108        wx.Dialog.__init__(self, parent, wx.ID_ANY, 'Select Version', style=style)
2109        pnl = wx.Panel(self)
2110        sizer = wx.BoxSizer(wx.VERTICAL)
2111        insver = GSASIIpath.svnGetRev(local=True)
2112        curver = int(GSASIIpath.svnGetRev(local=False))
2113        label = wx.StaticText(
2114            pnl,  wx.ID_ANY,
2115            'Select a specific GSAS-II version to install'
2116            )
2117        sizer.Add(label, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
2118        sizer1 = wx.BoxSizer(wx.HORIZONTAL)
2119        sizer1.Add(
2120            wx.StaticText(pnl,  wx.ID_ANY,
2121                          'Currently installed version: '+str(insver)),
2122            0, wx.ALIGN_CENTRE|wx.ALL, 5)
2123        sizer.Add(sizer1)
2124        sizer1 = wx.BoxSizer(wx.HORIZONTAL)
2125        sizer1.Add(
2126            wx.StaticText(pnl,  wx.ID_ANY,
2127                          'Select GSAS-II version to install: '),
2128            0, wx.ALIGN_CENTRE|wx.ALL, 5)
2129        self.spin = wx.SpinCtrl(pnl, wx.ID_ANY,size=(150,-1))
2130        self.spin.SetRange(1, curver)
2131        self.spin.SetValue(curver)
2132        self.Bind(wx.EVT_SPINCTRL, self._onSpin, self.spin)
2133        self.Bind(wx.EVT_KILL_FOCUS, self._onSpin, self.spin)
2134        sizer1.Add(self.spin)
2135        sizer.Add(sizer1)
2136
2137        line = wx.StaticLine(pnl,-1, size=(-1,3), style=wx.LI_HORIZONTAL)
2138        sizer.Add(line, 0, wx.EXPAND|wx.ALIGN_CENTER|wx.ALL, 10)
2139
2140        self.text = wx.StaticText(pnl,  wx.ID_ANY, "")
2141        sizer.Add(self.text, 0, wx.ALIGN_LEFT|wx.EXPAND|wx.ALL, 5)
2142
2143        line = wx.StaticLine(pnl,-1, size=(-1,3), style=wx.LI_HORIZONTAL)
2144        sizer.Add(line, 0, wx.EXPAND|wx.ALIGN_CENTER|wx.ALL, 10)
2145        sizer.Add(
2146            wx.StaticText(
2147                pnl,  wx.ID_ANY,
2148                'If "Install" is pressed, your project will be saved;\n'
2149                'GSAS-II will exit; The specified version will be loaded\n'
2150                'and GSAS-II will restart. Press "Cancel" to abort.'),
2151            0, wx.EXPAND|wx.ALL, 10)
2152        btnsizer = wx.StdDialogButtonSizer()
2153        btn = wx.Button(pnl, wx.ID_OK, "Install")
2154        btn.SetDefault()
2155        btnsizer.AddButton(btn)
2156        btn = wx.Button(pnl, wx.ID_CANCEL)
2157        btnsizer.AddButton(btn)
2158        btnsizer.Realize()
2159        sizer.Add(btnsizer, 0, wx.ALIGN_CENTER|wx.ALL, 5)
2160        pnl.SetSizer(sizer)
2161        sizer.Fit(self)
2162        self.topsizer=sizer
2163        self.CenterOnParent()
2164        self._onSpin(None)
2165
2166    def _onSpin(self,event):
2167        'Called to load info about the selected version in the dialog'
2168        ver = self.spin.GetValue()
2169        d = GSASIIpath.svnGetLog(version=ver)
2170        date = d.get('date','?').split('T')[0]
2171        s = '(Version '+str(ver)+' created '+date
2172        s += ' by '+d.get('author','?')+')'
2173        msg = d.get('msg')
2174        if msg: s += '\n\nComment: '+msg
2175        self.text.SetLabel(s)
2176        self.topsizer.Fit(self)
2177
2178    def getVersion(self):
2179        'Get the version number in the dialog'
2180        return self.spin.GetValue()
2181################################################################################
2182#### Display Help information
2183################################################################################
2184# define some globals
2185htmlPanel = None
2186htmlFrame = None
2187htmlFirstUse = True
2188helpLocDict = {}
2189path2GSAS2 = os.path.dirname(os.path.realpath(__file__)) # save location of this file
2190def ShowHelp(helpType,frame):
2191    '''Called to bring up a web page for documentation.'''
2192    global htmlFirstUse
2193    # look up a definition for help info from dict
2194    helplink = helpLocDict.get(helpType)
2195    if helplink is None:
2196        # no defined link to use, create a default based on key
2197        helplink = 'gsasII.html#'+helpType.replace(' ','_')
2198    helplink = os.path.join(path2GSAS2,'help',helplink)
2199    # determine if a web browser or the internal viewer should be used for help info
2200    if GSASIIpath.GetConfigValue('Help_mode'):
2201        helpMode = GSASIIpath.GetConfigValue('Help_mode')
2202    else:
2203        helpMode = 'browser'
2204    if helpMode == 'internal':
2205        try:
2206            htmlPanel.LoadFile(helplink)
2207            htmlFrame.Raise()
2208        except:
2209            htmlFrame = wx.Frame(frame, -1, size=(610, 510))
2210            htmlFrame.Show(True)
2211            htmlFrame.SetTitle("HTML Window") # N.B. reset later in LoadFile
2212            htmlPanel = MyHtmlPanel(htmlFrame,-1)
2213            htmlPanel.LoadFile(helplink)
2214    else:
2215        pfx = "file://"
2216        if sys.platform.lower().startswith('win'):
2217            pfx = ''
2218        if htmlFirstUse:
2219            webbrowser.open_new(pfx+helplink)
2220            htmlFirstUse = False
2221        else:
2222            webbrowser.open(pfx+helplink, new=0, autoraise=True)
2223def ShowWebPage(URL,frame):
2224    '''Called to show a tutorial web page.
2225    '''
2226    global htmlFirstUse
2227    # determine if a web browser or the internal viewer should be used for help info
2228    if GSASIIpath.GetConfigValue('Help_mode'):
2229        helpMode = GSASIIpath.GetConfigValue('Help_mode')
2230    else:
2231        helpMode = 'browser'
2232    if helpMode == 'internal':
2233        try:
2234            htmlPanel.LoadFile(URL)
2235            htmlFrame.Raise()
2236        except:
2237            htmlFrame = wx.Frame(frame, -1, size=(610, 510))
2238            htmlFrame.Show(True)
2239            htmlFrame.SetTitle("HTML Window") # N.B. reset later in LoadFile
2240            htmlPanel = MyHtmlPanel(htmlFrame,-1)
2241            htmlPanel.LoadFile(URL)
2242    else:
2243        if URL.startswith('http'): 
2244            pfx = ''
2245        elif sys.platform.lower().startswith('win'):
2246            pfx = ''
2247        else:
2248            pfx = "file://"
2249        if htmlFirstUse:
2250            webbrowser.open_new(pfx+URL)
2251            htmlFirstUse = False
2252        else:
2253            webbrowser.open(pfx+URL, new=0, autoraise=True)
2254################################################################################
2255#### Tutorials selector
2256################################################################################
2257G2BaseURL = "https://subversion.xray.aps.anl.gov/pyGSAS"
2258# N.B. tutorialCatalog is generated by routine catalog.py, which also generates the appropriate
2259# empty directories (.../MT/* .../trunk/GSASII/* *=[help,Exercises])
2260tutorialCatalog = (
2261    # tutorial dir,      exercise dir,      web page file name                                title for page
2262
2263    ['StartingGSASII', 'StartingGSASII', 'Starting GSAS.htm',
2264       'Starting GSAS-II'],
2265       
2266    ['FitPeaks', 'FitPeaks', 'Fit Peaks.htm',
2267       'Fitting individual peaks & autoindexing'],
2268       
2269    ['CWNeutron', 'CWNeutron', 'Neutron CW Powder Data.htm',
2270       'CW Neutron Powder fit for Yttrium-Iron Garnet'],
2271    ['LabData', 'LabData', 'Laboratory X.htm',
2272       'Fitting laboratory X-ray powder data for fluoroapatite'],
2273    ['CWCombined', 'CWCombined', 'Combined refinement.htm',
2274       'Combined X-ray/CW-neutron refinement of PbSO4'],
2275    ['TOF-CW Joint Refinement', 'TOF-CW Joint Refinement', 'TOF combined XN Rietveld refinement in GSAS.htm',
2276       'Combined X-ray/TOF-neutron Rietveld refinement'],
2277    ['SeqRefine', 'SeqRefine', 'SequentialTutorial.htm',
2278       'Sequential refinement of multiple datasets'],
2279    ['SeqParametric', 'SeqParametric', 'ParametricFitting.htm',
2280       'Parametric Fitting and Pseudo Variables for Sequential Fits'],
2281       
2282    ['CFjadarite', 'CFjadarite', 'Charge Flipping in GSAS.htm',
2283       'Charge Flipping structure solution for jadarite'],
2284    ['CFsucrose', 'CFsucrose', 'Charge Flipping - sucrose.htm',
2285       'Charge Flipping structure solution for sucrose'],
2286    ['TOF Charge Flipping', 'TOF Charge Flipping', 'Charge Flipping with TOF single crystal data in GSASII.htm',
2287       'Charge flipping with neutron TOF single crystal data'],
2288    ['MCsimanneal', 'MCsimanneal', 'MCSA in GSAS.htm',
2289       'Monte-Carlo simulated annealing structure'],
2290
2291    ['2DCalibration', '2DCalibration', 'Calibration of an area detector in GSAS.htm',
2292       'Calibration of an area detector'],
2293    ['2DIntegration', '2DIntegration', 'Integration of area detector data in GSAS.htm',
2294       'Integration of area detector data'],
2295    ['TOF Calibration', 'TOF Calibration', 'Calibration of a TOF powder diffractometer.htm',
2296       'Calibration of a Neutron TOF diffractometer'],
2297       
2298    ['2DStrain', '2DStrain', 'Strain fitting of 2D data in GSAS-II.htm',
2299       'Strain fitting of 2D data'],
2300       
2301    ['SAimages', 'SAimages', 'Small Angle Image Processing.htm',
2302       'Image Processing of small angle x-ray data'],
2303    ['SAfit', 'SAfit', 'Fitting Small Angle Scattering Data.htm',
2304       'Fitting small angle x-ray data (alumina powder)'],
2305    ['SAsize', 'SAsize', 'Small Angle Size Distribution.htm',
2306       'Small angle x-ray data size distribution (alumina powder)'],
2307    ['SAseqref', 'SAseqref', 'Sequential Refinement of Small Angle Scattering Data.htm',
2308       'Sequential refinement with small angle scattering data'],
2309   
2310    #['TOF Sequential Single Peak Fit', 'TOF Sequential Single Peak Fit', '', ''],
2311    #['TOF Single Crystal Refinement', 'TOF Single Crystal Refinement', '', ''],
2312    )
2313if GSASIIpath.GetConfigValue('Tutorial_location'):
2314    tutorialPath = GSASIIpath.GetConfigValue('Tutorial_location')
2315else:
2316    # pick a default directory in a logical place
2317    if sys.platform.lower().startswith('win') and os.path.exists(os.path.abspath(os.path.expanduser('~/My Documents'))):
2318        tutorialPath = os.path.abspath(os.path.expanduser('~/My Documents/G2tutorials'))
2319    else:
2320        tutorialPath = os.path.abspath(os.path.expanduser('~/G2tutorials'))
2321
2322class OpenTutorial(wx.Dialog):
2323    '''Open a tutorial, optionally copying it to the local disk. Always copy
2324    the data files locally.
2325
2326    For now tutorials will always be copied into the source code tree, but it
2327    might be better to have an option to copy them somewhere else, for people
2328    who don't have write access to the GSAS-II source code location.
2329    '''
2330    # TODO: set default input-file open location to the download location
2331    def __init__(self,parent=None):
2332        style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER
2333        wx.Dialog.__init__(self, parent, wx.ID_ANY, 'Open Tutorial', style=style)
2334        self.frame = parent
2335        pnl = wx.Panel(self)
2336        sizer = wx.BoxSizer(wx.VERTICAL)
2337        sizer1 = wx.BoxSizer(wx.HORIZONTAL)       
2338        label = wx.StaticText(
2339            pnl,  wx.ID_ANY,
2340            'Select the tutorial to be run and the mode of access'
2341            )
2342        msg = '''To save download time for GSAS-II tutorials and their
2343        sample data files are being moved out of the standard
2344        distribution. This dialog allows users to load selected
2345        tutorials to their computer.
2346
2347        Tutorials can be viewed over the internet or downloaded
2348        to this computer. The sample data can be downloaded or not,
2349        (but it is not possible to run the tutorial without the
2350        data). If no web access is available, tutorials that were
2351        previously downloaded can be viewed.
2352
2353        By default, files are downloaded into the location used
2354        for the GSAS-II distribution, but this may not be possible
2355        if the software is installed by a administrator. The
2356        download location can be changed using the "Set data
2357        location" or the "Tutorial_location" configuration option
2358        (see config_example.py).
2359        '''
2360        hlp = HelpButton(pnl,msg)
2361        sizer1.Add((-1,-1),1, wx.EXPAND, 0)
2362        sizer1.Add(label, 0, wx.ALIGN_CENTRE|wx.ALL, 0)
2363        sizer1.Add((-1,-1),1, wx.EXPAND, 0)
2364        sizer1.Add(hlp,0,wx.ALIGN_RIGHT|wx.ALL)
2365        sizer.Add(sizer1,0,wx.EXPAND|wx.ALL,0)
2366        sizer.Add((10,10))
2367        #======================================================================
2368        # # This is needed only until we get all the tutorials items moved
2369        # btn = wx.Button(pnl, wx.ID_ANY, "Open older tutorials")
2370        # btn.Bind(wx.EVT_BUTTON, self.OpenOld)
2371        # sizer.Add(btn,0,wx.ALIGN_CENTRE|wx.ALL)
2372        #======================================================================
2373        self.BrowseMode = 1
2374        choices = [
2375            'make local copy of tutorial and data, then open',
2376            'run from web (copy data locally)',
2377            'browse on web (data not loaded)', 
2378            'open from local tutorial copy',
2379        ]
2380        self.mode = wx.RadioBox(pnl,wx.ID_ANY,'access mode:',
2381                                wx.DefaultPosition, wx.DefaultSize,
2382                                choices, 1, wx.RA_SPECIFY_COLS)
2383        self.mode.SetSelection(self.BrowseMode)
2384        self.mode.Bind(wx.EVT_RADIOBOX, self.OnModeSelect)
2385        sizer.Add(self.mode,0,WACV)
2386        sizer.Add((10,10))
2387        label = wx.StaticText(pnl,  wx.ID_ANY,'Click on tutorial to be opened:')
2388        sizer.Add(label, 0, wx.ALIGN_LEFT|wx.ALL, 2)
2389        self.listbox = wx.ListBox(pnl, wx.ID_ANY, size=(450, 100), style=wx.LB_SINGLE)
2390        self.listbox.Bind(wx.EVT_LISTBOX, self.OnTutorialSelected)
2391        sizer.Add(self.listbox,1,WACV|wx.EXPAND|wx.ALL,1)
2392        sizer.Add((10,10))
2393        sizer1 = wx.BoxSizer(wx.HORIZONTAL)
2394        btn = wx.Button(pnl, wx.ID_ANY, "Set download location")
2395        btn.Bind(wx.EVT_BUTTON, self.SelectDownloadLoc)
2396        sizer1.Add(btn,0,WACV)
2397        self.dataLoc = wx.StaticText(pnl, wx.ID_ANY,tutorialPath)
2398        sizer1.Add(self.dataLoc,0,WACV)
2399        sizer.Add(sizer1)
2400        label = wx.StaticText(
2401            pnl,  wx.ID_ANY,
2402            'Tutorials and Exercise files will be downloaded to:'
2403            )
2404        sizer.Add(label, 0, wx.ALIGN_LEFT|wx.ALL, 1)
2405        self.TutorialLabel = wx.StaticText(pnl,wx.ID_ANY,'')
2406        sizer.Add(self.TutorialLabel, 0, wx.ALIGN_LEFT|wx.EXPAND, 5)
2407        self.ExerciseLabel = wx.StaticText(pnl,wx.ID_ANY,'')
2408        sizer.Add(self.ExerciseLabel, 0, wx.ALIGN_LEFT|wx.EXPAND, 5)
2409        self.ShowTutorialPath()
2410        self.OnModeSelect(None)
2411       
2412        btnsizer = wx.StdDialogButtonSizer()
2413        btn = wx.Button(pnl, wx.ID_CANCEL)
2414        btnsizer.AddButton(btn)
2415        btnsizer.Realize()
2416        sizer.Add(btnsizer, 0, wx.ALIGN_CENTER|wx.ALL, 5)
2417        pnl.SetSizer(sizer)
2418        sizer.Fit(self)
2419        self.topsizer=sizer
2420        self.CenterOnParent()
2421    # def OpenOld(self,event):
2422    #     '''Open old tutorials. This is needed only until we get all the tutorials items moved
2423    #     '''
2424    #     self.EndModal(wx.ID_OK)
2425    #     ShowHelp('Tutorials',self.frame)
2426    def OnModeSelect(self,event):
2427        '''Respond when the mode is changed
2428        '''
2429        self.BrowseMode = self.mode.GetSelection()
2430        if self.BrowseMode == 3:
2431            import glob
2432            filelist = glob.glob(os.path.join(tutorialPath,'help','*','*.htm'))
2433            taillist = [os.path.split(f)[1] for f in filelist]
2434            itemlist = [tut[-1] for tut in tutorialCatalog if tut[2] in taillist]
2435        else:
2436            itemlist = [tut[-1] for tut in tutorialCatalog if tut[-1]]
2437        self.listbox.Clear()
2438        self.listbox.AppendItems(itemlist)
2439    def OnTutorialSelected(self,event):
2440        '''Respond when a tutorial is selected. Load tutorials and data locally,
2441        as needed and then display the page
2442        '''
2443        for tutdir,exedir,htmlname,title in tutorialCatalog:
2444            if title == event.GetString(): break
2445        else:
2446            raise Exception("Match to file not found")
2447        if self.BrowseMode == 0 or self.BrowseMode == 1:
2448            try: 
2449                self.ValidateTutorialDir(tutorialPath,G2BaseURL)
2450            except:
2451                G2MessageBox(self.frame,
2452            '''The selected directory is not valid.
2453           
2454            You must use a directory that you have write access
2455            to. You can reuse a directory previously used for
2456            downloads, but the help and Tutorials subdirectories
2457             must be created by this routine.
2458            ''')
2459                return
2460        #self.dataLoc.SetLabel(tutorialPath)
2461        self.EndModal(wx.ID_OK)
2462        wx.BeginBusyCursor()
2463        if self.BrowseMode == 0:
2464            # xfer data & web page locally, then open web page
2465            self.LoadTutorial(tutdir,tutorialPath,G2BaseURL)
2466            self.LoadExercise(exedir,tutorialPath,G2BaseURL)
2467            URL = os.path.join(tutorialPath,'help',tutdir,htmlname)
2468            self.frame.ImportDir = os.path.join(tutorialPath,'Exercises',exedir)
2469            ShowWebPage(URL,self.frame)
2470        elif self.BrowseMode == 1:
2471            # xfer data locally, open web page remotely
2472            self.LoadExercise(exedir,tutorialPath,G2BaseURL)
2473            URL = os.path.join(G2BaseURL,'Tutorials',tutdir,htmlname)
2474            self.frame.ImportDir = os.path.join(tutorialPath,'Exercises',exedir)
2475            ShowWebPage(URL,self.frame)
2476        elif self.BrowseMode == 2:
2477            # open web page remotely, don't worry about data
2478            URL = os.path.join(G2BaseURL,'Tutorials',tutdir,htmlname)
2479            ShowWebPage(URL,self.frame)
2480        elif self.BrowseMode == 3:
2481            # open web page that has already been transferred
2482            URL = os.path.join(tutorialPath,'help',tutdir,htmlname)
2483            self.frame.ImportDir = os.path.join(tutorialPath,'Exercises',exedir)
2484            ShowWebPage(URL,self.frame)
2485        else:
2486            wx.EndBusyCursor()
2487            raise Exception("How did this happen!")
2488        wx.EndBusyCursor()
2489    def ShowTutorialPath(self):
2490        'Show the help and exercise directory names'
2491        self.TutorialLabel.SetLabel('\t'+
2492                                    os.path.join(tutorialPath,"help") +
2493                                    ' (tutorials)')
2494        self.ExerciseLabel.SetLabel('\t'+
2495                                    os.path.join(tutorialPath,"Exercises") +
2496                                    ' (exercises)')
2497    def ValidateTutorialDir(self,fullpath=tutorialPath,baseURL=G2BaseURL):
2498        '''Load help to new directory or make sure existing directory looks correctly set up
2499        throws an exception if there is a problem.
2500        '''
2501        wx.BeginBusyCursor()
2502        wx.Yield()
2503        if os.path.exists(fullpath):
2504            if os.path.exists(os.path.join(fullpath,"help")):
2505                if not GSASIIpath.svnGetRev(os.path.join(fullpath,"help")):
2506                    print("Problem with "+fullpath+" dir help exists but is not in SVN")
2507                    wx.EndBusyCursor()
2508                    raise Exception
2509            if os.path.exists(os.path.join(fullpath,"Exercises")):
2510                if not GSASIIpath.svnGetRev(os.path.join(fullpath,"Exercises")):
2511                    print("Problem with "+fullpath+" dir Exercises exists but is not in SVN")
2512                    wx.EndBusyCursor()
2513                    raise Exception
2514            if (os.path.exists(os.path.join(fullpath,"help")) and
2515                    os.path.exists(os.path.join(fullpath,"Exercises"))):
2516                if self.BrowseMode != 3:
2517                    print('Checking for directory updates')
2518                    GSASIIpath.svnUpdateDir(os.path.join(fullpath,"help"))
2519                    GSASIIpath.svnUpdateDir(os.path.join(fullpath,"Exercises"))
2520                wx.EndBusyCursor()
2521                return True # both good
2522            elif (os.path.exists(os.path.join(fullpath,"help")) or
2523                    os.path.exists(os.path.join(fullpath,"Exercises"))):
2524                print("Problem: dir "+fullpath+" exists has either help or Exercises, not both")
2525                wx.EndBusyCursor()
2526                raise Exception
2527        if not GSASIIpath.svnInstallDir(baseURL+"/MT",fullpath):
2528            wx.EndBusyCursor()
2529            print("Problem transferring empty directory from web")
2530            raise Exception
2531        wx.EndBusyCursor()
2532        return True
2533
2534    def LoadTutorial(self,tutorialname,fullpath=tutorialPath,baseURL=G2BaseURL):
2535        'Load a Tutorial to the selected location'
2536        if GSASIIpath.svnSwitchDir("help",tutorialname,baseURL+"/Tutorials",fullpath):
2537            return True
2538        print("Problem transferring Tutorial from web")
2539        raise Exception
2540       
2541    def LoadExercise(self,tutorialname,fullpath=tutorialPath,baseURL=G2BaseURL):
2542        'Load Exercise file(s) for a Tutorial to the selected location'
2543        if GSASIIpath.svnSwitchDir("Exercises",tutorialname,baseURL+"/Exercises",fullpath):
2544            return True
2545        print ("Problem transferring Exercise from web")
2546        raise Exception
2547       
2548    def SelectDownloadLoc(self,event):
2549        '''Select a download location,
2550        Cancel resets to the default
2551        '''
2552        global tutorialPath
2553        dlg = wx.DirDialog(self, "Choose a directory for downloads:",
2554                           defaultPath=tutorialPath)#,style=wx.DD_DEFAULT_STYLE)
2555                           #)
2556        try:
2557            if dlg.ShowModal() != wx.ID_OK:
2558                return
2559            pth = dlg.GetPath()
2560        finally:
2561            dlg.Destroy()
2562
2563        if not os.path.exists(pth):
2564            try:
2565                os.makedirs(pth)
2566            except OSError:
2567                msg = 'The selected directory is not valid.\n\t'
2568                msg += pth
2569                msg += '\n\nAn attempt to create the directory failed'
2570                G2MessageBox(self.frame,msg)
2571                return
2572        try:
2573            self.ValidateTutorialDir(pth,G2BaseURL)
2574            tutorialPath = pth
2575        except:
2576            G2MessageBox(self.frame,
2577            '''Error downloading to the selected directory
2578
2579            Are you connected to the internet? If not, you can
2580            only view previously downloaded tutorials (select
2581            "open from local...")
2582           
2583            You must use a directory that you have write access
2584            to. You can reuse a directory previously used for
2585            downloads, but the help and Tutorials subdirectories
2586            must have been created by this routine.
2587            ''')
2588        self.dataLoc.SetLabel(tutorialPath)
2589        self.ShowTutorialPath()
2590        self.OnModeSelect(None)
2591   
2592if __name__ == '__main__':
2593    app = wx.PySimpleApp()
2594    GSASIIpath.InvokeDebugOpts()
2595    frm = wx.Frame(None) # create a frame
2596    frm.Show(True)
2597    dlg = OpenTutorial(frm)
2598    if dlg.ShowModal() == wx.ID_OK:
2599        print "OK"
2600    else:
2601        print "Cancel"
2602    dlg.Destroy()
2603    import sys
2604    sys.exit()
2605    #======================================================================
2606    # test ScrolledMultiEditor
2607    #======================================================================
2608    # Data1 = {
2609    #      'Order':1,
2610    #      'omega':'string',
2611    #      'chi':2.0,
2612    #      'phi':'',
2613    #      }
2614    # elemlst = sorted(Data1.keys())
2615    # prelbl = sorted(Data1.keys())
2616    # dictlst = len(elemlst)*[Data1,]
2617    #Data2 = [True,False,False,True]
2618    #Checkdictlst = len(Data2)*[Data2,]
2619    #Checkelemlst = range(len(Checkdictlst))
2620    # print 'before',Data1,'\n',Data2
2621    # dlg = ScrolledMultiEditor(
2622    #     frm,dictlst,elemlst,prelbl,
2623    #     checkdictlst=Checkdictlst,checkelemlst=Checkelemlst,
2624    #     checklabel="Refine?",
2625    #     header="test")
2626    # if dlg.ShowModal() == wx.ID_OK:
2627    #     print "OK"
2628    # else:
2629    #     print "Cancel"
2630    # print 'after',Data1,'\n',Data2
2631    # dlg.Destroy()
2632    Data3 = {
2633         'Order':1.0,
2634         'omega':1.1,
2635         'chi':2.0,
2636         'phi':2.3,
2637         'Order1':1.0,
2638         'omega1':1.1,
2639         'chi1':2.0,
2640         'phi1':2.3,
2641         'Order2':1.0,
2642         'omega2':1.1,
2643         'chi2':2.0,
2644         'phi2':2.3,
2645         }
2646    elemlst = sorted(Data3.keys())
2647    dictlst = len(elemlst)*[Data3,]
2648    prelbl = elemlst[:]
2649    prelbl[0]="this is a much longer label to stretch things out"
2650    Data2 = len(elemlst)*[False,]
2651    Data2[1] = Data2[3] = True
2652    Checkdictlst = len(elemlst)*[Data2,]
2653    Checkelemlst = range(len(Checkdictlst))
2654    #print 'before',Data3,'\n',Data2
2655    #print dictlst,"\n",elemlst
2656    #print Checkdictlst,"\n",Checkelemlst
2657    dlg = ScrolledMultiEditor(
2658        frm,dictlst,elemlst,prelbl,
2659        checkdictlst=Checkdictlst,checkelemlst=Checkelemlst,
2660        checklabel="Refine?",
2661        header="test",CopyButton=True)
2662    if dlg.ShowModal() == wx.ID_OK:
2663        print "OK"
2664    else:
2665        print "Cancel"
2666    #print 'after',Data3,'\n',Data2
2667
2668    # Data2 = list(range(100))
2669    # elemlst += range(2,6)
2670    # postlbl += range(2,6)
2671    # dictlst += len(range(2,6))*[Data2,]
2672
2673    # prelbl = range(len(elemlst))
2674    # postlbl[1] = "a very long label for the 2nd item to force a horiz. scrollbar"
2675    # header="""This is a longer\nmultiline and perhaps silly header"""
2676    # dlg = ScrolledMultiEditor(frm,dictlst,elemlst,prelbl,postlbl,
2677    #                           header=header,CopyButton=True)
2678    # print Data1
2679    # if dlg.ShowModal() == wx.ID_OK:
2680    #     for d,k in zip(dictlst,elemlst):
2681    #         print k,d[k]
2682    # dlg.Destroy()
2683    # if CallScrolledMultiEditor(frm,dictlst,elemlst,prelbl,postlbl,
2684    #                            header=header):
2685    #     for d,k in zip(dictlst,elemlst):
2686    #         print k,d[k]
2687
2688    #======================================================================
2689    # test G2MultiChoiceDialog
2690    #======================================================================
2691    # choices = []
2692    # for i in range(21):
2693    #     choices.append("option_"+str(i))
2694    # dlg = G2MultiChoiceDialog(frm, 'Sequential refinement',
2695    #                           'Select dataset to include',
2696    #                           choices)
2697    # sel = range(2,11,2)
2698    # dlg.SetSelections(sel)
2699    # dlg.SetSelections((1,5))
2700    # if dlg.ShowModal() == wx.ID_OK:
2701    #     for sel in dlg.GetSelections():
2702    #         print sel,choices[sel]
2703   
2704    #======================================================================
2705    # test wx.MultiChoiceDialog
2706    #======================================================================
2707    # dlg = wx.MultiChoiceDialog(frm, 'Sequential refinement',
2708    #                           'Select dataset to include',
2709    #                           choices)
2710    # sel = range(2,11,2)
2711    # dlg.SetSelections(sel)
2712    # dlg.SetSelections((1,5))
2713    # if dlg.ShowModal() == wx.ID_OK:
2714    #     for sel in dlg.GetSelections():
2715    #         print sel,choices[sel]
2716
2717    pnl = wx.Panel(frm)
2718    siz = wx.BoxSizer(wx.VERTICAL)
2719
2720    td = {'Goni':200.,'a':1.,'calc':1./3.,'string':'s'}
2721    for key in sorted(td):
2722        txt = ValidatedTxtCtrl(pnl,td,key)
2723        siz.Add(txt)
2724    pnl.SetSizer(siz)
2725    siz.Fit(frm)
2726    app.MainLoop()
2727    print td
Note: See TracBrowser for help on using the repository browser.