source: trunk/GSASIIctrls.py @ 1656

Last change on this file since 1656 was 1619, checked in by toby, 10 years ago

add columnsorter for hetrogeneous seq ref; reorg to start moving widgets out of g2grid

  • Property svn:eol-style set to native
File size: 62.0 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
24# import time
25import copy
26# import cPickle
27# import sys
28# import os
29# import numpy as np
30# import numpy.ma as ma
31# import scipy.optimize as so
32# import wx.html        # could postpone this for quicker startup
33# import 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# globals we will use later
55WHITE = (255,255,255)
56DULL_YELLOW = (230,230,190)
57VERY_LIGHT_GREY = wx.Colour(235,235,235)
58
59################################################################################
60#### Tree Control
61################################################################################
62class G2TreeCtrl(wx.TreeCtrl):
63    '''Create a wrapper around the standard TreeCtrl so we can "wrap"
64    various events.
65   
66    This logs when a tree item is selected (in :meth:`onSelectionChanged`)
67
68    This also wraps lists and dicts pulled out of the tree to track where
69    they were retrieved from.
70    '''
71    def __init__(self,parent=None,*args,**kwargs):
72        super(self.__class__,self).__init__(parent=parent,*args,**kwargs)
73        self.G2frame = parent.GetParent()
74        self.root = self.AddRoot('Loaded Data: ')
75        self.SelectionChanged = None
76        log.LogInfo['Tree'] = self
77
78    def _getTreeItemsList(self,item):
79        '''Get the full tree hierarchy from a reference to a tree item.
80        Note that this effectively hard-codes phase and histogram names in the
81        returned list. We may want to make these names relative in the future.
82        '''
83        textlist = [self.GetItemText(item)]
84        parent = self.GetItemParent(item)
85        while parent:
86            if parent == self.root: break
87            textlist.insert(0,self.GetItemText(parent))
88            parent = self.GetItemParent(parent)
89        return textlist
90
91    def onSelectionChanged(self,event):
92        '''Log each press on a tree item here.
93        '''
94        if self.SelectionChanged:
95            textlist = self._getTreeItemsList(event.GetItem())
96            if log.LogInfo['Logging'] and event.GetItem() != self.root:
97                textlist[0] = self.GetRelativeHistNum(textlist[0])
98                if textlist[0] == "Phases" and len(textlist) > 1:
99                    textlist[1] = self.GetRelativePhaseNum(textlist[1])
100                log.MakeTreeLog(textlist)
101            self.SelectionChanged(event)
102
103    def Bind(self,eventtype,handler,*args,**kwargs):
104        '''Override the Bind() function so that page change events can be trapped
105        '''
106        if eventtype == wx.EVT_TREE_SEL_CHANGED:
107            self.SelectionChanged = handler
108            wx.TreeCtrl.Bind(self,eventtype,self.onSelectionChanged)
109            return
110        wx.TreeCtrl.Bind(self,eventtype,handler,*args,**kwargs)
111
112    # commented out, disables Logging
113    # def GetItemPyData(self,*args,**kwargs):
114    #    '''Override the standard method to wrap the contents
115    #    so that the source can be logged when changed
116    #    '''
117    #    data = super(self.__class__,self).GetItemPyData(*args,**kwargs)
118    #    textlist = self._getTreeItemsList(args[0])
119    #    if type(data) is dict:
120    #        return log.dictLogged(data,textlist)
121    #    if type(data) is list:
122    #        return log.listLogged(data,textlist)
123    #    if type(data) is tuple: #N.B. tuples get converted to lists
124    #        return log.listLogged(list(data),textlist)
125    #    return data
126
127    def GetRelativeHistNum(self,histname):
128        '''Returns list with a histogram type and a relative number for that
129        histogram, or the original string if not a histogram
130        '''
131        histtype = histname.split()[0]
132        if histtype != histtype.upper(): # histograms (only) have a keyword all in caps
133            return histname
134        item, cookie = self.GetFirstChild(self.root)
135        i = 0
136        while item:
137            itemtext = self.GetItemText(item)
138            if itemtext == histname:
139                return histtype,i
140            elif itemtext.split()[0] == histtype:
141                i += 1
142            item, cookie = self.GetNextChild(self.root, cookie)
143        else:
144            raise Exception("Histogram not found: "+histname)
145
146    def ConvertRelativeHistNum(self,histtype,histnum):
147        '''Converts a histogram type and relative histogram number to a
148        histogram name in the current project
149        '''
150        item, cookie = self.GetFirstChild(self.root)
151        i = 0
152        while item:
153            itemtext = self.GetItemText(item)
154            if itemtext.split()[0] == histtype:
155                if i == histnum: return itemtext
156                i += 1
157            item, cookie = self.GetNextChild(self.root, cookie)
158        else:
159            raise Exception("Histogram #'+str(histnum)+' of type "+histtype+' not found')
160       
161    def GetRelativePhaseNum(self,phasename):
162        '''Returns a phase number if the string matches a phase name
163        or else returns the original string
164        '''
165        item, cookie = self.GetFirstChild(self.root)
166        while item:
167            itemtext = self.GetItemText(item)
168            if itemtext == "Phases":
169                parent = item
170                item, cookie = self.GetFirstChild(parent)
171                i = 0
172                while item:
173                    itemtext = self.GetItemText(item)
174                    if itemtext == phasename:
175                        return i
176                    item, cookie = self.GetNextChild(parent, cookie)
177                    i += 1
178                else:
179                    return phasename # not a phase name
180            item, cookie = self.GetNextChild(self.root, cookie)
181        else:
182            raise Exception("No phases found ")
183
184    def ConvertRelativePhaseNum(self,phasenum):
185        '''Converts relative phase number to a phase name in
186        the current project
187        '''
188        item, cookie = self.GetFirstChild(self.root)
189        while item:
190            itemtext = self.GetItemText(item)
191            if itemtext == "Phases":
192                parent = item
193                item, cookie = self.GetFirstChild(parent)
194                i = 0
195                while item:
196                    if i == phasenum:
197                        return self.GetItemText(item)
198                    item, cookie = self.GetNextChild(parent, cookie)
199                    i += 1
200                else:
201                    raise Exception("Phase "+str(phasenum)+" not found")
202            item, cookie = self.GetNextChild(self.root, cookie)
203        else:
204            raise Exception("No phases found ")
205
206################################################################################
207#### TextCtrl that stores input as entered with optional validation
208################################################################################
209class ValidatedTxtCtrl(wx.TextCtrl):
210    '''Create a TextCtrl widget that uses a validator to prevent the
211    entry of inappropriate characters and changes color to highlight
212    when invalid input is supplied. As valid values are typed,
213    they are placed into the dict or list where the initial value
214    came from. The type of the initial value must be int,
215    float or str or None (see :obj:`key` and :obj:`typeHint`);
216    this type (or the one in :obj:`typeHint`) is preserved.
217
218    Float values can be entered in the TextCtrl as numbers or also
219    as algebraic expressions using operators + - / \* () and \*\*,
220    in addition pi, sind(), cosd(), tand(), and sqrt() can be used,
221    as well as appreviations s, sin, c, cos, t, tan and sq.
222
223    :param wx.Panel parent: name of panel or frame that will be
224      the parent to the TextCtrl. Can be None.
225
226    :param dict/list loc: the dict or list with the initial value to be
227      placed in the TextCtrl.
228
229    :param int/str key: the dict key or the list index for the value to be
230      edited by the TextCtrl. The ``loc[key]`` element must exist, but may
231      have value None. If None, the type for the element is taken from
232      :obj:`typeHint` and the value for the control is set initially
233      blank (and thus invalid.) This is a way to specify a field without a
234      default value: a user must set a valid value.
235      If the value is not None, it must have a base
236      type of int, float, str or unicode; the TextCrtl will be initialized
237      from this value.
238     
239    :param list nDig: number of digits & places ([nDig,nPlc]) after decimal to use
240      for display of float. Alternately, None can be specified which causes
241      numbers to be displayed with approximately 5 significant figures
242      (Default=None).
243
244    :param bool notBlank: if True (default) blank values are invalid
245      for str inputs.
246     
247    :param number min: minimum allowed valid value. If None (default) the
248      lower limit is unbounded.
249
250    :param number max: maximum allowed valid value. If None (default) the
251      upper limit is unbounded
252
253    :param function OKcontrol: specifies a function or method that will be
254      called when the input is validated. The called function is supplied
255      with one argument which is False if the TextCtrl contains an invalid
256      value and True if the value is valid.
257      Note that this function should check all values
258      in the dialog when True, since other entries might be invalid.
259      The default for this is None, which indicates no function should
260      be called.
261
262    :param function OnLeave: specifies a function or method that will be
263      called when the focus for the control is lost.
264      The called function is supplied with (at present) three keyword arguments:
265
266         * invalid: (*bool*) True if the value for the TextCtrl is invalid
267         * value:   (*int/float/str*)  the value contained in the TextCtrl
268         * tc:      (*wx.TextCtrl*)  the TextCtrl name
269
270      The number of keyword arguments may be increased in the future should needs arise,
271      so it is best to code these functions with a \*\*kwargs argument so they will
272      continue to run without errors
273
274      The default for OnLeave is None, which indicates no function should
275      be called.
276
277    :param type typeHint: the value of typeHint is overrides the initial value
278      for the dict/list element ``loc[key]``, if set to
279      int or float, which specifies the type for input to the TextCtrl.
280      Defaults as None, which is ignored.
281
282    :param bool CIFinput: for str input, indicates that only printable
283      ASCII characters may be entered into the TextCtrl. Forces output
284      to be ASCII rather than Unicode. For float and int input, allows
285      use of a single '?' or '.' character as valid input.
286
287    :param dict OnLeaveArgs: a dict with keyword args that are passed to
288      the :attr:`OnLeave` function. Defaults to ``{}``
289
290    :param (other): other optional keyword parameters for the
291      wx.TextCtrl widget such as size or style may be specified.
292
293    '''
294    def __init__(self,parent,loc,key,nDig=None,notBlank=True,min=None,max=None,
295                 OKcontrol=None,OnLeave=None,typeHint=None,
296                 CIFinput=False, OnLeaveArgs={}, **kw):
297        # save passed values needed outside __init__
298        self.result = loc
299        self.key = key
300        self.nDig = nDig
301        self.OKcontrol=OKcontrol
302        self.OnLeave = OnLeave
303        self.OnLeaveArgs = OnLeaveArgs
304        self.CIFinput = CIFinput
305        self.type = str
306        # initialization
307        self.invalid = False   # indicates if the control has invalid contents
308        self.evaluated = False # set to True when the validator recognizes an expression
309        val = loc[key]
310        if isinstance(val,int) or typeHint is int:
311            self.type = int
312            wx.TextCtrl.__init__(
313                self,parent,wx.ID_ANY,
314                validator=NumberValidator(int,result=loc,key=key,
315                                          min=min,max=max,
316                                          OKcontrol=OKcontrol,
317                                          CIFinput=CIFinput),
318                **kw)
319            if val is not None:
320                self._setValue(val)
321            else: # no default is invalid for a number
322                self.invalid = True
323                self._IndicateValidity()
324
325        elif isinstance(val,float) or typeHint is float:
326            self.type = float
327            wx.TextCtrl.__init__(
328                self,parent,wx.ID_ANY,
329                validator=NumberValidator(float,result=loc,key=key,
330                                          min=min,max=max,
331                                          OKcontrol=OKcontrol,
332                                          CIFinput=CIFinput),
333                **kw)
334            if val is not None:
335                self._setValue(val)
336            else:
337                self.invalid = True
338                self._IndicateValidity()
339
340        elif isinstance(val,str) or isinstance(val,unicode):
341            if self.CIFinput:
342                wx.TextCtrl.__init__(
343                    self,parent,wx.ID_ANY,val,
344                    validator=ASCIIValidator(result=loc,key=key),
345                    **kw)
346            else:
347                wx.TextCtrl.__init__(self,parent,wx.ID_ANY,val,**kw)
348            if notBlank:
349                self.Bind(wx.EVT_CHAR,self._onStringKey)
350                self.ShowStringValidity() # test if valid input
351            else:
352                self.invalid = False
353                self.Bind(wx.EVT_CHAR,self._GetStringValue)
354        elif val is None:
355            raise Exception,("ValidatedTxtCtrl error: value of "+str(key)+
356                             " element is None and typeHint not defined as int or float")
357        else:
358            raise Exception,("ValidatedTxtCtrl error: Unknown element ("+str(key)+
359                             ") type: "+str(type(val)))
360        # When the mouse is moved away or the widget loses focus,
361        # display the last saved value, if an expression
362        #self.Bind(wx.EVT_LEAVE_WINDOW, self._onLeaveWindow)
363        self.Bind(wx.EVT_TEXT_ENTER, self._onLoseFocus)
364        self.Bind(wx.EVT_KILL_FOCUS, self._onLoseFocus)
365        # patch for wx 2.9 on Mac
366        i,j= wx.__version__.split('.')[0:2]
367        if int(i)+int(j)/10. > 2.8 and 'wxOSX' in wx.PlatformInfo:
368            self.Bind(wx.EVT_KEY_DOWN, self.OnKeyDown)
369
370    def SetValue(self,val):
371        if self.result is not None: # note that this bypasses formatting
372            self.result[self.key] = val
373            log.LogVarChange(self.result,self.key)
374        self._setValue(val)
375
376    def _setValue(self,val):
377        self.invalid = False
378        if self.type is int:
379            try:
380                if int(val) != val:
381                    self.invalid = True
382                else:
383                    val = int(val)
384            except:
385                if self.CIFinput and (val == '?' or val == '.'):
386                    pass
387                else:
388                    self.invalid = True
389            wx.TextCtrl.SetValue(self,str(val))
390        elif self.type is float:
391            try:
392                val = float(val) # convert strings, if needed
393            except:
394                if self.CIFinput and (val == '?' or val == '.'):
395                    pass
396                else:
397                    self.invalid = True
398            if self.nDig:
399                wx.TextCtrl.SetValue(self,str(G2py3.FormatValue(val,self.nDig)))
400            else:
401                wx.TextCtrl.SetValue(self,str(G2py3.FormatSigFigs(val)).rstrip('0'))
402        else:
403            wx.TextCtrl.SetValue(self,str(val))
404            self.ShowStringValidity() # test if valid input
405            return
406       
407        self._IndicateValidity()
408        if self.OKcontrol:
409            self.OKcontrol(not self.invalid)
410
411    def OnKeyDown(self,event):
412        'Special callback for wx 2.9+ on Mac where backspace is not processed by validator'
413        key = event.GetKeyCode()
414        if key in [wx.WXK_BACK, wx.WXK_DELETE]:
415            if self.Validator: wx.CallAfter(self.Validator.TestValid,self)
416        if key == wx.WXK_RETURN:
417            self._onLoseFocus(None)
418        event.Skip()
419                   
420    def _onStringKey(self,event):
421        event.Skip()
422        if self.invalid: # check for validity after processing the keystroke
423            wx.CallAfter(self.ShowStringValidity,True) # was invalid
424        else:
425            wx.CallAfter(self.ShowStringValidity,False) # was valid
426
427    def _IndicateValidity(self):
428        'Set the control colors to show invalid input'
429        if self.invalid:
430            self.SetForegroundColour("red")
431            self.SetBackgroundColour("yellow")
432            self.SetFocus()
433            self.Refresh()
434        else: # valid input
435            self.SetBackgroundColour(
436                wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW))
437            self.SetForegroundColour("black")
438            self.Refresh()
439
440    def ShowStringValidity(self,previousInvalid=True):
441        '''Check if input is valid. Anytime the input is
442        invalid, call self.OKcontrol (if defined) because it is fast.
443        If valid, check for any other invalid entries only when
444        changing from invalid to valid, since that is slower.
445       
446        :param bool previousInvalid: True if the TextCtrl contents were
447          invalid prior to the current change.
448         
449        '''
450        val = self.GetValue().strip()
451        self.invalid = not val
452        self._IndicateValidity()
453        if self.invalid:
454            if self.OKcontrol:
455                self.OKcontrol(False)
456        elif self.OKcontrol and previousInvalid:
457            self.OKcontrol(True)
458        # always store the result
459        if self.CIFinput: # for CIF make results ASCII
460            self.result[self.key] = val.encode('ascii','replace') 
461        else:
462            self.result[self.key] = val
463        log.LogVarChange(self.result,self.key)
464
465    def _GetStringValue(self,event):
466        '''Get string input and store.
467        '''
468        event.Skip() # process keystroke
469        wx.CallAfter(self._SaveStringValue)
470       
471    def _SaveStringValue(self):
472        val = self.GetValue().strip()
473        # always store the result
474        if self.CIFinput: # for CIF make results ASCII
475            self.result[self.key] = val.encode('ascii','replace') 
476        else:
477            self.result[self.key] = val
478        log.LogVarChange(self.result,self.key)
479
480    def _onLoseFocus(self,event):
481        if self.evaluated:
482            self.EvaluateExpression()
483        elif self.result is not None: # show formatted result, as Bob wants
484            self._setValue(self.result[self.key])
485        if self.OnLeave: self.OnLeave(invalid=self.invalid,
486                                      value=self.result[self.key],
487                                      tc=self,
488                                      **self.OnLeaveArgs)
489        if event: event.Skip()
490
491    def EvaluateExpression(self):
492        '''Show the computed value when an expression is entered to the TextCtrl
493        Make sure that the number fits by truncating decimal places and switching
494        to scientific notation, as needed.
495        Called on loss of focus, enter, etc..
496        '''
497        if self.invalid: return # don't substitute for an invalid expression
498        if not self.evaluated: return # true when an expression is evaluated
499        if self.result is not None: # retrieve the stored result
500            self._setValue(self.result[self.key])
501        self.evaluated = False # expression has been recast as value, reset flag
502       
503class NumberValidator(wx.PyValidator):
504    '''A validator to be used with a TextCtrl to prevent
505    entering characters other than digits, signs, and for float
506    input, a period and exponents.
507   
508    The value is checked for validity after every keystroke
509      If an invalid number is entered, the box is highlighted.
510      If the number is valid, it is saved in result[key]
511
512    :param type typ: the base data type. Must be int or float.
513
514    :param bool positiveonly: If True, negative integers are not allowed
515      (default False). This prevents the + or - keys from being pressed.
516      Used with typ=int; ignored for typ=float.
517
518    :param number min: Minimum allowed value. If None (default) the
519      lower limit is unbounded
520
521    :param number max: Maximum allowed value. If None (default) the
522      upper limit is unbounded
523     
524    :param dict/list result: List or dict where value should be placed when valid
525
526    :param any key: key to use for result (int for list)
527
528    :param function OKcontrol: function or class method to control
529      an OK button for a window.
530      Ignored if None (default)
531
532    :param bool CIFinput: allows use of a single '?' or '.' character
533      as valid input.
534     
535    '''
536    def __init__(self, typ, positiveonly=False, min=None, max=None,
537                 result=None, key=None, OKcontrol=None, CIFinput=False):
538        'Create the validator'
539        wx.PyValidator.__init__(self)
540        # save passed parameters
541        self.typ = typ
542        self.positiveonly = positiveonly
543        self.min = min
544        self.max = max
545        self.result = result
546        self.key = key
547        self.OKcontrol = OKcontrol
548        self.CIFinput = CIFinput
549        # set allowed keys by data type
550        self.Bind(wx.EVT_CHAR, self.OnChar)
551        if self.typ == int and self.positiveonly:
552            self.validchars = '0123456789'
553        elif self.typ == int:
554            self.validchars = '0123456789+-'
555        elif self.typ == float:
556            # allow for above and sind, cosd, sqrt, tand, pi, and abbreviations
557            # also addition, subtraction, division, multiplication, exponentiation
558            self.validchars = '0123456789.-+eE/cosindcqrtap()*'
559        else:
560            self.validchars = None
561            return
562        if self.CIFinput:
563            self.validchars += '?.'
564    def Clone(self):
565        'Create a copy of the validator, a strange, but required component'
566        return NumberValidator(typ=self.typ, 
567                               positiveonly=self.positiveonly,
568                               min=self.min, max=self.max,
569                               result=self.result, key=self.key,
570                               OKcontrol=self.OKcontrol,
571                               CIFinput=self.CIFinput)
572    def TransferToWindow(self):
573        'Needed by validator, strange, but required component'
574        return True # Prevent wxDialog from complaining.
575    def TransferFromWindow(self):
576        'Needed by validator, strange, but required component'
577        return True # Prevent wxDialog from complaining.
578    def TestValid(self,tc):
579        '''Check if the value is valid by casting the input string
580        into the current type.
581
582        Set the invalid variable in the TextCtrl object accordingly.
583
584        If the value is valid, save it in the dict/list where
585        the initial value was stored, if appropriate.
586
587        :param wx.TextCtrl tc: A reference to the TextCtrl that the validator
588          is associated with.
589        '''
590        tc.invalid = False # assume valid
591        if self.CIFinput:
592            val = tc.GetValue().strip()
593            if val == '?' or val == '.':
594                self.result[self.key] = val
595                log.LogVarChange(self.result,self.key)
596                return
597        try:
598            val = self.typ(tc.GetValue())
599        except (ValueError, SyntaxError) as e:
600            if self.typ is float: # for float values, see if an expression can be evaluated
601                val = G2py3.FormulaEval(tc.GetValue())
602                if val is None:
603                    tc.invalid = True
604                    return
605                else:
606                    tc.evaluated = True
607            else: 
608                tc.invalid = True
609                return
610        # if self.max != None and self.typ == int:
611        #     if val > self.max:
612        #         tc.invalid = True
613        # if self.min != None and self.typ == int:
614        #     if val < self.min:
615        #         tc.invalid = True  # invalid
616        if self.max != None:
617            if val > self.max:
618                tc.invalid = True
619        if self.min != None:
620            if val < self.min:
621                tc.invalid = True  # invalid
622        if self.key is not None and self.result is not None and not tc.invalid:
623            self.result[self.key] = val
624            log.LogVarChange(self.result,self.key)
625
626    def ShowValidity(self,tc):
627        '''Set the control colors to show invalid input
628
629        :param wx.TextCtrl tc: A reference to the TextCtrl that the validator
630          is associated with.
631
632        '''
633        if tc.invalid:
634            tc.SetForegroundColour("red")
635            tc.SetBackgroundColour("yellow")
636            tc.SetFocus()
637            tc.Refresh()
638            return False
639        else: # valid input
640            tc.SetBackgroundColour(
641                wx.SystemSettings_GetColour(wx.SYS_COLOUR_WINDOW))
642            tc.SetForegroundColour("black")
643            tc.Refresh()
644            return True
645
646    def CheckInput(self,previousInvalid):
647        '''called to test every change to the TextCtrl for validity and
648        to change the appearance of the TextCtrl
649
650        Anytime the input is invalid, call self.OKcontrol
651        (if defined) because it is fast.
652        If valid, check for any other invalid entries only when
653        changing from invalid to valid, since that is slower.
654
655        :param bool previousInvalid: True if the TextCtrl contents were
656          invalid prior to the current change.
657        '''
658        tc = self.GetWindow()
659        self.TestValid(tc)
660        self.ShowValidity(tc)
661        # if invalid
662        if tc.invalid and self.OKcontrol:
663            self.OKcontrol(False)
664        if not tc.invalid and self.OKcontrol and previousInvalid:
665            self.OKcontrol(True)
666
667    def OnChar(self, event):
668        '''Called each type a key is pressed
669        ignores keys that are not allowed for int and float types
670        '''
671        key = event.GetKeyCode()
672        tc = self.GetWindow()
673        if key == wx.WXK_RETURN:
674            if tc.invalid:
675                self.CheckInput(True) 
676            else:
677                self.CheckInput(False) 
678            return
679        if key < wx.WXK_SPACE or key == wx.WXK_DELETE or key > 255: # control characters get processed
680            event.Skip()
681            if tc.invalid:
682                wx.CallAfter(self.CheckInput,True) 
683            else:
684                wx.CallAfter(self.CheckInput,False) 
685            return
686        elif chr(key) in self.validchars: # valid char pressed?
687            event.Skip()
688            if tc.invalid:
689                wx.CallAfter(self.CheckInput,True) 
690            else:
691                wx.CallAfter(self.CheckInput,False) 
692            return
693        if not wx.Validator_IsSilent(): wx.Bell()
694        return  # Returning without calling event.Skip, which eats the keystroke
695
696class ASCIIValidator(wx.PyValidator):
697    '''A validator to be used with a TextCtrl to prevent
698    entering characters other than ASCII characters.
699   
700    The value is checked for validity after every keystroke
701      If an invalid number is entered, the box is highlighted.
702      If the number is valid, it is saved in result[key]
703
704    :param dict/list result: List or dict where value should be placed when valid
705
706    :param any key: key to use for result (int for list)
707
708    '''
709    def __init__(self, result=None, key=None):
710        'Create the validator'
711        import string
712        wx.PyValidator.__init__(self)
713        # save passed parameters
714        self.result = result
715        self.key = key
716        self.validchars = string.ascii_letters + string.digits + string.punctuation + string.whitespace
717        self.Bind(wx.EVT_CHAR, self.OnChar)
718    def Clone(self):
719        'Create a copy of the validator, a strange, but required component'
720        return ASCIIValidator(result=self.result, key=self.key)
721        tc = self.GetWindow()
722        tc.invalid = False # make sure the validity flag is defined in parent
723    def TransferToWindow(self):
724        'Needed by validator, strange, but required component'
725        return True # Prevent wxDialog from complaining.
726    def TransferFromWindow(self):
727        'Needed by validator, strange, but required component'
728        return True # Prevent wxDialog from complaining.
729    def TestValid(self,tc):
730        '''Check if the value is valid by casting the input string
731        into ASCII.
732
733        Save it in the dict/list where the initial value was stored
734
735        :param wx.TextCtrl tc: A reference to the TextCtrl that the validator
736          is associated with.
737        '''
738        self.result[self.key] = tc.GetValue().encode('ascii','replace')
739        log.LogVarChange(self.result,self.key)
740
741    def OnChar(self, event):
742        '''Called each type a key is pressed
743        ignores keys that are not allowed for int and float types
744        '''
745        key = event.GetKeyCode()
746        tc = self.GetWindow()
747        if key == wx.WXK_RETURN:
748            self.TestValid(tc)
749            return
750        if key < wx.WXK_SPACE or key == wx.WXK_DELETE or key > 255: # control characters get processed
751            event.Skip()
752            self.TestValid(tc)
753            return
754        elif chr(key) in self.validchars: # valid char pressed?
755            event.Skip()
756            self.TestValid(tc)
757            return
758        if not wx.Validator_IsSilent():
759            wx.Bell()
760        return  # Returning without calling event.Skip, which eats the keystroke
761
762################################################################################
763#### Edit a large number of values
764################################################################################
765def CallScrolledMultiEditor(parent,dictlst,elemlst,prelbl=[],postlbl=[],
766                 title='Edit items',header='',size=(300,250),
767                             CopyButton=False, **kw):
768    '''Shell routine to call a ScrolledMultiEditor dialog. See
769    :class:`ScrolledMultiEditor` for parameter definitions.
770
771    :returns: True if the OK button is pressed; False if the window is closed
772      with the system menu or the Cancel button.
773
774    '''
775    dlg = ScrolledMultiEditor(parent,dictlst,elemlst,prelbl,postlbl,
776                              title,header,size,
777                              CopyButton, **kw)
778    if dlg.ShowModal() == wx.ID_OK:
779        dlg.Destroy()
780        return True
781    else:
782        dlg.Destroy()
783        return False
784
785class ScrolledMultiEditor(wx.Dialog):
786    '''Define a window for editing a potentially large number of dict- or
787    list-contained values with validation for each item. Edited values are
788    automatically placed in their source location. If invalid entries
789    are provided, the TextCtrl is turned yellow and the OK button is disabled.
790
791    The type for each TextCtrl validation is determined by the
792    initial value of the entry (int, float or string).
793    Float values can be entered in the TextCtrl as numbers or also
794    as algebraic expressions using operators + - / \* () and \*\*,
795    in addition pi, sind(), cosd(), tand(), and sqrt() can be used,
796    as well as appreviations s(), sin(), c(), cos(), t(), tan() and sq().
797
798    :param wx.Frame parent: name of parent window, or may be None
799
800    :param tuple dictlst: a list of dicts or lists containing values to edit
801
802    :param tuple elemlst: a list of keys for each item in a dictlst. Must have the
803      same length as dictlst.
804
805    :param wx.Frame parent: name of parent window, or may be None
806   
807    :param tuple prelbl: a list of labels placed before the TextCtrl for each
808      item (optional)
809   
810    :param tuple postlbl: a list of labels placed after the TextCtrl for each
811      item (optional)
812
813    :param str title: a title to place in the frame of the dialog
814
815    :param str header: text to place at the top of the window. May contain
816      new line characters.
817
818    :param wx.Size size: a size parameter that dictates the
819      size for the scrolled region of the dialog. The default is
820      (300,250).
821
822    :param bool CopyButton: if True adds a small button that copies the
823      value for the current row to all fields below (default is False)
824     
825    :param list minvals: optional list of minimum values for validation
826      of float or int values. Ignored if value is None.
827    :param list maxvals: optional list of maximum values for validation
828      of float or int values. Ignored if value is None.
829    :param list sizevals: optional list of wx.Size values for each input
830      widget. Ignored if value is None.
831     
832    :param tuple checkdictlst: an optional list of dicts or lists containing bool
833      values (similar to dictlst).
834    :param tuple checkelemlst: an optional list of dicts or lists containing bool
835      key values (similar to elemlst). Must be used with checkdictlst.
836    :param string checklabel: a string to use for each checkbutton
837     
838    :returns: the wx.Dialog created here. Use method .ShowModal() to display it.
839   
840    *Example for use of ScrolledMultiEditor:*
841
842    ::
843
844        dlg = <pkg>.ScrolledMultiEditor(frame,dictlst,elemlst,prelbl,postlbl,
845                                        header=header)
846        if dlg.ShowModal() == wx.ID_OK:
847             for d,k in zip(dictlst,elemlst):
848                 print d[k]
849
850    *Example definitions for dictlst and elemlst:*
851
852    ::
853     
854          dictlst = (dict1,list1,dict1,list1)
855          elemlst = ('a', 1, 2, 3)
856
857      This causes items dict1['a'], list1[1], dict1[2] and list1[3] to be edited.
858   
859    Note that these items must have int, float or str values assigned to
860    them. The dialog will force these types to be retained. String values
861    that are blank are marked as invalid.
862    '''
863   
864    def __init__(self,parent,dictlst,elemlst,prelbl=[],postlbl=[],
865                 title='Edit items',header='',size=(300,250),
866                 CopyButton=False,
867                 minvals=[],maxvals=[],sizevals=[],
868                 checkdictlst=[], checkelemlst=[], checklabel=""):
869        if len(dictlst) != len(elemlst):
870            raise Exception,"ScrolledMultiEditor error: len(dictlst) != len(elemlst) "+str(len(dictlst))+" != "+str(len(elemlst))
871        if len(checkdictlst) != len(checkelemlst):
872            raise Exception,"ScrolledMultiEditor error: len(checkdictlst) != len(checkelemlst) "+str(len(checkdictlst))+" != "+str(len(checkelemlst))
873        wx.Dialog.__init__( # create dialog & sizer
874            self,parent,wx.ID_ANY,title,
875            style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)
876        mainSizer = wx.BoxSizer(wx.VERTICAL)
877        self.orig = []
878        self.dictlst = dictlst
879        self.elemlst = elemlst
880        self.checkdictlst = checkdictlst
881        self.checkelemlst = checkelemlst
882        self.StartCheckValues = [checkdictlst[i][checkelemlst[i]] for i in range(len(checkdictlst))]
883        self.ButtonIndex = {}
884        for d,i in zip(dictlst,elemlst):
885            self.orig.append(d[i])
886        # add a header if supplied
887        if header:
888            subSizer = wx.BoxSizer(wx.HORIZONTAL)
889            subSizer.Add((-1,-1),1,wx.EXPAND)
890            subSizer.Add(wx.StaticText(self,wx.ID_ANY,header))
891            subSizer.Add((-1,-1),1,wx.EXPAND)
892            mainSizer.Add(subSizer,0,wx.EXPAND,0)
893        # make OK button now, because we will need it for validation
894        self.OKbtn = wx.Button(self, wx.ID_OK)
895        self.OKbtn.SetDefault()
896        # create scrolled panel and sizer
897        panel = wxscroll.ScrolledPanel(self, wx.ID_ANY,size=size,
898            style = wx.TAB_TRAVERSAL|wx.SUNKEN_BORDER)
899        cols = 4
900        if CopyButton: cols += 1
901        subSizer = wx.FlexGridSizer(cols=cols,hgap=2,vgap=2)
902        self.ValidatedControlsList = [] # make list of TextCtrls
903        self.CheckControlsList = [] # make list of CheckBoxes
904        for i,(d,k) in enumerate(zip(dictlst,elemlst)):
905            if i >= len(prelbl): # label before TextCtrl, or put in a blank
906                subSizer.Add((-1,-1)) 
907            else:
908                subSizer.Add(wx.StaticText(panel,wx.ID_ANY,str(prelbl[i])))
909            kargs = {}
910            if i < len(minvals):
911                if minvals[i] is not None: kargs['min']=minvals[i]
912            if i < len(maxvals):
913                if maxvals[i] is not None: kargs['max']=maxvals[i]
914            if i < len(sizevals):
915                if sizevals[i]: kargs['size']=sizevals[i]
916            if CopyButton:
917                import wx.lib.colourselect as wscs
918                but = wscs.ColourSelect(label='v', # would like to use u'\u2193' or u'\u25BC' but not in WinXP
919                                        # is there a way to test?
920                                        parent=panel,
921                                        colour=(255,255,200),
922                                        size=wx.Size(30,23),
923                                        style=wx.RAISED_BORDER)
924                but.Bind(wx.EVT_BUTTON, self._OnCopyButton)
925                but.SetToolTipString('Press to copy adjacent value to all rows below')
926                self.ButtonIndex[but] = i
927                subSizer.Add(but)
928            # create the validated TextCrtl, store it and add it to the sizer
929            ctrl = ValidatedTxtCtrl(panel,d,k,OKcontrol=self.ControlOKButton,
930                                    **kargs)
931            self.ValidatedControlsList.append(ctrl)
932            subSizer.Add(ctrl)
933            if i < len(postlbl): # label after TextCtrl, or put in a blank
934                subSizer.Add(wx.StaticText(panel,wx.ID_ANY,str(postlbl[i])))
935            else:
936                subSizer.Add((-1,-1))
937            if i < len(checkdictlst):
938                ch = G2CheckBox(panel,checklabel,checkdictlst[i],checkelemlst[i])
939                self.CheckControlsList.append(ch)
940                subSizer.Add(ch)                   
941            else:
942                subSizer.Add((-1,-1))
943        # finish up ScrolledPanel
944        panel.SetSizer(subSizer)
945        panel.SetAutoLayout(1)
946        panel.SetupScrolling()
947        # patch for wx 2.9 on Mac
948        i,j= wx.__version__.split('.')[0:2]
949        if int(i)+int(j)/10. > 2.8 and 'wxOSX' in wx.PlatformInfo:
950            panel.SetMinSize((subSizer.GetSize()[0]+30,panel.GetSize()[1]))       
951        mainSizer.Add(panel,1, wx.ALL|wx.EXPAND,1)
952
953        # Sizer for OK/Close buttons. N.B. on Close changes are discarded
954        # by restoring the initial values
955        btnsizer = wx.BoxSizer(wx.HORIZONTAL)
956        btnsizer.Add(self.OKbtn)
957        btn = wx.Button(self, wx.ID_CLOSE,"Cancel") 
958        btn.Bind(wx.EVT_BUTTON,self._onClose)
959        btnsizer.Add(btn)
960        mainSizer.Add(btnsizer, 0, wx.ALIGN_CENTER|wx.ALL, 5)
961        # size out the window. Set it to be enlarged but not made smaller
962        self.SetSizer(mainSizer)
963        mainSizer.Fit(self)
964        self.SetMinSize(self.GetSize())
965
966    def _OnCopyButton(self,event):
967        'Implements the copy down functionality'
968        but = event.GetEventObject()
969        n = self.ButtonIndex.get(but)
970        if n is None: return
971        for i,(d,k,ctrl) in enumerate(zip(self.dictlst,self.elemlst,self.ValidatedControlsList)):
972            if i < n: continue
973            if i == n:
974                val = d[k]
975                continue
976            d[k] = val
977            ctrl.SetValue(val)
978        for i in range(len(self.checkdictlst)):
979            if i < n: continue
980            self.checkdictlst[i][self.checkelemlst[i]] = self.checkdictlst[n][self.checkelemlst[n]]
981            self.CheckControlsList[i].SetValue(self.checkdictlst[i][self.checkelemlst[i]])
982    def _onClose(self,event):
983        'Used on Cancel: Restore original values & close the window'
984        for d,i,v in zip(self.dictlst,self.elemlst,self.orig):
985            d[i] = v
986        for i in range(len(self.checkdictlst)):
987            self.checkdictlst[i][self.checkelemlst[i]] = self.StartCheckValues[i]
988        self.EndModal(wx.ID_CANCEL)
989       
990    def ControlOKButton(self,setvalue):
991        '''Enable or Disable the OK button for the dialog. Note that this is
992        passed into the ValidatedTxtCtrl for use by validators.
993
994        :param bool setvalue: if True, all entries in the dialog are
995          checked for validity. if False then the OK button is disabled.
996
997        '''
998        if setvalue: # turn button on, do only if all controls show as valid
999            for ctrl in self.ValidatedControlsList:
1000                if ctrl.invalid:
1001                    self.OKbtn.Disable()
1002                    return
1003            else:
1004                self.OKbtn.Enable()
1005        else:
1006            self.OKbtn.Disable()
1007
1008################################################################################
1009####
1010################################################################################
1011def SelectEdit1Var(G2frame,array,labelLst,elemKeysLst,dspLst,refFlgElem):
1012    '''Select a variable from a list, then edit it and select histograms
1013    to copy it to.
1014
1015    :param wx.Frame G2frame: main GSAS-II frame
1016    :param dict array: the array (dict or list) where values to be edited are kept
1017    :param list labelLst: labels for each data item
1018    :param list elemKeysLst: a list of lists of keys needed to be applied (see below)
1019      to obtain the value of each parameter
1020    :param list dspLst: list list of digits to be displayed (10,4) is 10 digits
1021      with 4 decimal places. Can be None.
1022    :param list refFlgElem: a list of lists of keys needed to be applied (see below)
1023      to obtain the refine flag for each parameter or None if the parameter
1024      does not have refine flag.
1025
1026    Example::
1027      array = data
1028      labelLst = ['v1','v2']
1029      elemKeysLst = [['v1'], ['v2',0]]
1030      refFlgElem = [None, ['v2',1]]
1031
1032     * The value for v1 will be in data['v1'] and this cannot be refined while,
1033     * The value for v2 will be in data['v2'][0] and its refinement flag is data['v2'][1]
1034    '''
1035    def unkey(dct,keylist):
1036        '''dive into a nested set of dicts/lists applying keys in keylist
1037        consecutively
1038        '''
1039        d = dct
1040        for k in keylist:
1041            d = d[k]
1042        return d
1043
1044    def OnChoice(event):
1045        'Respond when a parameter is selected in the Choice box'
1046        valSizer.DeleteWindows()
1047        lbl = event.GetString()
1048        copyopts['currentsel'] = lbl
1049        i = labelLst.index(lbl)
1050        OKbtn.Enable(True)
1051        ch.SetLabel(lbl)
1052        args = {}
1053        if dspLst[i]:
1054            args = {'nDig':dspLst[i]}
1055        Val = ValidatedTxtCtrl(
1056            dlg,
1057            unkey(array,elemKeysLst[i][:-1]),
1058            elemKeysLst[i][-1],
1059            **args)
1060        copyopts['startvalue'] = unkey(array,elemKeysLst[i])
1061        #unkey(array,elemKeysLst[i][:-1])[elemKeysLst[i][-1]] =
1062        valSizer.Add(Val,0,wx.LEFT,5)
1063        dlg.SendSizeEvent()
1064       
1065    # SelectEdit1Var execution begins here
1066    saveArray = copy.deepcopy(array) # keep original values
1067    TreeItemType = G2frame.PatternTree.GetItemText(G2frame.PickId)
1068    copyopts = {'InTable':False,"startvalue":None,'currentsel':None}       
1069    hst = G2frame.PatternTree.GetItemText(G2frame.PatternId)
1070    histList = G2pdG.GetHistsLikeSelected(G2frame)
1071    if not histList:
1072        G2frame.ErrorDialog('No match','No histograms match '+hst,G2frame.dataFrame)
1073        return
1074    dlg = wx.Dialog(G2frame.dataDisplay,wx.ID_ANY,'Set a parameter value',
1075        style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)
1076    mainSizer = wx.BoxSizer(wx.VERTICAL)
1077    mainSizer.Add((5,5))
1078    subSizer = wx.BoxSizer(wx.HORIZONTAL)
1079    subSizer.Add((-1,-1),1,wx.EXPAND)
1080    subSizer.Add(wx.StaticText(dlg,wx.ID_ANY,'Select a parameter and set a new value'))
1081    subSizer.Add((-1,-1),1,wx.EXPAND)
1082    mainSizer.Add(subSizer,0,wx.EXPAND,0)
1083    mainSizer.Add((0,10))
1084
1085    subSizer = wx.FlexGridSizer(0,2,5,0)
1086    subSizer.Add(wx.StaticText(dlg,wx.ID_ANY,'Parameter: '))
1087    ch = wx.Choice(dlg, wx.ID_ANY, choices = sorted(labelLst))
1088    ch.SetSelection(-1)
1089    ch.Bind(wx.EVT_CHOICE, OnChoice)
1090    subSizer.Add(ch)
1091    subSizer.Add(wx.StaticText(dlg,wx.ID_ANY,'Value: '))
1092    valSizer = wx.BoxSizer(wx.HORIZONTAL)
1093    subSizer.Add(valSizer)
1094    mainSizer.Add(subSizer)
1095
1096    mainSizer.Add((-1,20))
1097    subSizer = wx.BoxSizer(wx.HORIZONTAL)
1098    subSizer.Add(G2CheckBox(dlg, 'Edit in table ', copyopts, 'InTable'))
1099    mainSizer.Add(subSizer)
1100
1101    btnsizer = wx.StdDialogButtonSizer()
1102    OKbtn = wx.Button(dlg, wx.ID_OK,'Continue')
1103    OKbtn.Enable(False)
1104    OKbtn.SetDefault()
1105    OKbtn.Bind(wx.EVT_BUTTON,lambda event: dlg.EndModal(wx.ID_OK))
1106    btnsizer.AddButton(OKbtn)
1107    btn = wx.Button(dlg, wx.ID_CANCEL)
1108    btnsizer.AddButton(btn)
1109    btnsizer.Realize()
1110    mainSizer.Add((-1,5),1,wx.EXPAND,1)
1111    mainSizer.Add(btnsizer,0,wx.ALIGN_CENTER,0)
1112    mainSizer.Add((-1,10))
1113
1114    dlg.SetSizer(mainSizer)
1115    dlg.CenterOnParent()
1116    if dlg.ShowModal() != wx.ID_OK:
1117        array.update(saveArray)
1118        dlg.Destroy()
1119        return
1120    dlg.Destroy()
1121
1122    copyList = []
1123    lbl = copyopts['currentsel']
1124    dlg = G2MultiChoiceDialog(
1125        G2frame.dataFrame, 
1126        'Copy parameter '+lbl+' from\n'+hst,
1127        'Copy parameters', histList)
1128    dlg.CenterOnParent()
1129    try:
1130        if dlg.ShowModal() == wx.ID_OK:
1131            for i in dlg.GetSelections(): 
1132                copyList.append(histList[i])
1133        else:
1134            # reset the parameter since cancel was pressed
1135            array.update(saveArray)
1136            return
1137    finally:
1138        dlg.Destroy()
1139
1140    prelbl = [hst]
1141    i = labelLst.index(lbl)
1142    keyLst = elemKeysLst[i]
1143    refkeys = refFlgElem[i]
1144    dictlst = [unkey(array,keyLst[:-1])]
1145    if refkeys is not None:
1146        refdictlst = [unkey(array,refkeys[:-1])]
1147    else:
1148        refdictlst = None
1149    Id = GetPatternTreeItemId(G2frame,G2frame.root,hst)
1150    hstData = G2frame.PatternTree.GetItemPyData(GetPatternTreeItemId(G2frame,Id,'Instrument Parameters'))[0]
1151    for h in copyList:
1152        Id = GetPatternTreeItemId(G2frame,G2frame.root,h)
1153        instData = G2frame.PatternTree.GetItemPyData(GetPatternTreeItemId(G2frame,Id,'Instrument Parameters'))[0]
1154        if len(hstData) != len(instData) or hstData['Type'][0] != instData['Type'][0]:  #don't mix data types or lam & lam1/lam2 parms!
1155            print h+' not copied - instrument parameters not commensurate'
1156            continue
1157        hData = G2frame.PatternTree.GetItemPyData(GetPatternTreeItemId(G2frame,Id,TreeItemType))
1158        if TreeItemType == 'Instrument Parameters':
1159            hData = hData[0]
1160        #copy the value if it is changed or we will not edit in a table
1161        valNow = unkey(array,keyLst)
1162        if copyopts['startvalue'] != valNow or not copyopts['InTable']:
1163            unkey(hData,keyLst[:-1])[keyLst[-1]] = valNow
1164        prelbl += [h]
1165        dictlst += [unkey(hData,keyLst[:-1])]
1166        if refdictlst is not None:
1167            refdictlst += [unkey(hData,refkeys[:-1])]
1168    if refdictlst is None:
1169        args = {}
1170    else:
1171        args = {'checkdictlst':refdictlst,
1172                'checkelemlst':len(dictlst)*[refkeys[-1]],
1173                'checklabel':'Refine?'}
1174    if copyopts['InTable']:
1175        dlg = ScrolledMultiEditor(
1176            G2frame.dataDisplay,dictlst,
1177            len(dictlst)*[keyLst[-1]],prelbl,
1178            header='Editing parameter '+lbl,
1179            CopyButton=True,**args)
1180        dlg.CenterOnParent()
1181        if dlg.ShowModal() != wx.ID_OK:
1182            array.update(saveArray)
1183        dlg.Destroy()
1184
1185################################################################################
1186#### Custom checkbox that saves values into dict/list as used
1187################################################################################
1188class G2CheckBox(wx.CheckBox):
1189    '''A customized version of a CheckBox that automatically initializes
1190    the control to a supplied list or dict entry and updates that
1191    entry as the widget is used.
1192
1193    :param wx.Panel parent: name of panel or frame that will be
1194      the parent to the widget. Can be None.
1195    :param str label: text to put on check button
1196    :param dict/list loc: the dict or list with the initial value to be
1197      placed in the CheckBox.
1198    :param int/str key: the dict key or the list index for the value to be
1199      edited by the CheckBox. The ``loc[key]`` element must exist.
1200      The CheckBox will be initialized from this value.
1201      If the value is anything other that True (or 1), it will be taken as
1202      False.
1203    '''
1204    def __init__(self,parent,label,loc,key):
1205        wx.CheckBox.__init__(self,parent,id=wx.ID_ANY,label=label)
1206        self.loc = loc
1207        self.key = key
1208        self.SetValue(self.loc[self.key]==True)
1209        self.Bind(wx.EVT_CHECKBOX, self._OnCheckBox)
1210    def _OnCheckBox(self,event):
1211        self.loc[self.key] = self.GetValue()
1212        log.LogVarChange(self.loc,self.key)
1213
1214################################################################################
1215####
1216################################################################################
1217class PickTwoDialog(wx.Dialog):
1218    '''This does not seem to be in use
1219    '''
1220    def __init__(self,parent,title,prompt,names,choices):
1221        wx.Dialog.__init__(self,parent,-1,title, 
1222            pos=wx.DefaultPosition,style=wx.DEFAULT_DIALOG_STYLE)
1223        self.panel = wx.Panel(self)         #just a dummy - gets destroyed in Draw!
1224        self.prompt = prompt
1225        self.choices = choices
1226        self.names = names
1227        self.Draw()
1228
1229    def Draw(self):
1230        Indx = {}
1231       
1232        def OnSelection(event):
1233            Obj = event.GetEventObject()
1234            id = Indx[Obj.GetId()]
1235            self.choices[id] = Obj.GetValue().encode()  #to avoid Unicode versions
1236            self.Draw()
1237           
1238        self.panel.DestroyChildren()
1239        self.panel.Destroy()
1240        self.panel = wx.Panel(self)
1241        mainSizer = wx.BoxSizer(wx.VERTICAL)
1242        mainSizer.Add(wx.StaticText(self.panel,-1,self.prompt),0,wx.ALIGN_CENTER)
1243        for isel,name in enumerate(self.choices):
1244            lineSizer = wx.BoxSizer(wx.HORIZONTAL)
1245            lineSizer.Add(wx.StaticText(self.panel,-1,'Reference atom '+str(isel+1)),0,wx.ALIGN_CENTER)
1246            nameList = self.names[:]
1247            if isel:
1248                if self.choices[0] in nameList:
1249                    nameList.remove(self.choices[0])
1250            choice = wx.ComboBox(self.panel,-1,value=name,choices=nameList,
1251                style=wx.CB_READONLY|wx.CB_DROPDOWN)
1252            Indx[choice.GetId()] = isel
1253            choice.Bind(wx.EVT_COMBOBOX, OnSelection)
1254            lineSizer.Add(choice,0,WACV)
1255            mainSizer.Add(lineSizer)
1256        OkBtn = wx.Button(self.panel,-1,"Ok")
1257        OkBtn.Bind(wx.EVT_BUTTON, self.OnOk)
1258        CancelBtn = wx.Button(self.panel,-1,'Cancel')
1259        CancelBtn.Bind(wx.EVT_BUTTON, self.OnCancel)
1260        btnSizer = wx.BoxSizer(wx.HORIZONTAL)
1261        btnSizer.Add((20,20),1)
1262        btnSizer.Add(OkBtn)
1263        btnSizer.Add(CancelBtn)
1264        btnSizer.Add((20,20),1)
1265        mainSizer.Add(btnSizer,0,wx.EXPAND|wx.BOTTOM|wx.TOP, 10)
1266        self.panel.SetSizer(mainSizer)
1267        self.panel.Fit()
1268        self.Fit()
1269       
1270    def GetSelection(self):
1271        return self.choices
1272
1273    def OnOk(self,event):
1274        parent = self.GetParent()
1275        parent.Raise()
1276        self.EndModal(wx.ID_OK)             
1277       
1278    def OnCancel(self,event):
1279        parent = self.GetParent()
1280        parent.Raise()
1281        self.EndModal(wx.ID_CANCEL)
1282
1283################################################################################
1284#### Column-order selection
1285################################################################################
1286
1287def GetItemOrder(parent,keylist,vallookup,posdict):
1288    '''Creates a panel where items can be ordered into columns
1289    :param list keylist: is a list of keys for column assignments
1290    :param dict vallookup: is a dict keyed by names in keylist where each item is a dict.
1291       Each inner dict contains variable names as keys and their associated values
1292    :param dict posdict: is a dict keyed by names in keylist where each item is a dict.
1293       Each inner dict contains column numbers as keys and their associated
1294       variable name as a value. This is used for both input and output.
1295    '''
1296    dlg = wx.Dialog(parent,style=wx.DEFAULT_DIALOG_STYLE|wx.RESIZE_BORDER)
1297    sizer = wx.BoxSizer(wx.VERTICAL)
1298    spanel = OrderBox(dlg,keylist,vallookup,posdict)
1299    spanel.Fit()
1300    sizer.Add(spanel,1,wx.EXPAND)
1301    btnsizer = wx.StdDialogButtonSizer()
1302    btn = wx.Button(dlg, wx.ID_OK)
1303    btn.SetDefault()
1304    btnsizer.AddButton(btn)
1305    #btn = wx.Button(dlg, wx.ID_CANCEL)
1306    #btnsizer.AddButton(btn)
1307    btnsizer.Realize()
1308    sizer.Add(btnsizer, 0, wx.ALIGN_CENTER_VERTICAL|wx.EXPAND|wx.ALL, 5)
1309    dlg.SetSizer(sizer)
1310    sizer.Fit(dlg)
1311    val = dlg.ShowModal()
1312
1313class OrderBox(wxscroll.ScrolledPanel):
1314    '''Creates a panel with scrollbars where items can be ordered into columns
1315    :param list keylist: is a list of keys for column assignments
1316    :param dict vallookup: is a dict keyed by names in keylist where each item is a dict.
1317      Each inner dict contains variable names as keys and their associated values
1318    :param dict posdict: is a dict keyed by names in keylist where each item is a dict.
1319      Each inner dict contains column numbers as keys and their associated
1320      variable name as a value. This is used for both input and output.
1321    '''
1322    def __init__(self,parent,keylist,vallookup,posdict,*arg,**kw):
1323        self.keylist = keylist
1324        self.vallookup = vallookup
1325        self.posdict = posdict
1326        self.maxcol = 0
1327        for nam in keylist:
1328            posdict = self.posdict[nam]
1329            if posdict.keys():
1330                self.maxcol = max(self.maxcol, max(posdict))
1331        wxscroll.ScrolledPanel.__init__(self,parent,wx.ID_ANY,*arg,**kw)
1332        self.GBsizer = wx.GridBagSizer(4,4)
1333        self.SetBackgroundColour(WHITE)
1334        self.SetSizer(self.GBsizer)
1335        colList = [str(i) for i in range(self.maxcol+2)]
1336        for i in range(self.maxcol+1):
1337            wid = wx.StaticText(self,wx.ID_ANY,str(i),style=wx.ALIGN_CENTER)
1338            wid.SetBackgroundColour(DULL_YELLOW)
1339            wid.SetMinSize((50,-1))
1340            self.GBsizer.Add(wid,(0,i),flag=wx.ALIGN_CENTER|wx.EXPAND)
1341        self.chceDict = {}
1342        for row,nam in enumerate(self.keylist):
1343            posdict = self.posdict[nam]
1344            for col in posdict:
1345                lbl = posdict[col]
1346                pnl = wx.Panel(self,wx.ID_ANY)
1347                pnl.SetBackgroundColour(VERY_LIGHT_GREY)
1348                insize = wx.BoxSizer(wx.VERTICAL)
1349                wid = wx.Choice(pnl,wx.ID_ANY,choices=colList)
1350                insize.Add(wid,0,wx.EXPAND|wx.BOTTOM,3)
1351                wid.SetSelection(col)
1352                self.chceDict[wid] = (row,col)
1353                wid.Bind(wx.EVT_CHOICE,self.OnChoice)
1354                wid = wx.StaticText(pnl,wx.ID_ANY,lbl)
1355                insize.Add(wid,0,flag=wx.EXPAND)
1356                val = G2py3.FormatSigFigs(self.vallookup[nam][lbl],maxdigits=8)
1357                wid = wx.StaticText(pnl,wx.ID_ANY,'('+val+')')
1358                insize.Add(wid,0,flag=wx.EXPAND)
1359                pnl.SetSizer(insize)
1360                self.GBsizer.Add(pnl,(row+1,col),flag=wx.EXPAND)
1361        self.SetAutoLayout(1)
1362        self.SetupScrolling()
1363        self.SetMinSize((
1364            min(700,self.GBsizer.GetSize()[0]),
1365            self.GBsizer.GetSize()[1]+20))
1366    def OnChoice(self,event):
1367        '''Called when a column is assigned to a variable
1368        '''
1369        row,col = self.chceDict[event.EventObject] # which variable was this?
1370        newcol = event.Selection # where will it be moved?
1371        if newcol == col:
1372            return # no change: nothing to do!
1373        prevmaxcol = self.maxcol # save current table size
1374        key = self.keylist[row] # get the key for the current row
1375        lbl = self.posdict[key][col] # selected variable name
1376        lbl1 = self.posdict[key].get(col+1,'') # next variable name, if any
1377        # if a posXXX variable is selected, and the next variable is posXXX, move them together
1378        repeat = 1
1379        if lbl[:3] == 'pos' and lbl1[:3] == 'int' and lbl[3:] == lbl1[3:]:
1380            repeat = 2
1381        for i in range(repeat): # process the posXXX and then the intXXX (or a single variable)
1382            col += i
1383            newcol += i
1384            if newcol in self.posdict[key]:
1385                # find first non-blank after newcol
1386                for mtcol in range(newcol+1,self.maxcol+2):
1387                    if mtcol not in self.posdict[key]: break
1388                l1 = range(mtcol,newcol,-1)+[newcol]
1389                l = range(mtcol-1,newcol-1,-1)+[col]
1390            else:
1391                l1 = [newcol]
1392                l = [col]
1393            # move all of the items, starting from the last column
1394            for newcol,col in zip(l1,l):
1395                #print 'moving',col,'to',newcol
1396                self.posdict[key][newcol] = self.posdict[key][col]
1397                del self.posdict[key][col]
1398                self.maxcol = max(self.maxcol,newcol)
1399                obj = self.GBsizer.FindItemAtPosition((row+1,col))
1400                self.GBsizer.SetItemPosition(obj.GetWindow(),(row+1,newcol))
1401                for wid in obj.GetWindow().Children:
1402                    if wid in self.chceDict:
1403                        self.chceDict[wid] = (row,newcol)
1404                        wid.SetSelection(self.chceDict[wid][1])
1405        # has the table gotten larger? If so we need new column heading(s)
1406        if prevmaxcol != self.maxcol:
1407            for i in range(prevmaxcol+1,self.maxcol+1):
1408                wid = wx.StaticText(self,wx.ID_ANY,str(i),style=wx.ALIGN_CENTER)
1409                wid.SetBackgroundColour(DULL_YELLOW)
1410                wid.SetMinSize((50,-1))
1411                self.GBsizer.Add(wid,(0,i),flag=wx.ALIGN_CENTER|wx.EXPAND)
1412            colList = [str(i) for i in range(self.maxcol+2)]
1413            for wid in self.chceDict:
1414                wid.SetItems(colList)
1415                wid.SetSelection(self.chceDict[wid][1])
1416        self.GBsizer.Layout()
1417        self.FitInside()
1418       
1419if __name__ == '__main__':
1420    app = wx.PySimpleApp()
1421    frm = wx.Frame(None) # create a frame
1422    frm.Show(True)
1423
1424    #======================================================================
1425    # test ScrolledMultiEditor
1426    #======================================================================
1427    # Data1 = {
1428    #      'Order':1,
1429    #      'omega':'string',
1430    #      'chi':2.0,
1431    #      'phi':'',
1432    #      }
1433    # elemlst = sorted(Data1.keys())
1434    # prelbl = sorted(Data1.keys())
1435    # dictlst = len(elemlst)*[Data1,]
1436    #Data2 = [True,False,False,True]
1437    #Checkdictlst = len(Data2)*[Data2,]
1438    #Checkelemlst = range(len(Checkdictlst))
1439    # print 'before',Data1,'\n',Data2
1440    # dlg = ScrolledMultiEditor(
1441    #     frm,dictlst,elemlst,prelbl,
1442    #     checkdictlst=Checkdictlst,checkelemlst=Checkelemlst,
1443    #     checklabel="Refine?",
1444    #     header="test")
1445    # if dlg.ShowModal() == wx.ID_OK:
1446    #     print "OK"
1447    # else:
1448    #     print "Cancel"
1449    # print 'after',Data1,'\n',Data2
1450    # dlg.Destroy()
1451    Data3 = {
1452         'Order':1.0,
1453         'omega':1.1,
1454         'chi':2.0,
1455         'phi':2.3,
1456         'Order1':1.0,
1457         'omega1':1.1,
1458         'chi1':2.0,
1459         'phi1':2.3,
1460         'Order2':1.0,
1461         'omega2':1.1,
1462         'chi2':2.0,
1463         'phi2':2.3,
1464         }
1465    elemlst = sorted(Data3.keys())
1466    dictlst = len(elemlst)*[Data3,]
1467    prelbl = elemlst[:]
1468    prelbl[0]="this is a much longer label to stretch things out"
1469    Data2 = len(elemlst)*[False,]
1470    Data2[1] = Data2[3] = True
1471    Checkdictlst = len(elemlst)*[Data2,]
1472    Checkelemlst = range(len(Checkdictlst))
1473    #print 'before',Data3,'\n',Data2
1474    #print dictlst,"\n",elemlst
1475    #print Checkdictlst,"\n",Checkelemlst
1476    dlg = ScrolledMultiEditor(
1477        frm,dictlst,elemlst,prelbl,
1478        checkdictlst=Checkdictlst,checkelemlst=Checkelemlst,
1479        checklabel="Refine?",
1480        header="test",CopyButton=True)
1481    if dlg.ShowModal() == wx.ID_OK:
1482        print "OK"
1483    else:
1484        print "Cancel"
1485    #print 'after',Data3,'\n',Data2
1486
1487    # Data2 = list(range(100))
1488    # elemlst += range(2,6)
1489    # postlbl += range(2,6)
1490    # dictlst += len(range(2,6))*[Data2,]
1491
1492    # prelbl = range(len(elemlst))
1493    # postlbl[1] = "a very long label for the 2nd item to force a horiz. scrollbar"
1494    # header="""This is a longer\nmultiline and perhaps silly header"""
1495    # dlg = ScrolledMultiEditor(frm,dictlst,elemlst,prelbl,postlbl,
1496    #                           header=header,CopyButton=True)
1497    # print Data1
1498    # if dlg.ShowModal() == wx.ID_OK:
1499    #     for d,k in zip(dictlst,elemlst):
1500    #         print k,d[k]
1501    # dlg.Destroy()
1502    # if CallScrolledMultiEditor(frm,dictlst,elemlst,prelbl,postlbl,
1503    #                            header=header):
1504    #     for d,k in zip(dictlst,elemlst):
1505    #         print k,d[k]
1506
1507    #======================================================================
1508    # test G2MultiChoiceDialog
1509    #======================================================================
1510    # choices = []
1511    # for i in range(21):
1512    #     choices.append("option_"+str(i))
1513    # dlg = G2MultiChoiceDialog(frm, 'Sequential refinement',
1514    #                           'Select dataset to include',
1515    #                           choices)
1516    # sel = range(2,11,2)
1517    # dlg.SetSelections(sel)
1518    # dlg.SetSelections((1,5))
1519    # if dlg.ShowModal() == wx.ID_OK:
1520    #     for sel in dlg.GetSelections():
1521    #         print sel,choices[sel]
1522   
1523    #======================================================================
1524    # test wx.MultiChoiceDialog
1525    #======================================================================
1526    # dlg = wx.MultiChoiceDialog(frm, 'Sequential refinement',
1527    #                           'Select dataset to include',
1528    #                           choices)
1529    # sel = range(2,11,2)
1530    # dlg.SetSelections(sel)
1531    # dlg.SetSelections((1,5))
1532    # if dlg.ShowModal() == wx.ID_OK:
1533    #     for sel in dlg.GetSelections():
1534    #         print sel,choices[sel]
1535
1536    pnl = wx.Panel(frm)
1537    siz = wx.BoxSizer(wx.VERTICAL)
1538
1539    td = {'Goni':200.,'a':1.,'calc':1./3.,'string':'s'}
1540    for key in sorted(td):
1541        txt = ValidatedTxtCtrl(pnl,td,key)
1542        siz.Add(txt)
1543    pnl.SetSizer(siz)
1544    siz.Fit(frm)
1545    app.MainLoop()
1546    print td
Note: See TracBrowser for help on using the repository browser.