source: trunk/GSASIIctrls.py @ 1946

Last change on this file since 1946 was 1946, checked in by vondreele, 7 years ago
  • 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(wx.StaticText(self,wx.ID_ANY,title,size=(-1,35)),
1268            1,wx.ALL|wx.EXPAND|WACV,1)
1269        if filterBox:
1270            self.timer = wx.Timer()
1271            self.timer.Bind(wx.EVT_TIMER,self.Filter)
1272            topSizer.Add(wx.StaticText(self,wx.ID_ANY,'Name \nFilter: '),0,wx.ALL|WACV,1)
1273            self.filterBox = wx.TextCtrl(self, wx.ID_ANY, size=(80,-1),style=wx.TE_PROCESS_ENTER)
1274            self.filterBox.Bind(wx.EVT_TEXT,self.onChar)
1275            self.filterBox.Bind(wx.EVT_TEXT_ENTER,self.Filter)
1276            topSizer.Add(self.filterBox,0,wx.ALL|WACV,0)
1277        Sizer.Add(topSizer,0,wx.ALL|wx.EXPAND,8)
1278        self.settingRange = False
1279        self.rangeFirst = None
1280        self.clb = wx.CheckListBox(self, wx.ID_ANY, (30,30), wx.DefaultSize, ChoiceList)
1281        self.clb.Bind(wx.EVT_CHECKLISTBOX,self.OnCheck)
1282        if monoFont:
1283            font1 = wx.Font(self.clb.GetFont().GetPointSize(),
1284                            wx.MODERN, wx.NORMAL, wx.NORMAL, False)
1285            self.clb.SetFont(font1)
1286        Sizer.Add(self.clb,1,wx.LEFT|wx.RIGHT|wx.EXPAND,10)
1287        Sizer.Add((-1,10))
1288        # set/toggle buttons
1289        if toggle:
1290            tSizer = wx.FlexGridSizer(cols=2,hgap=5,vgap=5)
1291            setBut = wx.Button(self,wx.ID_ANY,'Set All')
1292            setBut.Bind(wx.EVT_BUTTON,self._SetAll)
1293            tSizer.Add(setBut)
1294            togBut = wx.Button(self,wx.ID_ANY,'Toggle All')
1295            togBut.Bind(wx.EVT_BUTTON,self._ToggleAll)
1296            tSizer.Add(togBut)
1297            self.rangeBut = wx.ToggleButton(self,wx.ID_ANY,'Set Range')
1298            self.rangeBut.Bind(wx.EVT_TOGGLEBUTTON,self.SetRange)
1299            tSizer.Add(self.rangeBut)           
1300            self.rangeCapt = wx.StaticText(self,wx.ID_ANY,'')
1301            tSizer.Add(self.rangeCapt)
1302            Sizer.Add(tSizer,0,wx.LEFT,12)
1303        # OK/Cancel buttons
1304        btnsizer = wx.StdDialogButtonSizer()
1305        if useOK:
1306            self.OKbtn = wx.Button(self, wx.ID_OK)
1307            self.OKbtn.SetDefault()
1308            btnsizer.AddButton(self.OKbtn)
1309        if useCANCEL:
1310            btn = wx.Button(self, wx.ID_CANCEL)
1311            btnsizer.AddButton(btn)
1312        btnsizer.Realize()
1313        Sizer.Add((-1,5))
1314        Sizer.Add(btnsizer,0,wx.ALIGN_RIGHT,50)
1315        Sizer.Add((-1,20))
1316        # OK done, let's get outa here
1317        self.SetSizer(Sizer)
1318        self.CenterOnParent()
1319
1320    def SetRange(self,event):
1321        '''Respond to a press of the Set Range button. Set the range flag and
1322        the caption next to the button
1323        '''
1324        self.settingRange = self.rangeBut.GetValue()
1325        if self.settingRange:
1326            self.rangeCapt.SetLabel('Select range start')
1327        else:
1328            self.rangeCapt.SetLabel('')           
1329        self.rangeFirst = None
1330       
1331    def GetSelections(self):
1332        'Returns a list of the indices for the selected choices'
1333        # update self.Selections with settings for displayed items
1334        for i in range(len(self.filterlist)):
1335            self.Selections[self.filterlist[i]] = self.clb.IsChecked(i)
1336        # return all selections, shown or hidden
1337        return [i for i in range(len(self.Selections)) if self.Selections[i]]
1338       
1339    def SetSelections(self,selList):
1340        '''Sets the selection indices in selList as selected. Resets any previous
1341        selections for compatibility with wx.MultiChoiceDialog. Note that
1342        the state for only the filtered items is shown.
1343
1344        :param list selList: indices of items to be selected. These indices
1345          are referenced to the order in self.ChoiceList
1346        '''
1347        self.Selections = len(self.ChoiceList) * [False,] # reset selections
1348        for sel in selList:
1349            self.Selections[sel] = True
1350        self._ShowSelections()
1351
1352    def _ShowSelections(self):
1353        'Show the selection state for displayed items'
1354        self.clb.SetChecked(
1355            [i for i in range(len(self.filterlist)) if self.Selections[self.filterlist[i]]]
1356            ) # Note anything previously checked will be cleared.
1357           
1358    def _SetAll(self,event):
1359        'Set all viewed choices on'
1360        self.clb.SetChecked(range(len(self.filterlist)))
1361       
1362    def _ToggleAll(self,event):
1363        'flip the state of all viewed choices'
1364        for i in range(len(self.filterlist)):
1365            self.clb.Check(i,not self.clb.IsChecked(i))
1366           
1367    def onChar(self,event):
1368        'Respond to keyboard events in the Filter box'
1369        self.OKbtn.Enable(False)
1370        if self.timer.IsRunning():
1371            self.timer.Stop()
1372        self.timer.Start(1000,oneShot=True)
1373        event.Skip()
1374       
1375    def OnCheck(self,event):
1376        '''for CheckListBox events; if Set Range is in use, this sets/clears all
1377        entries in range between start and end according to the value in start.
1378        Repeated clicks on the start change the checkbox state, but do not trigger
1379        the range copy.
1380        The caption next to the button is updated on the first button press.
1381        '''
1382        if self.settingRange:
1383            id = event.GetInt()
1384            if self.rangeFirst is None:
1385                name = self.clb.GetString(id)
1386                self.rangeCapt.SetLabel(name+' to...')
1387                self.rangeFirst = id
1388            elif self.rangeFirst == id:
1389                pass
1390            else:
1391                for i in range(min(self.rangeFirst,id), max(self.rangeFirst,id)+1):
1392                    self.clb.Check(i,self.clb.IsChecked(self.rangeFirst))
1393                self.rangeBut.SetValue(False)
1394                self.rangeCapt.SetLabel('')
1395            return
1396       
1397    def Filter(self,event):
1398        '''Read text from filter control and select entries that match. Called by
1399        Timer after a delay with no input or if Enter is pressed.
1400        '''
1401        if self.timer.IsRunning():
1402            self.timer.Stop()
1403        self.GetSelections() # record current selections
1404        txt = self.filterBox.GetValue()
1405        self.clb.Clear()
1406       
1407        self.Update()
1408        self.filterlist = []
1409        if txt:
1410            txt = txt.lower()
1411            ChoiceList = []
1412            for i,item in enumerate(self.ChoiceList):
1413                if item.lower().find(txt) != -1:
1414                    ChoiceList.append(item)
1415                    self.filterlist.append(i)
1416        else:
1417            self.filterlist = range(len(self.ChoiceList))
1418            ChoiceList = self.ChoiceList
1419        self.clb.AppendItems(ChoiceList)
1420        self._ShowSelections()
1421        self.OKbtn.Enable(True)
1422
1423def SelectEdit1Var(G2frame,array,labelLst,elemKeysLst,dspLst,refFlgElem):
1424    '''Select a variable from a list, then edit it and select histograms
1425    to copy it to.
1426
1427    :param wx.Frame G2frame: main GSAS-II frame
1428    :param dict array: the array (dict or list) where values to be edited are kept
1429    :param list labelLst: labels for each data item
1430    :param list elemKeysLst: a list of lists of keys needed to be applied (see below)
1431      to obtain the value of each parameter
1432    :param list dspLst: list list of digits to be displayed (10,4) is 10 digits
1433      with 4 decimal places. Can be None.
1434    :param list refFlgElem: a list of lists of keys needed to be applied (see below)
1435      to obtain the refine flag for each parameter or None if the parameter
1436      does not have refine flag.
1437
1438    Example::
1439      array = data
1440      labelLst = ['v1','v2']
1441      elemKeysLst = [['v1'], ['v2',0]]
1442      refFlgElem = [None, ['v2',1]]
1443
1444     * The value for v1 will be in data['v1'] and this cannot be refined while,
1445     * The value for v2 will be in data['v2'][0] and its refinement flag is data['v2'][1]
1446    '''
1447    def unkey(dct,keylist):
1448        '''dive into a nested set of dicts/lists applying keys in keylist
1449        consecutively
1450        '''
1451        d = dct
1452        for k in keylist:
1453            d = d[k]
1454        return d
1455
1456    def OnChoice(event):
1457        'Respond when a parameter is selected in the Choice box'
1458        valSizer.DeleteWindows()
1459        lbl = event.GetString()
1460        copyopts['currentsel'] = lbl
1461        i = labelLst.index(lbl)
1462        OKbtn.Enable(True)
1463        ch.SetLabel(lbl)
1464        args = {}
1465        if dspLst[i]:
1466            args = {'nDig':dspLst[i]}
1467        Val = ValidatedTxtCtrl(
1468            dlg,
1469            unkey(array,elemKeysLst[i][:-1]),
1470            elemKeysLst[i][-1],
1471            **args)
1472        copyopts['startvalue'] = unkey(array,elemKeysLst[i])
1473        #unkey(array,elemKeysLst[i][:-1])[elemKeysLst[i][-1]] =
1474        valSizer.Add(Val,0,wx.LEFT,5)
1475        dlg.SendSizeEvent()
1476       
1477    # SelectEdit1Var execution begins here
1478    saveArray = copy.deepcopy(array) # keep original values
1479    TreeItemType = G2frame.PatternTree.GetItemText(G2frame.PickId)
1480    copyopts = {'InTable':False,"startvalue":None,'currentsel':None}       
1481    hst = G2frame.PatternTree.GetItemText(G2frame.PatternId)
1482    histList = G2pdG.GetHistsLikeSelected(G2frame)
1483    if not histList:
1484        G2frame.ErrorDialog('No match','No histograms match '+hst,G2frame.dataFrame)
1485        return
1486    dlg = wx.Dialog(G2frame.dataDisplay,wx.ID_ANY,'Set a parameter value',
1487        style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)
1488    mainSizer = wx.BoxSizer(wx.VERTICAL)
1489    mainSizer.Add((5,5))
1490    subSizer = wx.BoxSizer(wx.HORIZONTAL)
1491    subSizer.Add((-1,-1),1,wx.EXPAND)
1492    subSizer.Add(wx.StaticText(dlg,wx.ID_ANY,'Select a parameter and set a new value'))
1493    subSizer.Add((-1,-1),1,wx.EXPAND)
1494    mainSizer.Add(subSizer,0,wx.EXPAND,0)
1495    mainSizer.Add((0,10))
1496
1497    subSizer = wx.FlexGridSizer(0,2,5,0)
1498    subSizer.Add(wx.StaticText(dlg,wx.ID_ANY,'Parameter: '))
1499    ch = wx.Choice(dlg, wx.ID_ANY, choices = sorted(labelLst))
1500    ch.SetSelection(-1)
1501    ch.Bind(wx.EVT_CHOICE, OnChoice)
1502    subSizer.Add(ch)
1503    subSizer.Add(wx.StaticText(dlg,wx.ID_ANY,'Value: '))
1504    valSizer = wx.BoxSizer(wx.HORIZONTAL)
1505    subSizer.Add(valSizer)
1506    mainSizer.Add(subSizer)
1507
1508    mainSizer.Add((-1,20))
1509    subSizer = wx.BoxSizer(wx.HORIZONTAL)
1510    subSizer.Add(G2CheckBox(dlg, 'Edit in table ', copyopts, 'InTable'))
1511    mainSizer.Add(subSizer)
1512
1513    btnsizer = wx.StdDialogButtonSizer()
1514    OKbtn = wx.Button(dlg, wx.ID_OK,'Continue')
1515    OKbtn.Enable(False)
1516    OKbtn.SetDefault()
1517    OKbtn.Bind(wx.EVT_BUTTON,lambda event: dlg.EndModal(wx.ID_OK))
1518    btnsizer.AddButton(OKbtn)
1519    btn = wx.Button(dlg, wx.ID_CANCEL)
1520    btnsizer.AddButton(btn)
1521    btnsizer.Realize()
1522    mainSizer.Add((-1,5),1,wx.EXPAND,1)
1523    mainSizer.Add(btnsizer,0,wx.ALIGN_CENTER,0)
1524    mainSizer.Add((-1,10))
1525
1526    dlg.SetSizer(mainSizer)
1527    dlg.CenterOnParent()
1528    if dlg.ShowModal() != wx.ID_OK:
1529        array.update(saveArray)
1530        dlg.Destroy()
1531        return
1532    dlg.Destroy()
1533
1534    copyList = []
1535    lbl = copyopts['currentsel']
1536    dlg = G2MultiChoiceDialog(G2frame.dataFrame,'Copy parameter '+lbl+' from\n'+hst,
1537        'Copy parameters', histList)
1538    dlg.CenterOnParent()
1539    try:
1540        if dlg.ShowModal() == wx.ID_OK:
1541            for i in dlg.GetSelections(): 
1542                copyList.append(histList[i])
1543        else:
1544            # reset the parameter since cancel was pressed
1545            array.update(saveArray)
1546            return
1547    finally:
1548        dlg.Destroy()
1549
1550    prelbl = [hst]
1551    i = labelLst.index(lbl)
1552    keyLst = elemKeysLst[i]
1553    refkeys = refFlgElem[i]
1554    dictlst = [unkey(array,keyLst[:-1])]
1555    if refkeys is not None:
1556        refdictlst = [unkey(array,refkeys[:-1])]
1557    else:
1558        refdictlst = None
1559    Id = GetPatternTreeItemId(G2frame,G2frame.root,hst)
1560    hstData = G2frame.PatternTree.GetItemPyData(GetPatternTreeItemId(G2frame,Id,'Instrument Parameters'))[0]
1561    for h in copyList:
1562        Id = GetPatternTreeItemId(G2frame,G2frame.root,h)
1563        instData = G2frame.PatternTree.GetItemPyData(GetPatternTreeItemId(G2frame,Id,'Instrument Parameters'))[0]
1564        if len(hstData) != len(instData) or hstData['Type'][0] != instData['Type'][0]:  #don't mix data types or lam & lam1/lam2 parms!
1565            print h+' not copied - instrument parameters not commensurate'
1566            continue
1567        hData = G2frame.PatternTree.GetItemPyData(GetPatternTreeItemId(G2frame,Id,TreeItemType))
1568        if TreeItemType == 'Instrument Parameters':
1569            hData = hData[0]
1570        #copy the value if it is changed or we will not edit in a table
1571        valNow = unkey(array,keyLst)
1572        if copyopts['startvalue'] != valNow or not copyopts['InTable']:
1573            unkey(hData,keyLst[:-1])[keyLst[-1]] = valNow
1574        prelbl += [h]
1575        dictlst += [unkey(hData,keyLst[:-1])]
1576        if refdictlst is not None:
1577            refdictlst += [unkey(hData,refkeys[:-1])]
1578    if refdictlst is None:
1579        args = {}
1580    else:
1581        args = {'checkdictlst':refdictlst,
1582                'checkelemlst':len(dictlst)*[refkeys[-1]],
1583                'checklabel':'Refine?'}
1584    if copyopts['InTable']:
1585        dlg = ScrolledMultiEditor(
1586            G2frame.dataDisplay,dictlst,
1587            len(dictlst)*[keyLst[-1]],prelbl,
1588            header='Editing parameter '+lbl,
1589            CopyButton=True,**args)
1590        dlg.CenterOnParent()
1591        if dlg.ShowModal() != wx.ID_OK:
1592            array.update(saveArray)
1593        dlg.Destroy()
1594
1595################################################################        Single choice Dialog with filter options
1596class G2SingleChoiceDialog(wx.Dialog):
1597    '''A dialog similar to wx.SingleChoiceDialog except that a filter can be
1598    added.
1599
1600    :param wx.Frame ParentFrame: reference to parent frame
1601    :param str title: heading above list of choices
1602    :param str header: Title to place on window frame
1603    :param list ChoiceList: a list of choices where one will be selected
1604    :param bool monoFont: If False (default), use a variable-spaced font;
1605      if True use a equally-spaced font.
1606    :param bool filterBox: If True (default) an input widget is placed on
1607      the window and only entries matching the entered text are shown.
1608    :param kw: optional keyword parameters for the wx.Dialog may
1609      be included such as size [which defaults to `(320,310)`] and
1610      style (which defaults to ``wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER | wx.CENTRE | wx.OK | wx.CANCEL``);
1611      note that ``wx.OK`` and ``wx.CANCEL`` controls
1612      the presence of the eponymous buttons in the dialog.
1613    :returns: the name of the created dialog
1614    '''
1615    def __init__(self,parent, title, header, ChoiceList, 
1616                 monoFont=False, filterBox=True, **kw):
1617        # process keyword parameters, notably style
1618        options = {'size':(320,310), # default Frame keywords
1619                   'style':wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER|wx.CENTRE| wx.OK | wx.CANCEL,
1620                   }
1621        options.update(kw)
1622        self.ChoiceList = ChoiceList
1623        self.filterlist = range(len(self.ChoiceList))
1624        if options['style'] & wx.OK:
1625            useOK = True
1626            options['style'] ^= wx.OK
1627        else:
1628            useOK = False
1629        if options['style'] & wx.CANCEL:
1630            useCANCEL = True
1631            options['style'] ^= wx.CANCEL
1632        else:
1633            useCANCEL = False       
1634        # create the dialog frame
1635        wx.Dialog.__init__(self,parent,wx.ID_ANY,header,**options)
1636        # fill the dialog
1637        Sizer = wx.BoxSizer(wx.VERTICAL)
1638        topSizer = wx.BoxSizer(wx.HORIZONTAL)
1639        topSizer.Add(
1640            wx.StaticText(self,wx.ID_ANY,title,size=(-1,35)),
1641            1,wx.ALL|wx.EXPAND|WACV,1)
1642        if filterBox:
1643            self.timer = wx.Timer()
1644            self.timer.Bind(wx.EVT_TIMER,self.Filter)
1645            topSizer.Add(wx.StaticText(self,wx.ID_ANY,'Filter: '),0,wx.ALL,1)
1646            self.filterBox = wx.TextCtrl(self, wx.ID_ANY, size=(80,-1),
1647                                         style=wx.TE_PROCESS_ENTER)
1648            self.filterBox.Bind(wx.EVT_CHAR,self.onChar)
1649            self.filterBox.Bind(wx.EVT_TEXT_ENTER,self.Filter)
1650        topSizer.Add(self.filterBox,0,wx.ALL,0)
1651        Sizer.Add(topSizer,0,wx.ALL|wx.EXPAND,8)
1652        self.clb = wx.ListBox(self, wx.ID_ANY, (30,30), wx.DefaultSize, ChoiceList)
1653        self.clb.Bind(wx.EVT_LEFT_DCLICK,self.onDoubleClick)
1654        if monoFont:
1655            font1 = wx.Font(self.clb.GetFont().GetPointSize(),
1656                            wx.MODERN, wx.NORMAL, wx.NORMAL, False)
1657            self.clb.SetFont(font1)
1658        Sizer.Add(self.clb,1,wx.LEFT|wx.RIGHT|wx.EXPAND,10)
1659        Sizer.Add((-1,10))
1660        # OK/Cancel buttons
1661        btnsizer = wx.StdDialogButtonSizer()
1662        if useOK:
1663            self.OKbtn = wx.Button(self, wx.ID_OK)
1664            self.OKbtn.SetDefault()
1665            btnsizer.AddButton(self.OKbtn)
1666        if useCANCEL:
1667            btn = wx.Button(self, wx.ID_CANCEL)
1668            btnsizer.AddButton(btn)
1669        btnsizer.Realize()
1670        Sizer.Add((-1,5))
1671        Sizer.Add(btnsizer,0,wx.ALIGN_RIGHT,50)
1672        Sizer.Add((-1,20))
1673        # OK done, let's get outa here
1674        self.SetSizer(Sizer)
1675    def GetSelection(self):
1676        'Returns the index of the selected choice'
1677        i = self.clb.GetSelection()
1678        if i < 0 or i >= len(self.filterlist):
1679            return wx.NOT_FOUND
1680        return self.filterlist[i]
1681    def onChar(self,event):
1682        self.OKbtn.Enable(False)
1683        if self.timer.IsRunning():
1684            self.timer.Stop()
1685        self.timer.Start(1000,oneShot=True)
1686        event.Skip()
1687    def Filter(self,event):
1688        if self.timer.IsRunning():
1689            self.timer.Stop()
1690        txt = self.filterBox.GetValue()
1691        self.clb.Clear()
1692        self.Update()
1693        self.filterlist = []
1694        if txt:
1695            txt = txt.lower()
1696            ChoiceList = []
1697            for i,item in enumerate(self.ChoiceList):
1698                if item.lower().find(txt) != -1:
1699                    ChoiceList.append(item)
1700                    self.filterlist.append(i)
1701        else:
1702            self.filterlist = range(len(self.ChoiceList))
1703            ChoiceList = self.ChoiceList
1704        self.clb.AppendItems(ChoiceList)
1705        self.OKbtn.Enable(True)
1706    def onDoubleClick(self,event):
1707        self.EndModal(wx.ID_OK)
1708
1709################################################################################
1710def G2MessageBox(parent,msg,title='Error'):
1711    '''Simple code to display a error or warning message
1712    '''
1713    dlg = wx.MessageDialog(parent,StripIndents(msg), title, wx.OK)
1714    dlg.ShowModal()
1715    dlg.Destroy()
1716   
1717################################################################################
1718class PickTwoDialog(wx.Dialog):
1719    '''This does not seem to be in use
1720    '''
1721    def __init__(self,parent,title,prompt,names,choices):
1722        wx.Dialog.__init__(self,parent,-1,title, 
1723            pos=wx.DefaultPosition,style=wx.DEFAULT_DIALOG_STYLE)
1724        self.panel = wx.Panel(self)         #just a dummy - gets destroyed in Draw!
1725        self.prompt = prompt
1726        self.choices = choices
1727        self.names = names
1728        self.Draw()
1729
1730    def Draw(self):
1731        Indx = {}
1732       
1733        def OnSelection(event):
1734            Obj = event.GetEventObject()
1735            id = Indx[Obj.GetId()]
1736            self.choices[id] = Obj.GetValue().encode()  #to avoid Unicode versions
1737            self.Draw()
1738           
1739        self.panel.DestroyChildren()
1740        self.panel.Destroy()
1741        self.panel = wx.Panel(self)
1742        mainSizer = wx.BoxSizer(wx.VERTICAL)
1743        mainSizer.Add(wx.StaticText(self.panel,-1,self.prompt),0,wx.ALIGN_CENTER)
1744        for isel,name in enumerate(self.choices):
1745            lineSizer = wx.BoxSizer(wx.HORIZONTAL)
1746            lineSizer.Add(wx.StaticText(self.panel,-1,'Reference atom '+str(isel+1)),0,wx.ALIGN_CENTER)
1747            nameList = self.names[:]
1748            if isel:
1749                if self.choices[0] in nameList:
1750                    nameList.remove(self.choices[0])
1751            choice = wx.ComboBox(self.panel,-1,value=name,choices=nameList,
1752                style=wx.CB_READONLY|wx.CB_DROPDOWN)
1753            Indx[choice.GetId()] = isel
1754            choice.Bind(wx.EVT_COMBOBOX, OnSelection)
1755            lineSizer.Add(choice,0,WACV)
1756            mainSizer.Add(lineSizer)
1757        OkBtn = wx.Button(self.panel,-1,"Ok")
1758        OkBtn.Bind(wx.EVT_BUTTON, self.OnOk)
1759        CancelBtn = wx.Button(self.panel,-1,'Cancel')
1760        CancelBtn.Bind(wx.EVT_BUTTON, self.OnCancel)
1761        btnSizer = wx.BoxSizer(wx.HORIZONTAL)
1762        btnSizer.Add((20,20),1)
1763        btnSizer.Add(OkBtn)
1764        btnSizer.Add(CancelBtn)
1765        btnSizer.Add((20,20),1)
1766        mainSizer.Add(btnSizer,0,wx.EXPAND|wx.BOTTOM|wx.TOP, 10)
1767        self.panel.SetSizer(mainSizer)
1768        self.panel.Fit()
1769        self.Fit()
1770       
1771    def GetSelection(self):
1772        return self.choices
1773
1774    def OnOk(self,event):
1775        parent = self.GetParent()
1776        parent.Raise()
1777        self.EndModal(wx.ID_OK)             
1778       
1779    def OnCancel(self,event):
1780        parent = self.GetParent()
1781        parent.Raise()
1782        self.EndModal(wx.ID_CANCEL)
1783
1784################################################################################
1785class SingleFloatDialog(wx.Dialog):
1786    'Dialog to obtain a single float value from user'
1787    def __init__(self,parent,title,prompt,value,limits=[0.,1.],format='%.5g'):
1788        wx.Dialog.__init__(self,parent,-1,title, 
1789            pos=wx.DefaultPosition,style=wx.DEFAULT_DIALOG_STYLE)
1790        self.panel = wx.Panel(self)         #just a dummy - gets destroyed in Draw!
1791        self.limits = limits
1792        self.value = value
1793        self.prompt = prompt
1794        self.format = format
1795        self.Draw()
1796       
1797    def Draw(self):
1798       
1799        def OnValItem(event):
1800            try:
1801                val = float(valItem.GetValue())
1802                if val < self.limits[0] or val > self.limits[1]:
1803                    raise ValueError
1804            except ValueError:
1805                val = self.value
1806            self.value = val
1807            valItem.SetValue(self.format%(self.value))
1808           
1809        self.panel.Destroy()
1810        self.panel = wx.Panel(self)
1811        mainSizer = wx.BoxSizer(wx.VERTICAL)
1812        mainSizer.Add(wx.StaticText(self.panel,-1,self.prompt),0,wx.ALIGN_CENTER)
1813        valItem = wx.TextCtrl(self.panel,-1,value=self.format%(self.value),style=wx.TE_PROCESS_ENTER)
1814        mainSizer.Add(valItem,0,wx.ALIGN_CENTER)
1815        valItem.Bind(wx.EVT_TEXT_ENTER,OnValItem)
1816        valItem.Bind(wx.EVT_KILL_FOCUS,OnValItem)
1817        OkBtn = wx.Button(self.panel,-1,"Ok")
1818        OkBtn.Bind(wx.EVT_BUTTON, self.OnOk)
1819        CancelBtn = wx.Button(self.panel,-1,'Cancel')
1820        CancelBtn.Bind(wx.EVT_BUTTON, self.OnCancel)
1821        btnSizer = wx.BoxSizer(wx.HORIZONTAL)
1822        btnSizer.Add((20,20),1)
1823        btnSizer.Add(OkBtn)
1824        btnSizer.Add(CancelBtn)
1825        btnSizer.Add((20,20),1)
1826        mainSizer.Add(btnSizer,0,wx.EXPAND|wx.BOTTOM|wx.TOP, 10)
1827        self.panel.SetSizer(mainSizer)
1828        self.panel.Fit()
1829        self.Fit()
1830
1831    def GetValue(self):
1832        return self.value
1833       
1834    def OnOk(self,event):
1835        parent = self.GetParent()
1836        parent.Raise()
1837        self.EndModal(wx.ID_OK)             
1838       
1839    def OnCancel(self,event):
1840        parent = self.GetParent()
1841        parent.Raise()
1842        self.EndModal(wx.ID_CANCEL)
1843
1844################################################################################
1845class SingleStringDialog(wx.Dialog):
1846    '''Dialog to obtain a single string value from user
1847   
1848    :param wx.Frame parent: name of parent frame
1849    :param str title: title string for dialog
1850    :param str prompt: string to tell use what they are inputting
1851    :param str value: default input value, if any
1852    '''
1853    def __init__(self,parent,title,prompt,value='',size=(200,-1)):
1854        wx.Dialog.__init__(self,parent,wx.ID_ANY,title, 
1855                           pos=wx.DefaultPosition,
1856                           style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)
1857        self.value = value
1858        self.prompt = prompt
1859        self.CenterOnParent()
1860        self.panel = wx.Panel(self)
1861        mainSizer = wx.BoxSizer(wx.VERTICAL)
1862        mainSizer.Add(wx.StaticText(self.panel,-1,self.prompt),0,wx.ALIGN_CENTER)
1863        self.valItem = wx.TextCtrl(self.panel,-1,value=self.value,size=size)
1864        mainSizer.Add(self.valItem,0,wx.ALIGN_CENTER)
1865        btnsizer = wx.StdDialogButtonSizer()
1866        OKbtn = wx.Button(self.panel, wx.ID_OK)
1867        OKbtn.SetDefault()
1868        btnsizer.AddButton(OKbtn)
1869        btn = wx.Button(self.panel, wx.ID_CANCEL)
1870        btnsizer.AddButton(btn)
1871        btnsizer.Realize()
1872        mainSizer.Add(btnsizer,0,wx.ALIGN_CENTER)
1873        self.panel.SetSizer(mainSizer)
1874        self.panel.Fit()
1875        self.Fit()
1876
1877    def Show(self):
1878        '''Use this method after creating the dialog to post it
1879        :returns: True if the user pressed OK; False if the User pressed Cancel
1880        '''
1881        if self.ShowModal() == wx.ID_OK:
1882            self.value = self.valItem.GetValue()
1883            return True
1884        else:
1885            return False
1886
1887    def GetValue(self):
1888        '''Use this method to get the value entered by the user
1889        :returns: string entered by user
1890        '''
1891        return self.value
1892
1893################################################################################
1894class MultiStringDialog(wx.Dialog):
1895    '''Dialog to obtain a multi string values from user
1896   
1897    :param wx.Frame parent: name of parent frame
1898    :param str title: title string for dialog
1899    :param str prompts: strings to tell use what they are inputting
1900    :param str values: default input values, if any
1901    '''
1902    def __init__(self,parent,title,prompts,values=[]):      #,size=(200,-1)?
1903       
1904        wx.Dialog.__init__(self,parent,wx.ID_ANY,title, 
1905                           pos=wx.DefaultPosition,
1906                           style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)
1907        self.values = values
1908        self.prompts = prompts
1909        self.CenterOnParent()
1910        self.panel = wx.Panel(self)
1911        mainSizer = wx.BoxSizer(wx.VERTICAL)
1912        promptSizer = wx.FlexGridSizer(0,2,5,5)
1913        self.Indx = {}
1914        for prompt,value in zip(prompts,values):
1915            promptSizer.Add(wx.StaticText(self.panel,-1,prompt),0,WACV)
1916            valItem = wx.TextCtrl(self.panel,-1,value=value,style=wx.TE_PROCESS_ENTER)
1917            self.Indx[valItem.GetId()] = prompt
1918            valItem.Bind(wx.EVT_TEXT,self.newValue)
1919            promptSizer.Add(valItem,0,WACV)
1920        mainSizer.Add(promptSizer,0)
1921        btnsizer = wx.StdDialogButtonSizer()
1922        OKbtn = wx.Button(self.panel, wx.ID_OK)
1923        OKbtn.SetDefault()
1924        btnsizer.AddButton(OKbtn)
1925        btn = wx.Button(self.panel, wx.ID_CANCEL)
1926        btnsizer.AddButton(btn)
1927        btnsizer.Realize()
1928        mainSizer.Add(btnsizer,0,wx.ALIGN_CENTER)
1929        self.panel.SetSizer(mainSizer)
1930        self.panel.Fit()
1931        self.Fit()
1932       
1933    def newValue(self,event):
1934        Obj = event.GetEventObject()
1935        item = self.Indx[Obj.GetId()]
1936        id = self.prompts.index(item)
1937        self.values[id] = Obj.GetValue()
1938
1939    def Show(self):
1940        '''Use this method after creating the dialog to post it
1941        :returns: True if the user pressed OK; False if the User pressed Cancel
1942        '''
1943        if self.ShowModal() == wx.ID_OK:
1944            return True
1945        else:
1946            return False
1947
1948    def GetValues(self):
1949        '''Use this method to get the value entered by the user
1950        :returns: string entered by user
1951        '''
1952        return self.values
1953
1954################################################################################
1955class G2ColumnIDDialog(wx.Dialog):
1956    '''A dialog for matching column data to desired items; some columns may be ignored.
1957   
1958    :param wx.Frame ParentFrame: reference to parent frame
1959    :param str title: heading above list of choices
1960    :param str header: Title to place on window frame
1961    :param list ChoiceList: a list of possible choices for the columns
1962    :param list ColumnData: lists of column data to be matched with ChoiceList
1963    :param bool monoFont: If False (default), use a variable-spaced font;
1964      if True use a equally-spaced font.
1965    :param kw: optional keyword parameters for the wx.Dialog may
1966      be included such as size [which defaults to `(320,310)`] and
1967      style (which defaults to ``wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER | wx.CENTRE | wx.OK | wx.CANCEL``);
1968      note that ``wx.OK`` and ``wx.CANCEL`` controls
1969      the presence of the eponymous buttons in the dialog.
1970    :returns: the name of the created dialog
1971   
1972    '''
1973
1974    def __init__(self,parent, title, header,Comments,ChoiceList, ColumnData,
1975                 monoFont=False, **kw):
1976
1977        def OnOk(sevent):
1978            OK = True
1979            selCols = []
1980            for col in self.sel:
1981                item = col.GetValue()
1982                if item != ' ' and item in selCols:
1983                    OK = False
1984                    break
1985                else:
1986                    selCols.append(item)
1987            parent = self.GetParent()
1988            if not OK:
1989                parent.ErrorDialog('Duplicate',item+' selected more than once')
1990                return
1991            parent.Raise()
1992            self.EndModal(wx.ID_OK)
1993           
1994        def OnModify(event):
1995            Obj = event.GetEventObject()
1996            icol,colData = Indx[Obj.GetId()]
1997            modify = Obj.GetValue()
1998            if not modify:
1999                return
2000            print 'Modify column',icol,' by', modify
2001            for i,item in enumerate(self.ColumnData[icol]):
2002                self.ColumnData[icol][i] = str(eval(item+modify))
2003            colData.SetValue('\n'.join(self.ColumnData[icol]))
2004            Obj.SetValue('')
2005           
2006        # process keyword parameters, notably style
2007        options = {'size':(600,310), # default Frame keywords
2008                   'style':wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER|wx.CENTRE| wx.OK | wx.CANCEL,
2009                   }
2010        options.update(kw)
2011        self.Comments = ''.join(Comments)
2012        self.ChoiceList = ChoiceList
2013        self.ColumnData = ColumnData
2014        nCol = len(ColumnData)
2015        if options['style'] & wx.OK:
2016            useOK = True
2017            options['style'] ^= wx.OK
2018        else:
2019            useOK = False
2020        if options['style'] & wx.CANCEL:
2021            useCANCEL = True
2022            options['style'] ^= wx.CANCEL
2023        else:
2024            useCANCEL = False       
2025        # create the dialog frame
2026        wx.Dialog.__init__(self,parent,wx.ID_ANY,header,**options)
2027        panel = wxscroll.ScrolledPanel(self)
2028        # fill the dialog
2029        Sizer = wx.BoxSizer(wx.VERTICAL)
2030        Sizer.Add((-1,5))
2031        Sizer.Add(wx.StaticText(panel,label=title),0,WACV)
2032        if self.Comments:
2033            Sizer.Add(wx.StaticText(panel,label=' Header lines:'),0,WACV)
2034            Sizer.Add(wx.TextCtrl(panel,value=self.Comments,size=(200,-1),
2035                style=wx.TE_MULTILINE|wx.TE_READONLY|wx.TE_DONTWRAP),0,wx.ALL|wx.EXPAND|WACV,8)
2036        columnsSizer = wx.FlexGridSizer(0,nCol,5,10)
2037        self.sel = []
2038        self.mod = []
2039        Indx = {}
2040        for icol,col in enumerate(self.ColumnData):
2041            colSizer = wx.BoxSizer(wx.VERTICAL)
2042            colSizer.Add(wx.StaticText(panel,label=' Column #%d Select:'%(icol)),0,WACV)
2043            self.sel.append(wx.ComboBox(panel,value=' ',choices=self.ChoiceList,style=wx.CB_READONLY|wx.CB_DROPDOWN))
2044            colSizer.Add(self.sel[-1])
2045            colData = wx.TextCtrl(panel,value='\n'.join(self.ColumnData[icol]),size=(120,-1),
2046                style=wx.TE_MULTILINE|wx.TE_READONLY|wx.TE_DONTWRAP)
2047            colSizer.Add(colData,0,WACV)
2048            colSizer.Add(wx.StaticText(panel,label=' Modify by:'),0,WACV)
2049            mod = wx.TextCtrl(panel,size=(120,-1),value='',style=wx.TE_PROCESS_ENTER)
2050            mod.Bind(wx.EVT_TEXT_ENTER,OnModify)
2051            mod.Bind(wx.EVT_KILL_FOCUS,OnModify)
2052            Indx[mod.GetId()] = [icol,colData]
2053            colSizer.Add(mod,0,WACV)
2054            columnsSizer.Add(colSizer)
2055        Sizer.Add(columnsSizer)
2056        Sizer.Add(wx.StaticText(panel,label=' For modify by, enter arithmetic string eg. "-12345.67". "+","-","*","/","**" all allowed'),0,WACV) 
2057        Sizer.Add((-1,10))
2058        # OK/Cancel buttons
2059        btnsizer = wx.StdDialogButtonSizer()
2060        if useOK:
2061            self.OKbtn = wx.Button(panel, wx.ID_OK)
2062            self.OKbtn.SetDefault()
2063            btnsizer.AddButton(self.OKbtn)
2064            self.OKbtn.Bind(wx.EVT_BUTTON, OnOk)
2065        if useCANCEL:
2066            btn = wx.Button(panel, wx.ID_CANCEL)
2067            btnsizer.AddButton(btn)
2068        btnsizer.Realize()
2069        Sizer.Add((-1,5))
2070        Sizer.Add(btnsizer,0,wx.ALIGN_LEFT,20)
2071        Sizer.Add((-1,5))
2072        # OK done, let's get outa here
2073        panel.SetSizer(Sizer)
2074        panel.SetAutoLayout(1)
2075        panel.SetupScrolling()
2076        Size = [450,375]
2077        panel.SetSize(Size)
2078        Size[0] += 25; Size[1]+= 25
2079        self.SetSize(Size)
2080       
2081    def GetSelection(self):
2082        'Returns the selected sample parm for each column'
2083        selCols = []
2084        for item in self.sel:
2085            selCols.append(item.GetValue())
2086        return selCols,self.ColumnData
2087   
2088################################################################################
2089class G2HistoDataDialog(wx.Dialog):
2090    '''A dialog for editing histogram data globally.
2091   
2092    :param wx.Frame ParentFrame: reference to parent frame
2093    :param str title: heading above list of choices
2094    :param str header: Title to place on window frame
2095    :param list ParmList: a list of names for the columns
2096    :param list ParmFmt: a list of formatting strings for the columns
2097    :param list: HistoList: a list of histogram names
2098    :param list ParmData: a list of lists of data matched to ParmList; one for each item in HistoList
2099    :param bool monoFont: If False (default), use a variable-spaced font;
2100      if True use a equally-spaced font.
2101    :param kw: optional keyword parameters for the wx.Dialog may
2102      be included such as size [which defaults to `(320,310)`] and
2103      style (which defaults to
2104      ``wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER | wx.CENTRE | wx.OK | wx.CANCEL``);
2105      note that ``wx.OK`` and ``wx.CANCEL`` controls the presence of the eponymous buttons in the dialog.
2106    :returns: the modified ParmData
2107   
2108    '''
2109
2110    def __init__(self,parent, title, header,ParmList,ParmFmt,HistoList,ParmData,
2111                 monoFont=False, **kw):
2112
2113        def OnOk(sevent):
2114            parent.Raise()
2115            self.EndModal(wx.ID_OK)
2116           
2117        def OnModify(event):
2118            Obj = event.GetEventObject()
2119            irow,it = Indx[Obj.GetId()]
2120            try:
2121                val = float(Obj.GetValue())
2122            except ValueError:
2123                val = self.ParmData[irow][it]
2124            self.ParmData[irow][it] = val
2125            Obj.SetValue(self.ParmFmt[it]%val)
2126                       
2127        # process keyword parameters, notably style
2128        options = {'size':(600,310), # default Frame keywords
2129                   'style':wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER|wx.CENTRE| wx.OK | wx.CANCEL,
2130                   }
2131        options.update(kw)
2132        self.ParmList = ParmList
2133        self.ParmFmt = ParmFmt
2134        self.HistoList = HistoList
2135        self.ParmData = ParmData
2136        nCol = len(ParmList)
2137        if options['style'] & wx.OK:
2138            useOK = True
2139            options['style'] ^= wx.OK
2140        else:
2141            useOK = False
2142        if options['style'] & wx.CANCEL:
2143            useCANCEL = True
2144            options['style'] ^= wx.CANCEL
2145        else:
2146            useCANCEL = False       
2147        # create the dialog frame
2148        wx.Dialog.__init__(self,parent,wx.ID_ANY,header,**options)
2149        panel = wxscroll.ScrolledPanel(self)
2150        # fill the dialog
2151        Sizer = wx.BoxSizer(wx.VERTICAL)
2152        Sizer.Add((-1,5))
2153        Sizer.Add(wx.StaticText(panel,label=title),0,WACV)
2154        dataSizer = wx.FlexGridSizer(0,nCol+1,0,0)
2155        self.sel = []
2156        self.mod = []
2157        Indx = {}
2158        for item in ['Histogram',]+self.ParmList:
2159            dataSizer.Add(wx.StaticText(panel,-1,label=' %10s '%(item)),0,WACV)
2160        for irow,name in enumerate(self.HistoList):
2161            dataSizer.Add(wx.StaticText(panel,label=name),0,WACV|wx.LEFT|wx.RIGHT,10)
2162            for it,item in enumerate(self.ParmData[irow]):
2163                dat = wx.TextCtrl(panel,-1,value=self.ParmFmt[it]%(item),style=wx.TE_PROCESS_ENTER)
2164                dataSizer.Add(dat,0,WACV)
2165                dat.Bind(wx.EVT_TEXT_ENTER,OnModify)
2166                dat.Bind(wx.EVT_KILL_FOCUS,OnModify)
2167                Indx[dat.GetId()] = [irow,it]
2168        Sizer.Add(dataSizer)
2169        Sizer.Add((-1,10))
2170        # OK/Cancel buttons
2171        btnsizer = wx.StdDialogButtonSizer()
2172        if useOK:
2173            self.OKbtn = wx.Button(panel, wx.ID_OK)
2174            self.OKbtn.SetDefault()
2175            btnsizer.AddButton(self.OKbtn)
2176            self.OKbtn.Bind(wx.EVT_BUTTON, OnOk)
2177        if useCANCEL:
2178            btn = wx.Button(panel, wx.ID_CANCEL)
2179            btnsizer.AddButton(btn)
2180        btnsizer.Realize()
2181        Sizer.Add((-1,5))
2182        Sizer.Add(btnsizer,0,wx.ALIGN_LEFT,20)
2183        Sizer.Add((-1,5))
2184        # OK done, let's get outa here
2185        panel.SetSizer(Sizer)
2186        panel.SetAutoLayout(1)
2187        panel.SetupScrolling()
2188        Size = [450,375]
2189        panel.SetSize(Size)
2190        Size[0] += 25; Size[1]+= 25
2191        self.SetSize(Size)
2192       
2193    def GetData(self):
2194        'Returns the modified ParmData'
2195        return self.ParmData
2196   
2197################################################################################
2198def ItemSelector(ChoiceList, ParentFrame=None,
2199                 title='Select an item',
2200                 size=None, header='Item Selector',
2201                 useCancel=True,multiple=False):
2202    ''' Provide a wx dialog to select a single item or multiple items from list of choices
2203
2204    :param list ChoiceList: a list of choices where one will be selected
2205    :param wx.Frame ParentFrame: Name of parent frame (default None)
2206    :param str title: heading above list of choices (default 'Select an item')
2207    :param wx.Size size: Size for dialog to be created (default None -- size as needed)
2208    :param str header: Title to place on window frame (default 'Item Selector')
2209    :param bool useCancel: If True (default) both the OK and Cancel buttons are offered
2210    :param bool multiple: If True then multiple items can be selected (default False)
2211   
2212    :returns: the selection index or None or a selection list if multiple is true
2213    '''
2214    if multiple:
2215        if useCancel:
2216            dlg = G2MultiChoiceDialog(
2217                ParentFrame,title, header, ChoiceList)
2218        else:
2219            dlg = G2MultiChoiceDialog(
2220                ParentFrame,title, header, ChoiceList,
2221                style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER|wx.OK|wx.CENTRE)
2222    else:
2223        if useCancel:
2224            dlg = wx.SingleChoiceDialog(
2225                ParentFrame,title, header, ChoiceList)
2226        else:
2227            dlg = wx.SingleChoiceDialog(
2228                ParentFrame,title, header,ChoiceList,
2229                style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER|wx.OK|wx.CENTRE)
2230    if size: dlg.SetSize(size)
2231    if dlg.ShowModal() == wx.ID_OK:
2232        if multiple:
2233            dlg.Destroy()
2234            return dlg.GetSelections()
2235        else:
2236            dlg.Destroy()
2237            return dlg.GetSelection()
2238    else:
2239        dlg.Destroy()
2240        return None
2241    dlg.Destroy()
2242
2243######################################################### Column-order selection dialog
2244def GetItemOrder(parent,keylist,vallookup,posdict):
2245    '''Creates a panel where items can be ordered into columns
2246   
2247    :param list keylist: is a list of keys for column assignments
2248    :param dict vallookup: is a dict keyed by names in keylist where each item is a dict.
2249       Each inner dict contains variable names as keys and their associated values
2250    :param dict posdict: is a dict keyed by names in keylist where each item is a dict.
2251       Each inner dict contains column numbers as keys and their associated
2252       variable name as a value. This is used for both input and output.
2253       
2254    '''
2255    dlg = wx.Dialog(parent,style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)
2256    sizer = wx.BoxSizer(wx.VERTICAL)
2257    spanel = OrderBox(dlg,keylist,vallookup,posdict)
2258    spanel.Fit()
2259    sizer.Add(spanel,1,wx.EXPAND)
2260    btnsizer = wx.StdDialogButtonSizer()
2261    btn = wx.Button(dlg, wx.ID_OK)
2262    btn.SetDefault()
2263    btnsizer.AddButton(btn)
2264    #btn = wx.Button(dlg, wx.ID_CANCEL)
2265    #btnsizer.AddButton(btn)
2266    btnsizer.Realize()
2267    sizer.Add(btnsizer, 0, wx.ALIGN_CENTER_VERTICAL|wx.EXPAND|wx.ALL, 5)
2268    dlg.SetSizer(sizer)
2269    sizer.Fit(dlg)
2270    val = dlg.ShowModal()
2271
2272################################################################################
2273class OrderBox(wxscroll.ScrolledPanel):
2274    '''Creates a panel with scrollbars where items can be ordered into columns
2275   
2276    :param list keylist: is a list of keys for column assignments
2277    :param dict vallookup: is a dict keyed by names in keylist where each item is a dict.
2278      Each inner dict contains variable names as keys and their associated values
2279    :param dict posdict: is a dict keyed by names in keylist where each item is a dict.
2280      Each inner dict contains column numbers as keys and their associated
2281      variable name as a value. This is used for both input and output.
2282     
2283    '''
2284    def __init__(self,parent,keylist,vallookup,posdict,*arg,**kw):
2285        self.keylist = keylist
2286        self.vallookup = vallookup
2287        self.posdict = posdict
2288        self.maxcol = 0
2289        for nam in keylist:
2290            posdict = self.posdict[nam]
2291            if posdict.keys():
2292                self.maxcol = max(self.maxcol, max(posdict))
2293        wxscroll.ScrolledPanel.__init__(self,parent,wx.ID_ANY,*arg,**kw)
2294        self.GBsizer = wx.GridBagSizer(4,4)
2295        self.SetBackgroundColour(WHITE)
2296        self.SetSizer(self.GBsizer)
2297        colList = [str(i) for i in range(self.maxcol+2)]
2298        for i in range(self.maxcol+1):
2299            wid = wx.StaticText(self,wx.ID_ANY,str(i),style=wx.ALIGN_CENTER)
2300            wid.SetBackgroundColour(DULL_YELLOW)
2301            wid.SetMinSize((50,-1))
2302            self.GBsizer.Add(wid,(0,i),flag=wx.ALIGN_CENTER|wx.EXPAND)
2303        self.chceDict = {}
2304        for row,nam in enumerate(self.keylist):
2305            posdict = self.posdict[nam]
2306            for col in posdict:
2307                lbl = posdict[col]
2308                pnl = wx.Panel(self,wx.ID_ANY)
2309                pnl.SetBackgroundColour(VERY_LIGHT_GREY)
2310                insize = wx.BoxSizer(wx.VERTICAL)
2311                wid = wx.Choice(pnl,wx.ID_ANY,choices=colList)
2312                insize.Add(wid,0,wx.EXPAND|wx.BOTTOM,3)
2313                wid.SetSelection(col)
2314                self.chceDict[wid] = (row,col)
2315                wid.Bind(wx.EVT_CHOICE,self.OnChoice)
2316                wid = wx.StaticText(pnl,wx.ID_ANY,lbl)
2317                insize.Add(wid,0,flag=wx.EXPAND)
2318                val = G2py3.FormatSigFigs(self.vallookup[nam][lbl],maxdigits=8)
2319                wid = wx.StaticText(pnl,wx.ID_ANY,'('+val+')')
2320                insize.Add(wid,0,flag=wx.EXPAND)
2321                pnl.SetSizer(insize)
2322                self.GBsizer.Add(pnl,(row+1,col),flag=wx.EXPAND)
2323        self.SetAutoLayout(1)
2324        self.SetupScrolling()
2325        self.SetMinSize((
2326            min(700,self.GBsizer.GetSize()[0]),
2327            self.GBsizer.GetSize()[1]+20))
2328    def OnChoice(self,event):
2329        '''Called when a column is assigned to a variable
2330        '''
2331        row,col = self.chceDict[event.EventObject] # which variable was this?
2332        newcol = event.Selection # where will it be moved?
2333        if newcol == col:
2334            return # no change: nothing to do!
2335        prevmaxcol = self.maxcol # save current table size
2336        key = self.keylist[row] # get the key for the current row
2337        lbl = self.posdict[key][col] # selected variable name
2338        lbl1 = self.posdict[key].get(col+1,'') # next variable name, if any
2339        # if a posXXX variable is selected, and the next variable is posXXX, move them together
2340        repeat = 1
2341        if lbl[:3] == 'pos' and lbl1[:3] == 'int' and lbl[3:] == lbl1[3:]:
2342            repeat = 2
2343        for i in range(repeat): # process the posXXX and then the intXXX (or a single variable)
2344            col += i
2345            newcol += i
2346            if newcol in self.posdict[key]:
2347                # find first non-blank after newcol
2348                for mtcol in range(newcol+1,self.maxcol+2):
2349                    if mtcol not in self.posdict[key]: break
2350                l1 = range(mtcol,newcol,-1)+[newcol]
2351                l = range(mtcol-1,newcol-1,-1)+[col]
2352            else:
2353                l1 = [newcol]
2354                l = [col]
2355            # move all of the items, starting from the last column
2356            for newcol,col in zip(l1,l):
2357                #print 'moving',col,'to',newcol
2358                self.posdict[key][newcol] = self.posdict[key][col]
2359                del self.posdict[key][col]
2360                self.maxcol = max(self.maxcol,newcol)
2361                obj = self.GBsizer.FindItemAtPosition((row+1,col))
2362                self.GBsizer.SetItemPosition(obj.GetWindow(),(row+1,newcol))
2363                for wid in obj.GetWindow().Children:
2364                    if wid in self.chceDict:
2365                        self.chceDict[wid] = (row,newcol)
2366                        wid.SetSelection(self.chceDict[wid][1])
2367        # has the table gotten larger? If so we need new column heading(s)
2368        if prevmaxcol != self.maxcol:
2369            for i in range(prevmaxcol+1,self.maxcol+1):
2370                wid = wx.StaticText(self,wx.ID_ANY,str(i),style=wx.ALIGN_CENTER)
2371                wid.SetBackgroundColour(DULL_YELLOW)
2372                wid.SetMinSize((50,-1))
2373                self.GBsizer.Add(wid,(0,i),flag=wx.ALIGN_CENTER|wx.EXPAND)
2374            colList = [str(i) for i in range(self.maxcol+2)]
2375            for wid in self.chceDict:
2376                wid.SetItems(colList)
2377                wid.SetSelection(self.chceDict[wid][1])
2378        self.GBsizer.Layout()
2379        self.FitInside()
2380
2381################################################################################
2382#####  Customized Grid Support
2383################################################################################           
2384class GSGrid(wg.Grid):
2385    '''Basic wx.Grid implementation
2386    '''
2387    def __init__(self, parent, name=''):
2388        wg.Grid.__init__(self,parent,-1,name=name)                   
2389        #self.SetSize(parent.GetClientSize())
2390        # above removed to speed drawing of initial grid
2391        # does not appear to be needed
2392           
2393    def Clear(self):
2394        wg.Grid.ClearGrid(self)
2395       
2396    def SetCellReadOnly(self,r,c,readonly=True):
2397        self.SetReadOnly(r,c,isReadOnly=readonly)
2398       
2399    def SetCellStyle(self,r,c,color="white",readonly=True):
2400        self.SetCellBackgroundColour(r,c,color)
2401        self.SetReadOnly(r,c,isReadOnly=readonly)
2402       
2403    def GetSelection(self):
2404        #this is to satisfy structure drawing stuff in G2plt when focus changes
2405        return None
2406
2407    def InstallGridToolTip(self, rowcolhintcallback,
2408                           colLblCallback=None,rowLblCallback=None):
2409        '''code to display a tooltip for each item on a grid
2410        from http://wiki.wxpython.org/wxGrid%20ToolTips (buggy!), expanded to
2411        column and row labels using hints from
2412        https://groups.google.com/forum/#!topic/wxPython-users/bm8OARRVDCs
2413
2414        :param function rowcolhintcallback: a routine that returns a text
2415          string depending on the selected row and column, to be used in
2416          explaining grid entries.
2417        :param function colLblCallback: a routine that returns a text
2418          string depending on the selected column, to be used in
2419          explaining grid columns (if None, the default), column labels
2420          do not get a tooltip.
2421        :param function rowLblCallback: a routine that returns a text
2422          string depending on the selected row, to be used in
2423          explaining grid rows (if None, the default), row labels
2424          do not get a tooltip.
2425        '''
2426        prev_rowcol = [None,None,None]
2427        def OnMouseMotion(event):
2428            # event.GetRow() and event.GetCol() would be nice to have here,
2429            # but as this is a mouse event, not a grid event, they are not
2430            # available and we need to compute them by hand.
2431            x, y = self.CalcUnscrolledPosition(event.GetPosition())
2432            row = self.YToRow(y)
2433            col = self.XToCol(x)
2434            hinttext = ''
2435            win = event.GetEventObject()
2436            if [row,col,win] == prev_rowcol: # no change from last position
2437                event.Skip()
2438                return
2439            if win == self.GetGridWindow() and row >= 0 and col >= 0:
2440                hinttext = rowcolhintcallback(row, col)
2441            elif win == self.GetGridColLabelWindow() and col >= 0:
2442                if colLblCallback: hinttext = colLblCallback(col)
2443            elif win == self.GetGridRowLabelWindow() and row >= 0:
2444                if rowLblCallback: hinttext = rowLblCallback(row)
2445            else: # this should be the upper left corner, which is empty
2446                event.Skip()
2447                return
2448            if hinttext is None: hinttext = ''
2449            win.SetToolTipString(hinttext)
2450            prev_rowcol[:] = [row,col,win]
2451            event.Skip()
2452
2453        wx.EVT_MOTION(self.GetGridWindow(), OnMouseMotion)
2454        if colLblCallback: wx.EVT_MOTION(self.GetGridColLabelWindow(), OnMouseMotion)
2455        if rowLblCallback: wx.EVT_MOTION(self.GetGridRowLabelWindow(), OnMouseMotion)
2456                                                   
2457################################################################################           
2458class Table(wg.PyGridTableBase):
2459    '''Basic data table for use with GSgrid
2460    '''
2461    def __init__(self, data=[], rowLabels=None, colLabels=None, types = None):
2462        wg.PyGridTableBase.__init__(self)
2463        self.colLabels = colLabels
2464        self.rowLabels = rowLabels
2465        self.dataTypes = types
2466        self.data = data
2467       
2468    def AppendRows(self, numRows=1):
2469        self.data.append([])
2470        return True
2471       
2472    def CanGetValueAs(self, row, col, typeName):
2473        if self.dataTypes:
2474            colType = self.dataTypes[col].split(':')[0]
2475            if typeName == colType:
2476                return True
2477            else:
2478                return False
2479        else:
2480            return False
2481
2482    def CanSetValueAs(self, row, col, typeName):
2483        return self.CanGetValueAs(row, col, typeName)
2484
2485    def DeleteRow(self,pos):
2486        data = self.GetData()
2487        self.SetData([])
2488        new = []
2489        for irow,row in enumerate(data):
2490            if irow <> pos:
2491                new.append(row)
2492        self.SetData(new)
2493       
2494    def GetColLabelValue(self, col):
2495        if self.colLabels:
2496            return self.colLabels[col]
2497           
2498    def GetData(self):
2499        data = []
2500        for row in range(self.GetNumberRows()):
2501            data.append(self.GetRowValues(row))
2502        return data
2503       
2504    def GetNumberCols(self):
2505        try:
2506            return len(self.colLabels)
2507        except TypeError:
2508            return None
2509       
2510    def GetNumberRows(self):
2511        return len(self.data)
2512       
2513    def GetRowLabelValue(self, row):
2514        if self.rowLabels:
2515            return self.rowLabels[row]
2516       
2517    def GetColValues(self, col):
2518        data = []
2519        for row in range(self.GetNumberRows()):
2520            data.append(self.GetValue(row, col))
2521        return data
2522       
2523    def GetRowValues(self, row):
2524        data = []
2525        for col in range(self.GetNumberCols()):
2526            data.append(self.GetValue(row, col))
2527        return data
2528       
2529    def GetTypeName(self, row, col):
2530        try:
2531            if self.data[row][col] is None: return None
2532            return self.dataTypes[col]
2533        except (TypeError,IndexError):
2534            return None
2535
2536    def GetValue(self, row, col):
2537        try:
2538            if self.data[row][col] is None: return ""
2539            return self.data[row][col]
2540        except IndexError:
2541            return None
2542           
2543    def InsertRows(self, pos, rows):
2544        for row in range(rows):
2545            self.data.insert(pos,[])
2546            pos += 1
2547       
2548    def IsEmptyCell(self,row,col):
2549        try:
2550            return not self.data[row][col]
2551        except IndexError:
2552            return True
2553       
2554    def OnKeyPress(self, event):
2555        dellist = self.GetSelectedRows()
2556        if event.GetKeyCode() == wx.WXK_DELETE and dellist:
2557            grid = self.GetView()
2558            for i in dellist: grid.DeleteRow(i)
2559               
2560    def SetColLabelValue(self, col, label):
2561        numcols = self.GetNumberCols()
2562        if col > numcols-1:
2563            self.colLabels.append(label)
2564        else:
2565            self.colLabels[col]=label
2566       
2567    def SetData(self,data):
2568        for row in range(len(data)):
2569            self.SetRowValues(row,data[row])
2570               
2571    def SetRowLabelValue(self, row, label):
2572        self.rowLabels[row]=label
2573           
2574    def SetRowValues(self,row,data):
2575        self.data[row] = data
2576           
2577    def SetValue(self, row, col, value):
2578        def innerSetValue(row, col, value):
2579            try:
2580                self.data[row][col] = value
2581            except TypeError:
2582                return
2583            except IndexError: # has this been tested?
2584                #print row,col,value
2585                # add a new row
2586                if row > self.GetNumberRows():
2587                    self.data.append([''] * self.GetNumberCols())
2588                elif col > self.GetNumberCols():
2589                    for row in range(self.GetNumberRows()): # bug fixed here
2590                        self.data[row].append('')
2591                #print self.data
2592                self.data[row][col] = value
2593        innerSetValue(row, col, value)
2594
2595################################################################################
2596class GridFractionEditor(wg.PyGridCellEditor):
2597    '''A grid cell editor class that allows entry of values as fractions as well
2598    as sine and cosine values [as s() and c()]
2599    '''
2600    def __init__(self,grid):
2601        wg.PyGridCellEditor.__init__(self)
2602
2603    def Create(self, parent, id, evtHandler):
2604        self._tc = wx.TextCtrl(parent, id, "")
2605        self._tc.SetInsertionPoint(0)
2606        self.SetControl(self._tc)
2607
2608        if evtHandler:
2609            self._tc.PushEventHandler(evtHandler)
2610
2611        self._tc.Bind(wx.EVT_CHAR, self.OnChar)
2612
2613    def SetSize(self, rect):
2614        self._tc.SetDimensions(rect.x, rect.y, rect.width+2, rect.height+2,
2615                               wx.SIZE_ALLOW_MINUS_ONE)
2616
2617    def BeginEdit(self, row, col, grid):
2618        self.startValue = grid.GetTable().GetValue(row, col)
2619        self._tc.SetValue(str(self.startValue))
2620        self._tc.SetInsertionPointEnd()
2621        self._tc.SetFocus()
2622        self._tc.SetSelection(0, self._tc.GetLastPosition())
2623
2624    def EndEdit(self, row, col, grid, oldVal=None):
2625        changed = False
2626
2627        self.nextval = self.startValue
2628        val = self._tc.GetValue().lower()
2629        if val != self.startValue:
2630            changed = True
2631            neg = False
2632            if '-' in val:
2633                neg = True
2634            if '/' in val and '.' not in val:
2635                val += '.'
2636            elif 's' in val and not 'sind(' in val:
2637                if neg:
2638                    val = '-sind('+val.strip('-s')+')'
2639                else:
2640                    val = 'sind('+val.strip('s')+')'
2641            elif 'c' in val and not 'cosd(' in val:
2642                if neg:
2643                    val = '-cosd('+val.strip('-c')+')'
2644                else:
2645                    val = 'cosd('+val.strip('c')+')'
2646            try:
2647                self.nextval = val = float(eval(val))
2648            except (SyntaxError,NameError,ZeroDivisionError):
2649                val = self.startValue
2650                return None
2651           
2652            if oldVal is None: # this arg appears in 2.9+; before, we should go ahead & change the table
2653                grid.GetTable().SetValue(row, col, val) # update the table
2654            # otherwise self.ApplyEdit gets called
2655
2656        self.startValue = ''
2657        self._tc.SetValue('')
2658        return changed
2659   
2660    def ApplyEdit(self, row, col, grid):
2661        """ Called only in wx >= 2.9
2662        Save the value of the control into the grid if EndEdit() returns as True
2663        """
2664        grid.GetTable().SetValue(row, col, self.nextval) # update the table
2665
2666    def Reset(self):
2667        self._tc.SetValue(self.startValue)
2668        self._tc.SetInsertionPointEnd()
2669
2670    def Clone(self):
2671        return GridFractionEditor(grid)
2672
2673    def StartingKey(self, evt):
2674        self.OnChar(evt)
2675        if evt.GetSkipped():
2676            self._tc.EmulateKeyPress(evt)
2677
2678    def OnChar(self, evt):
2679        key = evt.GetKeyCode()
2680        if key == 15:
2681            return
2682        if key > 255:
2683            evt.Skip()
2684            return
2685        char = chr(key)
2686        if char in '.+-/0123456789cosind()':
2687            self._tc.WriteText(char)
2688        else:
2689            evt.Skip()
2690           
2691################################################################################
2692#####  Customized Notebook
2693################################################################################           
2694class GSNoteBook(wx.aui.AuiNotebook):
2695    '''Notebook used in various locations; implemented with wx.aui extension
2696    '''
2697    def __init__(self, parent, name='',size = None):
2698        wx.aui.AuiNotebook.__init__(self, parent, -1,
2699                                    style=wx.aui.AUI_NB_TOP |
2700                                    wx.aui.AUI_NB_SCROLL_BUTTONS)
2701        if size: self.SetSize(size)
2702        self.parent = parent
2703        self.PageChangeHandler = None
2704       
2705    def PageChangeEvent(self,event):
2706        G2frame = self.parent.G2frame
2707        page = event.GetSelection()
2708        if self.PageChangeHandler:
2709            if log.LogInfo['Logging']:
2710                log.MakeTabLog(
2711                    G2frame.dataFrame.GetTitle(),
2712                    G2frame.dataDisplay.GetPageText(page)
2713                    )
2714            self.PageChangeHandler(event)
2715           
2716    def Bind(self,eventtype,handler,*args,**kwargs):
2717        '''Override the Bind() function so that page change events can be trapped
2718        '''
2719        if eventtype == wx.aui.EVT_AUINOTEBOOK_PAGE_CHANGED:
2720            self.PageChangeHandler = handler
2721            wx.aui.AuiNotebook.Bind(self,eventtype,self.PageChangeEvent)
2722            return
2723        wx.aui.AuiNotebook.Bind(self,eventtype,handler,*args,**kwargs)
2724                                                     
2725    def Clear(self):       
2726        GSNoteBook.DeleteAllPages(self)
2727       
2728    def FindPage(self,name):
2729        numPage = self.GetPageCount()
2730        for page in range(numPage):
2731            if self.GetPageText(page) == name:
2732                return page
2733
2734    def ChangeSelection(self,page):
2735        # in wx.Notebook ChangeSelection is like SetSelection, but it
2736        # does not invoke the event related to pressing the tab button
2737        # I don't see a way to do that in aui.
2738        oldPage = self.GetSelection()
2739        self.SetSelection(page)
2740        return oldPage
2741
2742    # def __getattribute__(self,name):
2743    #     '''This method provides a way to print out a message every time
2744    #     that a method in a class is called -- to see what all the calls
2745    #     might be, or where they might be coming from.
2746    #     Cute trick for debugging!
2747    #     '''
2748    #     attr = object.__getattribute__(self, name)
2749    #     if hasattr(attr, '__call__'):
2750    #         def newfunc(*args, **kwargs):
2751    #             print('GSauiNoteBook calling %s' %attr.__name__)
2752    #             result = attr(*args, **kwargs)
2753    #             return result
2754    #         return newfunc
2755    #     else:
2756    #         return attr
2757           
2758################################################################################
2759#### Help support routines
2760################################################################################
2761class MyHelp(wx.Menu):
2762    '''
2763    A class that creates the contents of a help menu.
2764    The menu will start with two entries:
2765
2766    * 'Help on <helpType>': where helpType is a reference to an HTML page to
2767      be opened
2768    * About: opens an About dialog using OnHelpAbout. N.B. on the Mac this
2769      gets moved to the App menu to be consistent with Apple style.
2770
2771    NOTE: for this to work properly with respect to system menus, the title
2772    for the menu must be &Help, or it will not be processed properly:
2773
2774    ::
2775
2776       menu.Append(menu=MyHelp(self,...),title="&Help")
2777
2778    '''
2779    def __init__(self,frame,helpType=None,helpLbl=None,morehelpitems=[],title=''):
2780        wx.Menu.__init__(self,title)
2781        self.HelpById = {}
2782        self.frame = frame
2783        self.Append(help='', id=wx.ID_ABOUT, kind=wx.ITEM_NORMAL,
2784            text='&About GSAS-II')
2785        frame.Bind(wx.EVT_MENU, self.OnHelpAbout, id=wx.ID_ABOUT)
2786        if GSASIIpath.whichsvn():
2787            helpobj = self.Append(
2788                help='', id=wx.ID_ANY, kind=wx.ITEM_NORMAL,
2789                text='&Check for updates')
2790            frame.Bind(wx.EVT_MENU, self.OnCheckUpdates, helpobj)
2791            helpobj = self.Append(
2792                help='', id=wx.ID_ANY, kind=wx.ITEM_NORMAL,
2793                text='&Regress to an old GSAS-II version')
2794            frame.Bind(wx.EVT_MENU, self.OnSelectVersion, helpobj)
2795        for lbl,indx in morehelpitems:
2796            helpobj = self.Append(text=lbl,
2797                id=wx.ID_ANY, kind=wx.ITEM_NORMAL)
2798            frame.Bind(wx.EVT_MENU, self.OnHelpById, helpobj)
2799            self.HelpById[helpobj.GetId()] = indx
2800        # add a help item only when helpType is specified
2801        if helpType is not None:
2802            self.AppendSeparator()
2803            if helpLbl is None: helpLbl = helpType
2804            helpobj = self.Append(text='Help on '+helpLbl,
2805                                  id=wx.ID_ANY, kind=wx.ITEM_NORMAL)
2806            frame.Bind(wx.EVT_MENU, self.OnHelpById, helpobj)
2807            self.HelpById[helpobj.GetId()] = helpType
2808       
2809    def OnHelpById(self,event):
2810        '''Called when Help on... is pressed in a menu. Brings up
2811        a web page for documentation.
2812        '''
2813        helpType = self.HelpById.get(event.GetId())
2814        if helpType is None:
2815            print 'Error: help lookup failed!',event.GetEventObject()
2816            print 'id=',event.GetId()
2817        elif helpType == 'Tutorials': 
2818            dlg = OpenTutorial(self.frame)
2819            dlg.ShowModal()
2820            dlg.Destroy()
2821            return
2822        else:
2823            ShowHelp(helpType,self.frame)
2824
2825    def OnHelpAbout(self, event):
2826        "Display an 'About GSAS-II' box"
2827        import GSASII
2828        info = wx.AboutDialogInfo()
2829        info.Name = 'GSAS-II'
2830        ver = GSASIIpath.svnGetRev()
2831        if ver: 
2832            info.Version = 'Revision '+str(ver)+' (svn), version '+GSASII.__version__
2833        else:
2834            info.Version = 'Revision '+str(GSASIIpath.GetVersionNumber())+' (.py files), version '+GSASII.__version__
2835        #info.Developers = ['Robert B. Von Dreele','Brian H. Toby']
2836        info.Copyright = ('(c) ' + time.strftime('%Y') +
2837''' Argonne National Laboratory
2838This product includes software developed
2839by the UChicago Argonne, LLC, as
2840Operator of Argonne National Laboratory.''')
2841        info.Description = '''General Structure Analysis System-II (GSAS-II)
2842Robert B. Von Dreele and Brian H. Toby
2843
2844Please cite as:
2845B.H. Toby & R.B. Von Dreele, J. Appl. Cryst. 46, 544-549 (2013) '''
2846
2847        info.WebSite = ("https://subversion.xray.aps.anl.gov/trac/pyGSAS","GSAS-II home page")
2848        wx.AboutBox(info)
2849
2850    def OnCheckUpdates(self,event):
2851        '''Check if the GSAS-II repository has an update for the current source files
2852        and perform that update if requested.
2853        '''
2854        if not GSASIIpath.whichsvn():
2855            dlg = wx.MessageDialog(self.frame,
2856                                   'No Subversion','Cannot update GSAS-II because subversion (svn) was not found.',
2857                                   wx.OK)
2858            dlg.ShowModal()
2859            dlg.Destroy()
2860            return
2861        wx.BeginBusyCursor()
2862        local = GSASIIpath.svnGetRev()
2863        if local is None: 
2864            wx.EndBusyCursor()
2865            dlg = wx.MessageDialog(self.frame,
2866                                   'Unable to run subversion on the GSAS-II current directory. Is GSAS-II installed correctly?',
2867                                   'Subversion error',
2868                                   wx.OK)
2869            dlg.ShowModal()
2870            dlg.Destroy()
2871            return
2872        print 'Installed GSAS-II version: '+local
2873        repos = GSASIIpath.svnGetRev(local=False)
2874        wx.EndBusyCursor()
2875        if repos is None: 
2876            dlg = wx.MessageDialog(self.frame,
2877                                   'Unable to access the GSAS-II server. Is this computer on the internet?',
2878                                   'Server unavailable',
2879                                   wx.OK)
2880            dlg.ShowModal()
2881            dlg.Destroy()
2882            return
2883        print 'GSAS-II version on server: '+repos
2884        if local == repos:
2885            dlg = wx.MessageDialog(self.frame,
2886                                   'GSAS-II is up-to-date. Version '+local+' is already loaded.',
2887                                   'GSAS-II Up-to-date',
2888                                   wx.OK)
2889            dlg.ShowModal()
2890            dlg.Destroy()
2891            return
2892        mods = GSASIIpath.svnFindLocalChanges()
2893        if mods:
2894            dlg = wx.MessageDialog(self.frame,
2895                                   'You have version '+local+
2896                                   ' of GSAS-II installed, but the current version is '+repos+
2897                                   '. However, '+str(len(mods))+
2898                                   ' file(s) on your local computer have been modified.'
2899                                   ' Updating will attempt to merge your local changes with '
2900                                   'the latest GSAS-II version, but if '
2901                                   'conflicts arise, local changes will be '
2902                                   'discarded. It is also possible that the '
2903                                   'local changes my prevent GSAS-II from running. '
2904                                   'Press OK to start an update if this is acceptable:',
2905                                   'Local GSAS-II Mods',
2906                                   wx.OK|wx.CANCEL)
2907            if dlg.ShowModal() != wx.ID_OK:
2908                dlg.Destroy()
2909                return
2910            else:
2911                dlg.Destroy()
2912        else:
2913            dlg = wx.MessageDialog(self.frame,
2914                                   'You have version '+local+
2915                                   ' of GSAS-II installed, but the current version is '+repos+
2916                                   '. Press OK to start an update:',
2917                                   'GSAS-II Updates',
2918                                   wx.OK|wx.CANCEL)
2919            if dlg.ShowModal() != wx.ID_OK:
2920                dlg.Destroy()
2921                return
2922            dlg.Destroy()
2923        print 'start updates'
2924        dlg = wx.MessageDialog(self.frame,
2925                               'Your project will now be saved, GSAS-II will exit and an update '
2926                               'will be performed and GSAS-II will restart. Press Cancel to '
2927                               'abort the update',
2928                               'Start update?',
2929                               wx.OK|wx.CANCEL)
2930        if dlg.ShowModal() != wx.ID_OK:
2931            dlg.Destroy()
2932            return
2933        dlg.Destroy()
2934        self.frame.OnFileSave(event)
2935        GSASIIpath.svnUpdateProcess(projectfile=self.frame.GSASprojectfile)
2936        return
2937
2938    def OnSelectVersion(self,event):
2939        '''Allow the user to select a specific version of GSAS-II
2940        '''
2941        if not GSASIIpath.whichsvn():
2942            dlg = wx.MessageDialog(self,'No Subversion','Cannot update GSAS-II because subversion (svn) '+
2943                                   'was not found.'
2944                                   ,wx.OK)
2945            dlg.ShowModal()
2946            return
2947        local = GSASIIpath.svnGetRev()
2948        if local is None: 
2949            dlg = wx.MessageDialog(self.frame,
2950                                   'Unable to run subversion on the GSAS-II current directory. Is GSAS-II installed correctly?',
2951                                   'Subversion error',
2952                                   wx.OK)
2953            dlg.ShowModal()
2954            return
2955        mods = GSASIIpath.svnFindLocalChanges()
2956        if mods:
2957            dlg = wx.MessageDialog(self.frame,
2958                                   'You have version '+local+
2959                                   ' of GSAS-II installed'
2960                                   '. However, '+str(len(mods))+
2961                                   ' file(s) on your local computer have been modified.'
2962                                   ' Downdating will attempt to merge your local changes with '
2963                                   'the selected GSAS-II version. '
2964                                   'Downdating is not encouraged because '
2965                                   'if merging is not possible, your local changes will be '
2966                                   'discarded. It is also possible that the '
2967                                   'local changes my prevent GSAS-II from running. '
2968                                   'Press OK to continue anyway.',
2969                                   'Local GSAS-II Mods',
2970                                   wx.OK|wx.CANCEL)
2971            if dlg.ShowModal() != wx.ID_OK:
2972                dlg.Destroy()
2973                return
2974            dlg.Destroy()
2975        dlg = downdate(parent=self.frame)
2976        if dlg.ShowModal() == wx.ID_OK:
2977            ver = dlg.getVersion()
2978        else:
2979            dlg.Destroy()
2980            return
2981        dlg.Destroy()
2982        print('start regress to '+str(ver))
2983        GSASIIpath.svnUpdateProcess(
2984            projectfile=self.frame.GSASprojectfile,
2985            version=str(ver)
2986            )
2987        self.frame.OnFileSave(event)
2988        return
2989
2990################################################################################
2991class AddHelp(wx.Menu):
2992    '''For the Mac: creates an entry to the help menu of type
2993    'Help on <helpType>': where helpType is a reference to an HTML page to
2994    be opened.
2995
2996    NOTE: when appending this menu (menu.Append) be sure to set the title to
2997    '&Help' so that wx handles it correctly.
2998    '''
2999    def __init__(self,frame,helpType,helpLbl=None,title=''):
3000        wx.Menu.__init__(self,title)
3001        self.frame = frame
3002        if helpLbl is None: helpLbl = helpType
3003        # add a help item only when helpType is specified
3004        helpobj = self.Append(text='Help on '+helpLbl,
3005                              id=wx.ID_ANY, kind=wx.ITEM_NORMAL)
3006        frame.Bind(wx.EVT_MENU, self.OnHelpById, helpobj)
3007        self.HelpById = helpType
3008       
3009    def OnHelpById(self,event):
3010        '''Called when Help on... is pressed in a menu. Brings up
3011        a web page for documentation.
3012        '''
3013        ShowHelp(self.HelpById,self.frame)
3014
3015################################################################################
3016class HelpButton(wx.Button):
3017    '''Create a help button that displays help information.
3018    The text is displayed in a modal message window.
3019
3020    TODO: it might be nice if it were non-modal: e.g. it stays around until
3021    the parent is deleted or the user closes it, but this did not work for
3022    me.
3023
3024    :param parent: the panel which will be the parent of the button
3025    :param str msg: the help text to be displayed
3026    '''
3027    def __init__(self,parent,msg):
3028        if sys.platform == "darwin": 
3029            wx.Button.__init__(self,parent,wx.ID_HELP)
3030        else:
3031            wx.Button.__init__(self,parent,wx.ID_ANY,'?',style=wx.BU_EXACTFIT)
3032        self.Bind(wx.EVT_BUTTON,self._onPress)
3033        self.msg=StripIndents(msg)
3034        self.parent = parent
3035    def _onClose(self,event):
3036        self.dlg.EndModal(wx.ID_CANCEL)
3037    def _onPress(self,event):
3038        'Respond to a button press by displaying the requested text'
3039        #dlg = wx.MessageDialog(self.parent,self.msg,'Help info',wx.OK)
3040        self.dlg = wx.Dialog(self.parent,wx.ID_ANY,'Help information', 
3041                        style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)
3042        #self.dlg.SetBackgroundColour(wx.WHITE)
3043        mainSizer = wx.BoxSizer(wx.VERTICAL)
3044        txt = wx.StaticText(self.dlg,wx.ID_ANY,self.msg)
3045        mainSizer.Add(txt,1,wx.ALL|wx.EXPAND,10)
3046        txt.SetBackgroundColour(wx.WHITE)
3047
3048        btnsizer = wx.BoxSizer(wx.HORIZONTAL)
3049        btn = wx.Button(self.dlg, wx.ID_CLOSE) 
3050        btn.Bind(wx.EVT_BUTTON,self._onClose)
3051        btnsizer.Add(btn)
3052        mainSizer.Add(btnsizer, 0, wx.ALIGN_CENTER|wx.ALL, 5)
3053        self.dlg.SetSizer(mainSizer)
3054        mainSizer.Fit(self.dlg)
3055        self.dlg.CenterOnParent()
3056        self.dlg.ShowModal()
3057        self.dlg.Destroy()
3058################################################################################
3059class MyHtmlPanel(wx.Panel):
3060    '''Defines a panel to display HTML help information, as an alternative to
3061    displaying help information in a web browser.
3062    '''
3063    def __init__(self, frame, id):
3064        self.frame = frame
3065        wx.Panel.__init__(self, frame, id)
3066        sizer = wx.BoxSizer(wx.VERTICAL)
3067        back = wx.Button(self, -1, "Back")
3068        back.Bind(wx.EVT_BUTTON, self.OnBack)
3069        self.htmlwin = G2HtmlWindow(self, id, size=(750,450))
3070        sizer.Add(self.htmlwin, 1,wx.EXPAND)
3071        sizer.Add(back, 0, wx.ALIGN_LEFT, 0)
3072        self.SetSizer(sizer)
3073        sizer.Fit(frame)       
3074        self.Bind(wx.EVT_SIZE,self.OnHelpSize)
3075    def OnHelpSize(self,event):         #does the job but weirdly!!
3076        anchor = self.htmlwin.GetOpenedAnchor()
3077        if anchor:           
3078            self.htmlwin.ScrollToAnchor(anchor)
3079            wx.CallAfter(self.htmlwin.ScrollToAnchor,anchor)
3080            event.Skip()
3081    def OnBack(self, event):
3082        self.htmlwin.HistoryBack()
3083    def LoadFile(self,file):
3084        pos = file.rfind('#')
3085        if pos != -1:
3086            helpfile = file[:pos]
3087            helpanchor = file[pos+1:]
3088        else:
3089            helpfile = file
3090            helpanchor = None
3091        self.htmlwin.LoadPage(helpfile)
3092        if helpanchor is not None:
3093            self.htmlwin.ScrollToAnchor(helpanchor)
3094            xs,ys = self.htmlwin.GetViewStart()
3095            self.htmlwin.Scroll(xs,ys-1)
3096################################################################################
3097class G2HtmlWindow(wx.html.HtmlWindow):
3098    '''Displays help information in a primitive HTML browser type window
3099    '''
3100    def __init__(self, parent, *args, **kwargs):
3101        self.parent = parent
3102        wx.html.HtmlWindow.__init__(self, parent, *args, **kwargs)
3103    def LoadPage(self, *args, **kwargs):
3104        wx.html.HtmlWindow.LoadPage(self, *args, **kwargs)
3105        self.TitlePage()
3106    def OnLinkClicked(self, *args, **kwargs):
3107        wx.html.HtmlWindow.OnLinkClicked(self, *args, **kwargs)
3108        xs,ys = self.GetViewStart()
3109        self.Scroll(xs,ys-1)
3110        self.TitlePage()
3111    def HistoryBack(self, *args, **kwargs):
3112        wx.html.HtmlWindow.HistoryBack(self, *args, **kwargs)
3113        self.TitlePage()
3114    def TitlePage(self):
3115        self.parent.frame.SetTitle(self.GetOpenedPage() + ' -- ' + 
3116            self.GetOpenedPageTitle())
3117
3118################################################################################
3119def StripIndents(msg):
3120    'Strip indentation from multiline strings'
3121    msg1 = msg.replace('\n ','\n')
3122    while msg != msg1:
3123        msg = msg1
3124        msg1 = msg.replace('\n ','\n')
3125    return msg.replace('\n\t','\n')
3126       
3127################################################################################
3128class downdate(wx.Dialog):
3129    '''Dialog to allow a user to select a version of GSAS-II to install
3130    '''
3131    def __init__(self,parent=None):
3132        style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER
3133        wx.Dialog.__init__(self, parent, wx.ID_ANY, 'Select Version', style=style)
3134        pnl = wx.Panel(self)
3135        sizer = wx.BoxSizer(wx.VERTICAL)
3136        insver = GSASIIpath.svnGetRev(local=True)
3137        curver = int(GSASIIpath.svnGetRev(local=False))
3138        label = wx.StaticText(
3139            pnl,  wx.ID_ANY,
3140            'Select a specific GSAS-II version to install'
3141            )
3142        sizer.Add(label, 0, wx.ALIGN_CENTRE|wx.ALL, 5)
3143        sizer1 = wx.BoxSizer(wx.HORIZONTAL)
3144        sizer1.Add(
3145            wx.StaticText(pnl,  wx.ID_ANY,
3146                          'Currently installed version: '+str(insver)),
3147            0, wx.ALIGN_CENTRE|wx.ALL, 5)
3148        sizer.Add(sizer1)
3149        sizer1 = wx.BoxSizer(wx.HORIZONTAL)
3150        sizer1.Add(
3151            wx.StaticText(pnl,  wx.ID_ANY,
3152                          'Select GSAS-II version to install: '),
3153            0, wx.ALIGN_CENTRE|wx.ALL, 5)
3154        self.spin = wx.SpinCtrl(pnl, wx.ID_ANY,size=(150,-1))
3155        self.spin.SetRange(1, curver)
3156        self.spin.SetValue(curver)
3157        self.Bind(wx.EVT_SPINCTRL, self._onSpin, self.spin)
3158        self.Bind(wx.EVT_KILL_FOCUS, self._onSpin, self.spin)
3159        sizer1.Add(self.spin)
3160        sizer.Add(sizer1)
3161
3162        line = wx.StaticLine(pnl,-1, size=(-1,3), style=wx.LI_HORIZONTAL)
3163        sizer.Add(line, 0, wx.EXPAND|wx.ALIGN_CENTER|wx.ALL, 10)
3164
3165        self.text = wx.StaticText(pnl,  wx.ID_ANY, "")
3166        sizer.Add(self.text, 0, wx.ALIGN_LEFT|wx.EXPAND|wx.ALL, 5)
3167
3168        line = wx.StaticLine(pnl,-1, size=(-1,3), style=wx.LI_HORIZONTAL)
3169        sizer.Add(line, 0, wx.EXPAND|wx.ALIGN_CENTER|wx.ALL, 10)
3170        sizer.Add(
3171            wx.StaticText(
3172                pnl,  wx.ID_ANY,
3173                'If "Install" is pressed, your project will be saved;\n'
3174                'GSAS-II will exit; The specified version will be loaded\n'
3175                'and GSAS-II will restart. Press "Cancel" to abort.'),
3176            0, wx.EXPAND|wx.ALL, 10)
3177        btnsizer = wx.StdDialogButtonSizer()
3178        btn = wx.Button(pnl, wx.ID_OK, "Install")
3179        btn.SetDefault()
3180        btnsizer.AddButton(btn)
3181        btn = wx.Button(pnl, wx.ID_CANCEL)
3182        btnsizer.AddButton(btn)
3183        btnsizer.Realize()
3184        sizer.Add(btnsizer, 0, wx.ALIGN_CENTER|wx.ALL, 5)
3185        pnl.SetSizer(sizer)
3186        sizer.Fit(self)
3187        self.topsizer=sizer
3188        self.CenterOnParent()
3189        self._onSpin(None)
3190
3191    def _onSpin(self,event):
3192        'Called to load info about the selected version in the dialog'
3193        ver = self.spin.GetValue()
3194        d = GSASIIpath.svnGetLog(version=ver)
3195        date = d.get('date','?').split('T')[0]
3196        s = '(Version '+str(ver)+' created '+date
3197        s += ' by '+d.get('author','?')+')'
3198        msg = d.get('msg')
3199        if msg: s += '\n\nComment: '+msg
3200        self.text.SetLabel(s)
3201        self.topsizer.Fit(self)
3202
3203    def getVersion(self):
3204        'Get the version number in the dialog'
3205        return self.spin.GetValue()
3206
3207################################################################################
3208#### Display Help information
3209################################################################################
3210# define some globals
3211htmlPanel = None
3212htmlFrame = None
3213htmlFirstUse = True
3214helpLocDict = {}
3215path2GSAS2 = os.path.dirname(os.path.realpath(__file__)) # save location of this file
3216def ShowHelp(helpType,frame):
3217    '''Called to bring up a web page for documentation.'''
3218    global htmlFirstUse
3219    # look up a definition for help info from dict
3220    helplink = helpLocDict.get(helpType)
3221    if helplink is None:
3222        # no defined link to use, create a default based on key
3223        helplink = 'gsasII.html#'+helpType.replace(' ','_')
3224    helplink = os.path.join(path2GSAS2,'help',helplink)
3225    # determine if a web browser or the internal viewer should be used for help info
3226    if GSASIIpath.GetConfigValue('Help_mode'):
3227        helpMode = GSASIIpath.GetConfigValue('Help_mode')
3228    else:
3229        helpMode = 'browser'
3230    if helpMode == 'internal':
3231        try:
3232            htmlPanel.LoadFile(helplink)
3233            htmlFrame.Raise()
3234        except:
3235            htmlFrame = wx.Frame(frame, -1, size=(610, 510))
3236            htmlFrame.Show(True)
3237            htmlFrame.SetTitle("HTML Window") # N.B. reset later in LoadFile
3238            htmlPanel = MyHtmlPanel(htmlFrame,-1)
3239            htmlPanel.LoadFile(helplink)
3240    else:
3241        pfx = "file://"
3242        if sys.platform.lower().startswith('win'):
3243            pfx = ''
3244        if htmlFirstUse:
3245            webbrowser.open_new(pfx+helplink)
3246            htmlFirstUse = False
3247        else:
3248            webbrowser.open(pfx+helplink, new=0, autoraise=True)
3249
3250def ShowWebPage(URL,frame):
3251    '''Called to show a tutorial web page.
3252    '''
3253    global htmlFirstUse
3254    # determine if a web browser or the internal viewer should be used for help info
3255    if GSASIIpath.GetConfigValue('Help_mode'):
3256        helpMode = GSASIIpath.GetConfigValue('Help_mode')
3257    else:
3258        helpMode = 'browser'
3259    if helpMode == 'internal':
3260        try:
3261            htmlPanel.LoadFile(URL)
3262            htmlFrame.Raise()
3263        except:
3264            htmlFrame = wx.Frame(frame, -1, size=(610, 510))
3265            htmlFrame.Show(True)
3266            htmlFrame.SetTitle("HTML Window") # N.B. reset later in LoadFile
3267            htmlPanel = MyHtmlPanel(htmlFrame,-1)
3268            htmlPanel.LoadFile(URL)
3269    else:
3270        if URL.startswith('http'): 
3271            pfx = ''
3272        elif sys.platform.lower().startswith('win'):
3273            pfx = ''
3274        else:
3275            pfx = "file://"
3276        if htmlFirstUse:
3277            webbrowser.open_new(pfx+URL)
3278            htmlFirstUse = False
3279        else:
3280            webbrowser.open(pfx+URL, new=0, autoraise=True)
3281
3282################################################################################
3283#### Tutorials support
3284################################################################################
3285G2BaseURL = "https://subversion.xray.aps.anl.gov/pyGSAS"
3286# N.B. tutorialCatalog is generated by routine catalog.py, which also generates the appropriate
3287# empty directories (.../MT/* .../trunk/GSASII/* *=[help,Exercises])
3288tutorialCatalog = (
3289    # tutorial dir,      exercise dir,      web page file name,      title for page
3290
3291    ['StartingGSASII', 'StartingGSASII', 'Starting GSAS.htm',
3292        'Starting GSAS-II'],
3293       
3294    ['FitPeaks', 'FitPeaks', 'Fit Peaks.htm',
3295        'Fitting individual peaks & autoindexing'],
3296    ['BkgFit', 'BkgFit', 'FitBkgTut.htm',
3297       'Fitting the Starting Background using Fixed Points'],
3298       
3299    ['CWNeutron', 'CWNeutron', 'Neutron CW Powder Data.htm',
3300        'CW Neutron Powder fit for Yttrium-Iron Garnet'],
3301    ['LabData', 'LabData', 'Laboratory X.htm',
3302        'Fitting laboratory X-ray powder data for fluoroapatite'],
3303    ['CWCombined', 'CWCombined', 'Combined refinement.htm',
3304        'Combined X-ray/CW-neutron refinement of PbSO4'],
3305    ['TOF-CW Joint Refinement', 'TOF-CW Joint Refinement', 'TOF combined XN Rietveld refinement in GSAS.htm',
3306        'Combined X-ray/TOF-neutron Rietveld refinement'],
3307    ['SeqRefine', 'SeqRefine', 'SequentialTutorial.htm',
3308        'Sequential refinement of multiple datasets'],
3309    ['SeqParametric', 'SeqParametric', 'ParametricFitting.htm',
3310        'Parametric Fitting and Pseudo Variables for Sequential Fits'],
3311       
3312    ['CFjadarite', 'CFjadarite', 'Charge Flipping in GSAS.htm',
3313        'Charge Flipping structure solution for jadarite'],
3314    ['CFsucrose', 'CFsucrose', 'Charge Flipping - sucrose.htm',
3315        'Charge Flipping structure solution for sucrose'],
3316    ['TOF Charge Flipping', 'TOF Charge Flipping', 'Charge Flipping with TOF single crystal data in GSASII.htm',
3317        'Charge flipping with neutron TOF single crystal data'],
3318    ['MCsimanneal', 'MCsimanneal', 'MCSA in GSAS.htm',
3319        'Monte-Carlo simulated annealing structure'],
3320
3321    ['2DCalibration', '2DCalibration', 'Calibration of an area detector in GSAS.htm',
3322        'Calibration of an area detector'],
3323    ['2DIntegration', '2DIntegration', 'Integration of area detector data in GSAS.htm',
3324        'Integration of area detector data'],
3325    ['TOF Calibration', 'TOF Calibration', 'Calibration of a TOF powder diffractometer.htm',
3326        'Calibration of a Neutron TOF diffractometer'],
3327    ['TOF Single Crystal Refinement', 'TOF Single Crystal Refinement', 'TOF single crystal refinement in GSAS.htm',
3328        'Single crystal refinement from TOF data'],
3329       
3330    ['2DStrain', '2DStrain', 'Strain fitting of 2D data in GSAS-II.htm',
3331        'Strain fitting of 2D data'],
3332    ['2DTexture', '2DTexture', 'Texture analysis of 2D data in GSAS-II.htm',
3333        'Texture analysis of 2D data'],
3334             
3335    ['SAimages', 'SAimages', 'Small Angle Image Processing.htm',
3336        'Image Processing of small angle x-ray data'],
3337    ['SAfit', 'SAfit', 'Fitting Small Angle Scattering Data.htm',
3338        'Fitting small angle x-ray data (alumina powder)'],
3339    ['SAsize', 'SAsize', 'Small Angle Size Distribution.htm',
3340        'Small angle x-ray data size distribution (alumina powder)'],
3341    ['SAseqref', 'SAseqref', 'Sequential Refinement of Small Angle Scattering Data.htm',
3342        'Sequential refinement with small angle scattering data'],
3343   
3344    #['TOF Sequential Single Peak Fit', 'TOF Sequential Single Peak Fit', '', ''],
3345    )
3346if GSASIIpath.GetConfigValue('Tutorial_location'):
3347    tutorialPath = GSASIIpath.GetConfigValue('Tutorial_location')
3348else:
3349    # pick a default directory in a logical place
3350    if sys.platform.lower().startswith('win') and os.path.exists(os.path.abspath(os.path.expanduser('~/My Documents'))):
3351        tutorialPath = os.path.abspath(os.path.expanduser('~/My Documents/G2tutorials'))
3352    else:
3353        tutorialPath = os.path.abspath(os.path.expanduser('~/G2tutorials'))
3354
3355class OpenTutorial(wx.Dialog):
3356    '''Open a tutorial, optionally copying it to the local disk. Always copy
3357    the data files locally.
3358
3359    For now tutorials will always be copied into the source code tree, but it
3360    might be better to have an option to copy them somewhere else, for people
3361    who don't have write access to the GSAS-II source code location.
3362    '''
3363    # TODO: set default input-file open location to the download location
3364    def __init__(self,parent=None):
3365        style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER
3366        wx.Dialog.__init__(self, parent, wx.ID_ANY, 'Open Tutorial', style=style)
3367        self.frame = parent
3368        pnl = wx.Panel(self)
3369        sizer = wx.BoxSizer(wx.VERTICAL)
3370        sizer1 = wx.BoxSizer(wx.HORIZONTAL)       
3371        label = wx.StaticText(
3372            pnl,  wx.ID_ANY,
3373            'Select the tutorial to be run and the mode of access'
3374            )
3375        msg = '''To save download time for GSAS-II tutorials and their
3376        sample data files are being moved out of the standard
3377        distribution. This dialog allows users to load selected
3378        tutorials to their computer.
3379
3380        Tutorials can be viewed over the internet or downloaded
3381        to this computer. The sample data can be downloaded or not,
3382        (but it is not possible to run the tutorial without the
3383        data). If no web access is available, tutorials that were
3384        previously downloaded can be viewed.
3385
3386        By default, files are downloaded into the location used
3387        for the GSAS-II distribution, but this may not be possible
3388        if the software is installed by a administrator. The
3389        download location can be changed using the "Set data
3390        location" or the "Tutorial_location" configuration option
3391        (see config_example.py).
3392        '''
3393        hlp = HelpButton(pnl,msg)
3394        sizer1.Add((-1,-1),1, wx.EXPAND, 0)
3395        sizer1.Add(label, 0, wx.ALIGN_CENTRE|wx.ALL, 0)
3396        sizer1.Add((-1,-1),1, wx.EXPAND, 0)
3397        sizer1.Add(hlp,0,wx.ALIGN_RIGHT|wx.ALL)
3398        sizer.Add(sizer1,0,wx.EXPAND|wx.ALL,0)
3399        sizer.Add((10,10))
3400        self.BrowseMode = 1
3401        choices = [
3402            'make local copy of tutorial and data, then open',
3403            'run from web (copy data locally)',
3404            'browse on web (data not loaded)', 
3405            'open from local tutorial copy',
3406        ]
3407        self.mode = wx.RadioBox(pnl,wx.ID_ANY,'access mode:',
3408                                wx.DefaultPosition, wx.DefaultSize,
3409                                choices, 1, wx.RA_SPECIFY_COLS)
3410        self.mode.SetSelection(self.BrowseMode)
3411        self.mode.Bind(wx.EVT_RADIOBOX, self.OnModeSelect)
3412        sizer.Add(self.mode,0,WACV)
3413        sizer.Add((10,10))
3414        label = wx.StaticText(pnl,  wx.ID_ANY,'Click on tutorial to be opened:')
3415        sizer.Add(label, 0, wx.ALIGN_LEFT|wx.ALL, 2)
3416        self.listbox = wx.ListBox(pnl, wx.ID_ANY, size=(450, 100), style=wx.LB_SINGLE)
3417        self.listbox.Bind(wx.EVT_LISTBOX, self.OnTutorialSelected)
3418        sizer.Add(self.listbox,1,WACV|wx.EXPAND|wx.ALL,1)
3419        sizer.Add((10,10))
3420        sizer1 = wx.BoxSizer(wx.HORIZONTAL)
3421        btn = wx.Button(pnl, wx.ID_ANY, "Set download location")
3422        btn.Bind(wx.EVT_BUTTON, self.SelectDownloadLoc)
3423        sizer1.Add(btn,0,WACV)
3424        self.dataLoc = wx.StaticText(pnl, wx.ID_ANY,tutorialPath)
3425        sizer1.Add(self.dataLoc,0,WACV)
3426        sizer.Add(sizer1)
3427        label = wx.StaticText(
3428            pnl,  wx.ID_ANY,
3429            'Tutorials and Exercise files will be downloaded to:'
3430            )
3431        sizer.Add(label, 0, wx.ALIGN_LEFT|wx.ALL, 1)
3432        self.TutorialLabel = wx.StaticText(pnl,wx.ID_ANY,'')
3433        sizer.Add(self.TutorialLabel, 0, wx.ALIGN_LEFT|wx.EXPAND, 5)
3434        self.ExerciseLabel = wx.StaticText(pnl,wx.ID_ANY,'')
3435        sizer.Add(self.ExerciseLabel, 0, wx.ALIGN_LEFT|wx.EXPAND, 5)
3436        self.ShowTutorialPath()
3437        self.OnModeSelect(None)
3438       
3439        btnsizer = wx.StdDialogButtonSizer()
3440        btn = wx.Button(pnl, wx.ID_CANCEL)
3441        btnsizer.AddButton(btn)
3442        btnsizer.Realize()
3443        sizer.Add(btnsizer, 0, wx.ALIGN_CENTER|wx.ALL, 5)
3444        pnl.SetSizer(sizer)
3445        sizer.Fit(self)
3446        self.topsizer=sizer
3447        self.CenterOnParent()
3448    # def OpenOld(self,event):
3449    #     '''Open old tutorials. This is needed only until we get all the tutorials items moved
3450    #     '''
3451    #     self.EndModal(wx.ID_OK)
3452    #     ShowHelp('Tutorials',self.frame)
3453    def OnModeSelect(self,event):
3454        '''Respond when the mode is changed
3455        '''
3456        self.BrowseMode = self.mode.GetSelection()
3457        if self.BrowseMode == 3:
3458            import glob
3459            filelist = glob.glob(os.path.join(tutorialPath,'help','*','*.htm'))
3460            taillist = [os.path.split(f)[1] for f in filelist]
3461            itemlist = [tut[-1] for tut in tutorialCatalog if tut[2] in taillist]
3462        else:
3463            itemlist = [tut[-1] for tut in tutorialCatalog if tut[-1]]
3464        self.listbox.Clear()
3465        self.listbox.AppendItems(itemlist)
3466    def OnTutorialSelected(self,event):
3467        '''Respond when a tutorial is selected. Load tutorials and data locally,
3468        as needed and then display the page
3469        '''
3470        for tutdir,exedir,htmlname,title in tutorialCatalog:
3471            if title == event.GetString(): break
3472        else:
3473            raise Exception("Match to file not found")
3474        if self.BrowseMode == 0 or self.BrowseMode == 1:
3475            try: 
3476                self.ValidateTutorialDir(tutorialPath,G2BaseURL)
3477            except:
3478                G2MessageBox(self.frame,
3479            '''The selected directory is not valid.
3480           
3481            You must use a directory that you have write access
3482            to. You can reuse a directory previously used for
3483            downloads, but the help and Tutorials subdirectories
3484             must be created by this routine.
3485            ''')
3486                return
3487        #self.dataLoc.SetLabel(tutorialPath)
3488        self.EndModal(wx.ID_OK)
3489        wx.BeginBusyCursor()
3490        if self.BrowseMode == 0:
3491            # xfer data & web page locally, then open web page
3492            self.LoadTutorial(tutdir,tutorialPath,G2BaseURL)
3493            self.LoadExercise(exedir,tutorialPath,G2BaseURL)
3494            URL = os.path.join(tutorialPath,'help',tutdir,htmlname)
3495            self.frame.ImportDir = os.path.join(tutorialPath,'Exercises',exedir)
3496            ShowWebPage(URL,self.frame)
3497        elif self.BrowseMode == 1:
3498            # xfer data locally, open web page remotely
3499            self.LoadExercise(exedir,tutorialPath,G2BaseURL)
3500            URL = os.path.join(G2BaseURL,'Tutorials',tutdir,htmlname)
3501            self.frame.ImportDir = os.path.join(tutorialPath,'Exercises',exedir)
3502            ShowWebPage(URL,self.frame)
3503        elif self.BrowseMode == 2:
3504            # open web page remotely, don't worry about data
3505            URL = os.path.join(G2BaseURL,'Tutorials',tutdir,htmlname)
3506            ShowWebPage(URL,self.frame)
3507        elif self.BrowseMode == 3:
3508            # open web page that has already been transferred
3509            URL = os.path.join(tutorialPath,'help',tutdir,htmlname)
3510            self.frame.ImportDir = os.path.join(tutorialPath,'Exercises',exedir)
3511            ShowWebPage(URL,self.frame)
3512        else:
3513            wx.EndBusyCursor()
3514            raise Exception("How did this happen!")
3515        wx.EndBusyCursor()
3516    def ShowTutorialPath(self):
3517        'Show the help and exercise directory names'
3518        self.TutorialLabel.SetLabel('\t'+
3519                                    os.path.join(tutorialPath,"help") +
3520                                    ' (tutorials)')
3521        self.ExerciseLabel.SetLabel('\t'+
3522                                    os.path.join(tutorialPath,"Exercises") +
3523                                    ' (exercises)')
3524    def ValidateTutorialDir(self,fullpath=tutorialPath,baseURL=G2BaseURL):
3525        '''Load help to new directory or make sure existing directory looks correctly set up
3526        throws an exception if there is a problem.
3527        '''
3528        wx.BeginBusyCursor()
3529        wx.Yield()
3530        if os.path.exists(fullpath):
3531            if os.path.exists(os.path.join(fullpath,"help")):
3532                if not GSASIIpath.svnGetRev(os.path.join(fullpath,"help")):
3533                    print("Problem with "+fullpath+" dir help exists but is not in SVN")
3534                    wx.EndBusyCursor()
3535                    raise Exception
3536            if os.path.exists(os.path.join(fullpath,"Exercises")):
3537                if not GSASIIpath.svnGetRev(os.path.join(fullpath,"Exercises")):
3538                    print("Problem with "+fullpath+" dir Exercises exists but is not in SVN")
3539                    wx.EndBusyCursor()
3540                    raise Exception
3541            if (os.path.exists(os.path.join(fullpath,"help")) and
3542                    os.path.exists(os.path.join(fullpath,"Exercises"))):
3543                if self.BrowseMode != 3:
3544                    print('Checking for directory updates')
3545                    GSASIIpath.svnUpdateDir(os.path.join(fullpath,"help"))
3546                    GSASIIpath.svnUpdateDir(os.path.join(fullpath,"Exercises"))
3547                wx.EndBusyCursor()
3548                return True # both good
3549            elif (os.path.exists(os.path.join(fullpath,"help")) or
3550                    os.path.exists(os.path.join(fullpath,"Exercises"))):
3551                print("Problem: dir "+fullpath+" exists has either help or Exercises, not both")
3552                wx.EndBusyCursor()
3553                raise Exception
3554        if not GSASIIpath.svnInstallDir(baseURL+"/MT",fullpath):
3555            wx.EndBusyCursor()
3556            print("Problem transferring empty directory from web")
3557            raise Exception
3558        wx.EndBusyCursor()
3559        return True
3560
3561    def LoadTutorial(self,tutorialname,fullpath=tutorialPath,baseURL=G2BaseURL):
3562        'Load a Tutorial to the selected location'
3563        if GSASIIpath.svnSwitchDir("help",tutorialname,baseURL+"/Tutorials",fullpath):
3564            return True
3565        print("Problem transferring Tutorial from web")
3566        raise Exception
3567       
3568    def LoadExercise(self,tutorialname,fullpath=tutorialPath,baseURL=G2BaseURL):
3569        'Load Exercise file(s) for a Tutorial to the selected location'
3570        if GSASIIpath.svnSwitchDir("Exercises",tutorialname,baseURL+"/Exercises",fullpath):
3571            return True
3572        print ("Problem transferring Exercise from web")
3573        raise Exception
3574       
3575    def SelectDownloadLoc(self,event):
3576        '''Select a download location,
3577        Cancel resets to the default
3578        '''
3579        global tutorialPath
3580        dlg = wx.DirDialog(self, "Choose a directory for downloads:",
3581                           defaultPath=tutorialPath)#,style=wx.DD_DEFAULT_STYLE)
3582                           #)
3583        try:
3584            if dlg.ShowModal() != wx.ID_OK:
3585                return
3586            pth = dlg.GetPath()
3587        finally:
3588            dlg.Destroy()
3589
3590        if not os.path.exists(pth):
3591            try:
3592                os.makedirs(pth)
3593            except OSError:
3594                msg = 'The selected directory is not valid.\n\t'
3595                msg += pth
3596                msg += '\n\nAn attempt to create the directory failed'
3597                G2MessageBox(self.frame,msg)
3598                return
3599        try:
3600            self.ValidateTutorialDir(pth,G2BaseURL)
3601            tutorialPath = pth
3602        except:
3603            G2MessageBox(self.frame,
3604            '''Error downloading to the selected directory
3605
3606            Are you connected to the internet? If not, you can
3607            only view previously downloaded tutorials (select
3608            "open from local...")
3609           
3610            You must use a directory that you have write access
3611            to. You can reuse a directory previously used for
3612            downloads, but the help and Tutorials subdirectories
3613            must have been created by this routine.
3614            ''')
3615        self.dataLoc.SetLabel(tutorialPath)
3616        self.ShowTutorialPath()
3617        self.OnModeSelect(None)
3618   
3619if __name__ == '__main__':
3620    app = wx.PySimpleApp()
3621    GSASIIpath.InvokeDebugOpts()
3622    frm = wx.Frame(None) # create a frame
3623    frm.Show(True)
3624    #dlg = OpenTutorial(frm)
3625    #if dlg.ShowModal() == wx.ID_OK:
3626    #    print "OK"
3627    #else:
3628    #    print "Cancel"
3629    #dlg.Destroy()
3630    #import sys
3631    #sys.exit()
3632    #======================================================================
3633    # test ScrolledMultiEditor
3634    #======================================================================
3635    # Data1 = {
3636    #      'Order':1,
3637    #      'omega':'string',
3638    #      'chi':2.0,
3639    #      'phi':'',
3640    #      }
3641    # elemlst = sorted(Data1.keys())
3642    # prelbl = sorted(Data1.keys())
3643    # dictlst = len(elemlst)*[Data1,]
3644    #Data2 = [True,False,False,True]
3645    #Checkdictlst = len(Data2)*[Data2,]
3646    #Checkelemlst = range(len(Checkdictlst))
3647    # print 'before',Data1,'\n',Data2
3648    # dlg = ScrolledMultiEditor(
3649    #     frm,dictlst,elemlst,prelbl,
3650    #     checkdictlst=Checkdictlst,checkelemlst=Checkelemlst,
3651    #     checklabel="Refine?",
3652    #     header="test")
3653    # if dlg.ShowModal() == wx.ID_OK:
3654    #     print "OK"
3655    # else:
3656    #     print "Cancel"
3657    # print 'after',Data1,'\n',Data2
3658    # dlg.Destroy()
3659    Data3 = {
3660         'Order':1.0,
3661         'omega':1.1,
3662         'chi':2.0,
3663         'phi':2.3,
3664         'Order1':1.0,
3665         'omega1':1.1,
3666         'chi1':2.0,
3667         'phi1':2.3,
3668         'Order2':1.0,
3669         'omega2':1.1,
3670         'chi2':2.0,
3671         'phi2':2.3,
3672         }
3673    elemlst = sorted(Data3.keys())
3674    dictlst = len(elemlst)*[Data3,]
3675    prelbl = elemlst[:]
3676    prelbl[0]="this is a much longer label to stretch things out"
3677    Data2 = len(elemlst)*[False,]
3678    Data2[1] = Data2[3] = True
3679    Checkdictlst = len(elemlst)*[Data2,]
3680    Checkelemlst = range(len(Checkdictlst))
3681    #print 'before',Data3,'\n',Data2
3682    #print dictlst,"\n",elemlst
3683    #print Checkdictlst,"\n",Checkelemlst
3684    # dlg = ScrolledMultiEditor(
3685    #     frm,dictlst,elemlst,prelbl,
3686    #     checkdictlst=Checkdictlst,checkelemlst=Checkelemlst,
3687    #     checklabel="Refine?",
3688    #     header="test",CopyButton=True)
3689    # if dlg.ShowModal() == wx.ID_OK:
3690    #     print "OK"
3691    # else:
3692    #     print "Cancel"
3693    #print 'after',Data3,'\n',Data2
3694
3695    # Data2 = list(range(100))
3696    # elemlst += range(2,6)
3697    # postlbl += range(2,6)
3698    # dictlst += len(range(2,6))*[Data2,]
3699
3700    # prelbl = range(len(elemlst))
3701    # postlbl[1] = "a very long label for the 2nd item to force a horiz. scrollbar"
3702    # header="""This is a longer\nmultiline and perhaps silly header"""
3703    # dlg = ScrolledMultiEditor(frm,dictlst,elemlst,prelbl,postlbl,
3704    #                           header=header,CopyButton=True)
3705    # print Data1
3706    # if dlg.ShowModal() == wx.ID_OK:
3707    #     for d,k in zip(dictlst,elemlst):
3708    #         print k,d[k]
3709    # dlg.Destroy()
3710    # if CallScrolledMultiEditor(frm,dictlst,elemlst,prelbl,postlbl,
3711    #                            header=header):
3712    #     for d,k in zip(dictlst,elemlst):
3713    #         print k,d[k]
3714
3715    #======================================================================
3716    # test G2MultiChoiceDialog
3717    #======================================================================
3718    choices = []
3719    for i in range(21):
3720        choices.append("option_"+str(i))
3721    dlg = G2MultiChoiceDialog(frm, 'Sequential refinement',
3722                              'Select dataset to include',
3723                              choices)
3724    sel = range(2,11,2)
3725    dlg.SetSelections(sel)
3726    dlg.SetSelections((1,5))
3727    if dlg.ShowModal() == wx.ID_OK:
3728        for sel in dlg.GetSelections():
3729            print sel,choices[sel]
3730   
3731    #======================================================================
3732    # test wx.MultiChoiceDialog
3733    #======================================================================
3734    # dlg = wx.MultiChoiceDialog(frm, 'Sequential refinement',
3735    #                           'Select dataset to include',
3736    #                           choices)
3737    # sel = range(2,11,2)
3738    # dlg.SetSelections(sel)
3739    # dlg.SetSelections((1,5))
3740    # if dlg.ShowModal() == wx.ID_OK:
3741    #     for sel in dlg.GetSelections():
3742    #         print sel,choices[sel]
3743
3744    # pnl = wx.Panel(frm)
3745    # siz = wx.BoxSizer(wx.VERTICAL)
3746
3747    # td = {'Goni':200.,'a':1.,'calc':1./3.,'string':'s'}
3748    # for key in sorted(td):
3749    #     txt = ValidatedTxtCtrl(pnl,td,key)
3750    #     siz.Add(txt)
3751    # pnl.SetSizer(siz)
3752    # siz.Fit(frm)
3753    # app.MainLoop()
3754    # print td
Note: See TracBrowser for help on using the repository browser.