source: trunk/GSASIIctrls.py @ 1807

Last change on this file since 1807 was 1807, checked in by vondreele, 9 years ago

remove some (now) dead code
fix shift-arrow for stepping thru tree
remove extra selections in tree processing

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