source: trunk/GSASIIctrls.py @ 1945

Last change on this file since 1945 was 1945, checked in by vondreele, 8 years ago

fix another new histo in new phase error
some cleanup in texture calculations - unneeded list generation

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