source: trunk/GSASIIctrls.py @ 2751

Last change on this file since 2751 was 2751, checked in by vondreele, 5 years ago

fix bug setting atom parms to zero.

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