source: trunk/GSASIIctrls.py @ 1803

Last change on this file since 1803 was 1803, checked in by toby, 9 years ago

fix range selection in G2MultiChoiceDialog

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